From a5a45e92ae8f131071d469608f595f43ccc489bd Mon Sep 17 00:00:00 2001 From: Llewellyn van der Merwe Date: Tue, 24 May 2022 08:59:17 +0200 Subject: [PATCH] first commit --- .gitignore | 9 + .htaccess | 52 + LICENSE | 340 ++ README.md | 63 + administrator/favicon.ico | Bin 0 -> 1150 bytes administrator/includes/app.php | 100 + administrator/includes/defines.php | 24 + administrator/includes/framework.php | 35 + administrator/includes/index.html | 1 + administrator/index.php | 35 + administrator/media/css/index.html | 1 + administrator/media/css/template.css | 0 administrator/media/index.html | 1 + administrator/media/js/index.html | 1 + administrator/media/js/template.js | 0 administrator/media/mix-manifest.json | 1 + administrator/media/sri-manifest.json | 1 + composer.json | 72 + composer.lock | 5398 +++++++++++++++++ config.php.example | 95 + favicon.ico | Bin 0 -> 1150 bytes htaccess.txt | 52 + includes/app.php | 96 + includes/defines.php | 23 + includes/framework.php | 35 + includes/index.html | 1 + index.php | 35 + installation/includes/app.php | 31 + installation/includes/defines.php | 23 + installation/includes/framework.php | 41 + installation/index.php | 35 + libraries/.htaccess | 9 + libraries/bootstrap.php | 48 + libraries/loader.php | 599 ++ .../src/Application/AdminApplication.php | 90 + .../Application/IdentityAwareInterface.php | 57 + .../src/Application/IdentityAwareTrait.php | 91 + .../SessionMessageAwareInterface.php | 44 + .../Application/SessionMessageAwareTrait.php | 88 + libraries/src/Application/SiteApplication.php | 86 + libraries/src/Asset/MixPathPackage.php | 71 + libraries/src/Autoload/ClassLoader.php | 63 + .../src/Controller/DashboardController.php | 91 + libraries/src/Controller/ItemController.php | 272 + libraries/src/Controller/ItemsController.php | 94 + libraries/src/Controller/LoginController.php | 95 + libraries/src/Controller/MenuController.php | 249 + libraries/src/Controller/MenusController.php | 94 + libraries/src/Controller/PageController.php | 105 + libraries/src/Controller/UserController.php | 452 ++ .../src/Controller/UserGroupController.php | 262 + .../src/Controller/UsergroupsController.php | 94 + libraries/src/Controller/UsersController.php | 93 + .../src/Controller/Util/AccessInterface.php | 28 + libraries/src/Controller/Util/AccessTrait.php | 111 + .../Controller/Util/CheckTokenInterface.php | 26 + .../src/Controller/Util/CheckTokenTrait.php | 36 + .../src/Controller/WrongCmsController.php | 40 + libraries/src/Date/Date.php | 484 ++ .../src/EventListener/ErrorSubscriber.php | 195 + libraries/src/Factory.php | 301 + libraries/src/Filter/InputFilter.php | 527 ++ libraries/src/Model/DashboardModel.php | 47 + libraries/src/Model/ItemModel.php | 287 + libraries/src/Model/ItemsModel.php | 54 + libraries/src/Model/MenuModel.php | 353 ++ libraries/src/Model/MenusModel.php | 62 + libraries/src/Model/PageModel.php | 98 + libraries/src/Model/UserModel.php | 337 + libraries/src/Model/UsergroupModel.php | 239 + libraries/src/Model/UsergroupsModel.php | 50 + libraries/src/Model/UsersModel.php | 67 + .../src/Model/Util/GetUsergroupsInterface.php | 37 + .../src/Model/Util/GetUsergroupsTrait.php | 103 + .../src/Model/Util/HomeMenuInterface.php | 24 + libraries/src/Model/Util/HomeMenuTrait.php | 51 + libraries/src/Model/Util/MenuInterface.php | 28 + libraries/src/Model/Util/PageInterface.php | 41 + libraries/src/Model/Util/SelectMenuTrait.php | 54 + libraries/src/Model/Util/SiteMenuTrait.php | 70 + libraries/src/Model/Util/SitePageTrait.php | 103 + libraries/src/Model/Util/UniqueInterface.php | 44 + .../src/Model/Util/UniqueMenuAliasTrait.php | 103 + libraries/src/Renderer/ApplicationContext.php | 62 + libraries/src/Renderer/FrameworkExtension.php | 67 + .../src/Renderer/FrameworkTwigRuntime.php | 278 + .../src/Service/AdminApplicationProvider.php | 103 + libraries/src/Service/AdminMVCProvider.php | 605 ++ libraries/src/Service/AdminRouterProvider.php | 108 + .../src/Service/AdminTemplatingProvider.php | 314 + .../src/Service/ConfigurationProvider.php | 80 + libraries/src/Service/EventProvider.php | 75 + libraries/src/Service/HttpProvider.php | 64 + libraries/src/Service/InputProvider.php | 46 + libraries/src/Service/LoggingProvider.php | 132 + libraries/src/Service/SessionProvider.php | 126 + .../src/Service/SiteApplicationProvider.php | 265 + .../src/Service/SiteTemplatingProvider.php | 313 + libraries/src/Service/UserProvider.php | 81 + libraries/src/Session/MetadataManager.php | 326 + libraries/src/String/PunycodeHelper.php | 260 + libraries/src/User/User.php | 179 + libraries/src/User/UserFactory.php | 645 ++ libraries/src/User/UserFactoryInterface.php | 129 + libraries/src/Utilities/ArrayHelper.php | 79 + libraries/src/Utilities/StringHelper.php | 322 + .../src/View/Admin/DashboardHtmlView.php | 83 + libraries/src/View/Admin/ItemHtmlView.php | 84 + libraries/src/View/Admin/ItemsHtmlView.php | 64 + libraries/src/View/Admin/MenuHtmlView.php | 95 + libraries/src/View/Admin/MenusHtmlView.php | 64 + libraries/src/View/Admin/UserHtmlView.php | 87 + .../src/View/Admin/UsergroupHtmlView.php | 85 + .../src/View/Admin/UsergroupsHtmlView.php | 64 + libraries/src/View/Admin/UsersHtmlView.php | 64 + libraries/src/View/Site/PageHtmlView.php | 73 + libraries/web.config | 8 + logs/framework.log | 1 + logs/index.html | 1 + media/css/index.html | 1 + media/css/template.css | 0 media/images/index.html | 1 + media/images/tutorial_thumb.jpg | Bin 0 -> 163071 bytes media/index.html | 1 + media/js/index.html | 1 + media/js/template.js | 0 media/mix-manifest.json | 1 + media/sri-manifest.json | 1 + robots.txt.dist | 28 + sql/index.html | 1 + sql/install.sql | 263 + templates/admin/dashboard.twig | 57 + templates/admin/exception.twig | 29 + templates/admin/footer.twig | 9 + templates/admin/header.twig | 45 + templates/admin/index.html | 1 + templates/admin/index.twig | 13 + templates/admin/item.twig | 120 + templates/admin/items.twig | 79 + templates/admin/login.twig | 20 + templates/admin/menu.twig | 154 + templates/admin/menus.twig | 90 + templates/admin/message_queue.twig | 20 + templates/admin/nav.twig | 30 + templates/admin/signup.twig | 23 + templates/admin/user.twig | 116 + templates/admin/usergroup.twig | 46 + templates/admin/usergroups.twig | 82 + templates/admin/users.twig | 112 + templates/index.html | 1 + templates/site/exception.twig | 27 + templates/site/footer.twig | 9 + templates/site/header.twig | 43 + templates/site/index.html | 1 + templates/site/index.twig | 12 + templates/site/nav.twig | 258 + templates/site/page.twig | 21 + templates/system/build_incomplete.html | 34 + templates/system/incompatible.html | 27 + templates/system/index.html | 1 + templates/system/install_notice.html | 38 + tmp/index.html | 1 + web.config.txt | 36 + 163 files changed, 21033 insertions(+) create mode 100644 .gitignore create mode 100644 .htaccess create mode 100644 LICENSE create mode 100644 README.md create mode 100644 administrator/favicon.ico create mode 100644 administrator/includes/app.php create mode 100644 administrator/includes/defines.php create mode 100644 administrator/includes/framework.php create mode 100644 administrator/includes/index.html create mode 100644 administrator/index.php create mode 100644 administrator/media/css/index.html create mode 100644 administrator/media/css/template.css create mode 100644 administrator/media/index.html create mode 100644 administrator/media/js/index.html create mode 100644 administrator/media/js/template.js create mode 100644 administrator/media/mix-manifest.json create mode 100644 administrator/media/sri-manifest.json create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 config.php.example create mode 100644 favicon.ico create mode 100644 htaccess.txt create mode 100644 includes/app.php create mode 100644 includes/defines.php create mode 100644 includes/framework.php create mode 100644 includes/index.html create mode 100644 index.php create mode 100644 installation/includes/app.php create mode 100644 installation/includes/defines.php create mode 100644 installation/includes/framework.php create mode 100644 installation/index.php create mode 100644 libraries/.htaccess create mode 100644 libraries/bootstrap.php create mode 100644 libraries/loader.php create mode 100644 libraries/src/Application/AdminApplication.php create mode 100644 libraries/src/Application/IdentityAwareInterface.php create mode 100644 libraries/src/Application/IdentityAwareTrait.php create mode 100644 libraries/src/Application/SessionMessageAwareInterface.php create mode 100644 libraries/src/Application/SessionMessageAwareTrait.php create mode 100644 libraries/src/Application/SiteApplication.php create mode 100644 libraries/src/Asset/MixPathPackage.php create mode 100644 libraries/src/Autoload/ClassLoader.php create mode 100644 libraries/src/Controller/DashboardController.php create mode 100644 libraries/src/Controller/ItemController.php create mode 100644 libraries/src/Controller/ItemsController.php create mode 100644 libraries/src/Controller/LoginController.php create mode 100644 libraries/src/Controller/MenuController.php create mode 100644 libraries/src/Controller/MenusController.php create mode 100644 libraries/src/Controller/PageController.php create mode 100644 libraries/src/Controller/UserController.php create mode 100644 libraries/src/Controller/UserGroupController.php create mode 100644 libraries/src/Controller/UsergroupsController.php create mode 100644 libraries/src/Controller/UsersController.php create mode 100644 libraries/src/Controller/Util/AccessInterface.php create mode 100644 libraries/src/Controller/Util/AccessTrait.php create mode 100644 libraries/src/Controller/Util/CheckTokenInterface.php create mode 100644 libraries/src/Controller/Util/CheckTokenTrait.php create mode 100644 libraries/src/Controller/WrongCmsController.php create mode 100644 libraries/src/Date/Date.php create mode 100644 libraries/src/EventListener/ErrorSubscriber.php create mode 100644 libraries/src/Factory.php create mode 100644 libraries/src/Filter/InputFilter.php create mode 100644 libraries/src/Model/DashboardModel.php create mode 100644 libraries/src/Model/ItemModel.php create mode 100644 libraries/src/Model/ItemsModel.php create mode 100644 libraries/src/Model/MenuModel.php create mode 100644 libraries/src/Model/MenusModel.php create mode 100644 libraries/src/Model/PageModel.php create mode 100644 libraries/src/Model/UserModel.php create mode 100644 libraries/src/Model/UsergroupModel.php create mode 100644 libraries/src/Model/UsergroupsModel.php create mode 100644 libraries/src/Model/UsersModel.php create mode 100644 libraries/src/Model/Util/GetUsergroupsInterface.php create mode 100644 libraries/src/Model/Util/GetUsergroupsTrait.php create mode 100644 libraries/src/Model/Util/HomeMenuInterface.php create mode 100644 libraries/src/Model/Util/HomeMenuTrait.php create mode 100644 libraries/src/Model/Util/MenuInterface.php create mode 100644 libraries/src/Model/Util/PageInterface.php create mode 100644 libraries/src/Model/Util/SelectMenuTrait.php create mode 100644 libraries/src/Model/Util/SiteMenuTrait.php create mode 100644 libraries/src/Model/Util/SitePageTrait.php create mode 100644 libraries/src/Model/Util/UniqueInterface.php create mode 100644 libraries/src/Model/Util/UniqueMenuAliasTrait.php create mode 100644 libraries/src/Renderer/ApplicationContext.php create mode 100644 libraries/src/Renderer/FrameworkExtension.php create mode 100644 libraries/src/Renderer/FrameworkTwigRuntime.php create mode 100644 libraries/src/Service/AdminApplicationProvider.php create mode 100644 libraries/src/Service/AdminMVCProvider.php create mode 100644 libraries/src/Service/AdminRouterProvider.php create mode 100644 libraries/src/Service/AdminTemplatingProvider.php create mode 100644 libraries/src/Service/ConfigurationProvider.php create mode 100644 libraries/src/Service/EventProvider.php create mode 100644 libraries/src/Service/HttpProvider.php create mode 100644 libraries/src/Service/InputProvider.php create mode 100644 libraries/src/Service/LoggingProvider.php create mode 100644 libraries/src/Service/SessionProvider.php create mode 100644 libraries/src/Service/SiteApplicationProvider.php create mode 100644 libraries/src/Service/SiteTemplatingProvider.php create mode 100644 libraries/src/Service/UserProvider.php create mode 100644 libraries/src/Session/MetadataManager.php create mode 100644 libraries/src/String/PunycodeHelper.php create mode 100644 libraries/src/User/User.php create mode 100644 libraries/src/User/UserFactory.php create mode 100644 libraries/src/User/UserFactoryInterface.php create mode 100644 libraries/src/Utilities/ArrayHelper.php create mode 100644 libraries/src/Utilities/StringHelper.php create mode 100644 libraries/src/View/Admin/DashboardHtmlView.php create mode 100644 libraries/src/View/Admin/ItemHtmlView.php create mode 100644 libraries/src/View/Admin/ItemsHtmlView.php create mode 100644 libraries/src/View/Admin/MenuHtmlView.php create mode 100644 libraries/src/View/Admin/MenusHtmlView.php create mode 100644 libraries/src/View/Admin/UserHtmlView.php create mode 100644 libraries/src/View/Admin/UsergroupHtmlView.php create mode 100644 libraries/src/View/Admin/UsergroupsHtmlView.php create mode 100644 libraries/src/View/Admin/UsersHtmlView.php create mode 100644 libraries/src/View/Site/PageHtmlView.php create mode 100644 libraries/web.config create mode 100644 logs/framework.log create mode 100644 logs/index.html create mode 100644 media/css/index.html create mode 100644 media/css/template.css create mode 100644 media/images/index.html create mode 100644 media/images/tutorial_thumb.jpg create mode 100644 media/index.html create mode 100644 media/js/index.html create mode 100644 media/js/template.js create mode 100644 media/mix-manifest.json create mode 100644 media/sri-manifest.json create mode 100644 robots.txt.dist create mode 100644 sql/index.html create mode 100644 sql/install.sql create mode 100644 templates/admin/dashboard.twig create mode 100644 templates/admin/exception.twig create mode 100644 templates/admin/footer.twig create mode 100644 templates/admin/header.twig create mode 100644 templates/admin/index.html create mode 100644 templates/admin/index.twig create mode 100644 templates/admin/item.twig create mode 100644 templates/admin/items.twig create mode 100644 templates/admin/login.twig create mode 100644 templates/admin/menu.twig create mode 100644 templates/admin/menus.twig create mode 100644 templates/admin/message_queue.twig create mode 100644 templates/admin/nav.twig create mode 100644 templates/admin/signup.twig create mode 100644 templates/admin/user.twig create mode 100644 templates/admin/usergroup.twig create mode 100644 templates/admin/usergroups.twig create mode 100644 templates/admin/users.twig create mode 100644 templates/index.html create mode 100644 templates/site/exception.twig create mode 100644 templates/site/footer.twig create mode 100644 templates/site/header.twig create mode 100644 templates/site/index.html create mode 100644 templates/site/index.twig create mode 100644 templates/site/nav.twig create mode 100644 templates/site/page.twig create mode 100644 templates/system/build_incomplete.html create mode 100644 templates/system/incompatible.html create mode 100644 templates/system/index.html create mode 100644 templates/system/install_notice.html create mode 100644 tmp/index.html create mode 100644 web.config.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8debfd9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +# IDE & System Related Files +.idea + +# Local System File +config.php +php.ini + +# Vendor directory handling +/libraries/vendor \ No newline at end of file diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..88eeca7 --- /dev/null +++ b/.htaccess @@ -0,0 +1,52 @@ +########################################### +# ======= Enable the Rewrite Engine ======= + +RewriteEngine On + +########################################### + + +########################################### +# ======= No directory listings ======= + +IndexIgnore * +Options +FollowSymLinks +Options -Indexes + +########################################### + + +########################################### +# ======== Remove multiple slashes ======== + +RewriteCond %{HTTP_HOST} !="" +RewriteCond %{THE_REQUEST} ^[A-Z]+\s//+(.*)\sHTTP/[0-9.]+$ [OR] +RewriteCond %{THE_REQUEST} ^[A-Z]+\s(.*/)/+\sHTTP/[0-9.]+$ +RewriteRule .* http://%{HTTP_HOST}/%1 [R=301,L] + +########################################### + + +########################################### +# ======== Remove trailing slashes ======== + +RewriteCond %{REQUEST_FILENAME} !-d +RewriteRule ^(.*)/$ /$1 [R=301,L] + +########################################### + + +########################################### +# ======== SEF URL Routing ======== + +# If the request is not for a static asset +RewriteCond %{REQUEST_URI} !^/media/ + +# Or for a file that exists in the web directory +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d + +# Rewrite the request to run the application +RewriteRule (.*) index.php + +########################################### diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..df50810 --- /dev/null +++ b/LICENSE @@ -0,0 +1,340 @@ +GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8bdbe31 --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# Content Management System + +Content Management of Pages in the Kumwe CMS + +# Tutorial + +[![](https://git.vdm.dev/Kumwe/cms/raw/branch/master/media/images/tutorial_thumb.jpg "View Tutorial")](https://www.youtube.com/watch?v=43_V9OxUAdE) + +## To install this CMS + +1. Import the SQL tables into your database found in /sql/install.sql +2. Copy the /config.php.example file to /config.php +3. Update the /config.php to reflect your CMS details +4. Copy the /htaccess.txt file to /.htaccess +5. **Remove** the /installation folder from you root directory + +## To install all composer libraries + +0. Make sure you have [composer](https://getcomposer.org/doc/00-intro.md#installation-linux-unix-macos) installed on your system. +1. In your terminal go to the root folder of your Kumwe website where you will find the composer.json file. +2. Run the following command `composer install` to install all PHP packages. + +## To create an account + +1. Open [hostname:]/administrator +2. Click on link that says [Create Account] __FIRST account will get admin access, but there rest created will need admin approval__ +3. Fill in your details [done] + +## To login to admin/staff area again + +1. Open [hostname:]/administrator +2. Add you username and password +3. Click login [done] + +## To add Items + +> Items get linked to menus and are the text of your pages + +1. Login to [hostname:]/administrator +2. Click on items menu [hostname:]/administrator/index.php/items +3. Here you can update, delete and create items + +## To add menus + +> Menus link to items, and mange the menus of your site + +1. Login to [hostname:]/administrator +2. Click on menus menu [hostname:]/administrator/index.php/menus +3. Here you can update, delete and create menus (pages) that link to items + +## To set site home page + +> Home page is the first page you see when you open your public website + +1. Inside the menu edit/create view [hostname:]/administrator/index.php/menu +2. You can select one to be the home page + +# Just for fun... ((ewɘ))yn + +### License & Copyright +- Written by [Llewellyn van der Merwe](https://github.com/Llewellynvdm), March 2022 +- Copyright (C) 2022. All Rights Reserved +- License [GNU/GPL Version 2](http://www.gnu.org/licenses/gpl-2.0.html) diff --git a/administrator/favicon.ico b/administrator/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..63cd6a18d680c75f4b5819e79765e9bc55edb328 GIT binary patch literal 1150 zcmds1%}N4M7(G*FqfqLoGvoiD;twh!1A-!z>82Ku)n*JAwaCIvJwmN=?*j$(AVyCR zZ38=ZOi?%Po#EVjzwbNW&$$c(NPN9sa5Zr608{`uf-ZWder^C`@B35O_J%H_(FltW zGKa$<3WWmN?Y7kEbkOZ~ZS@I0$z*ano6RDh&+ig$x|C9>WVBi>SF_o)d4WIxnx^SK zpYOr%_nVW+1o3!05sSr+h^nx2xm-A%PM<}iQH#DoCX;c|2il#}}4~N4&@(VmmBEMWNOXRoN-xPBghT&n29uehP)OPYL=6d#c zJPp${U1DhRHP&)7W)i2hT210iVosseQK?jHZJ|&Ikw}Csbc?(1-_}@fK)sLL?^tJ# zu=%SC`$3jvut$S9=1gLqOV)}|SBW~~oC}s^t)|Xx-nr1VL+%A}OB^_zP85s9IcMF0 zazPD#=AN*%%zIgvvJ~(4`&h(Ma6=7#wwU4fi~ox;g}xE5z>5rgB!KZD@JZe`@k7k` GV*dlRWnf|e literal 0 HcmV?d00001 diff --git a/administrator/includes/app.php b/administrator/includes/app.php new file mode 100644 index 0000000..4aeff53 --- /dev/null +++ b/administrator/includes/app.php @@ -0,0 +1,100 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +defined('_LEXEC') or die; + +// Option to override defines from root folder +// source: https://github.com/joomla/joomla-cms/blob/4.1-dev/includes/app.php#L15 +if (file_exists(dirname(__DIR__) . '/defines.php')) +{ + include_once dirname(__DIR__) . '/defines.php'; +} + +// Load the default defines +// source: https://github.com/joomla/joomla-cms/blob/4.1-dev/includes/app.php#L20 +if (!defined('_LDEFINES')) +{ + define('LPATH_BASE', dirname(__DIR__)); + require_once LPATH_BASE . '/includes/defines.php'; +} + +// Check for presence of vendor dependencies not included in the git repository +// source: https://github.com/joomla/joomla-cms/blob/4.1-dev/includes/app.php#L26 +if (!file_exists(LPATH_LIBRARIES . '/vendor/autoload.php')) +{ + echo file_get_contents(LPATH_ROOT . '/templates/system/build_incomplete.html'); + + exit; +} + +// Load configuration (or install) +// source: https://github.com/joomla/joomla-cms/blob/4.1-dev/includes/app.php#L34 +require_once LPATH_BASE . '/includes/framework.php'; + +// Wrap in a try/catch so we can display an error if need be +try +{ + $container = (new Joomla\DI\Container) + ->registerServiceProvider(new Kumwe\CMS\Service\ConfigurationProvider(LPATH_CONFIGURATION . '/config.php')) + ->registerServiceProvider(new Kumwe\CMS\Service\SessionProvider) + ->registerServiceProvider(new Kumwe\CMS\Service\UserProvider) + ->registerServiceProvider(new Kumwe\CMS\Service\InputProvider) + ->registerServiceProvider(new Kumwe\CMS\Service\AdminApplicationProvider) + ->registerServiceProvider(new Kumwe\CMS\Service\AdminRouterProvider) + ->registerServiceProvider(new Kumwe\CMS\Service\AdminMVCProvider) + ->registerServiceProvider(new Joomla\Database\Service\DatabaseProvider) + ->registerServiceProvider(new Kumwe\CMS\Service\EventProvider) + ->registerServiceProvider(new Kumwe\CMS\Service\HttpProvider) + ->registerServiceProvider(new Kumwe\CMS\Service\LoggingProvider) + ->registerServiceProvider(new Joomla\Preload\Service\PreloadProvider) + ->registerServiceProvider(new Kumwe\CMS\Service\AdminTemplatingProvider); + + // Alias the web application to Kumwe's base application class as this is the primary application for the environment + $container->alias(Joomla\Application\AbstractApplication::class, Joomla\Application\AbstractWebApplication::class); + + // Alias the web logger to the PSR-3 interface as this is the primary logger for the environment + $container->alias(Monolog\Logger::class, 'monolog.logger.application.web') + ->alias(Psr\Log\LoggerInterface::class, 'monolog.logger.application.web'); +} +catch (\Throwable $e) +{ + error_log($e); + + header('HTTP/1.1 500 Internal Server Error', null, 500); + echo 'Container Initialization Error

Container Initialization Error

An error occurred while creating the DI container: ' . $e->getMessage() . '

'; + + exit(1); +} + +// Execute the application +// source: https://github.com/joomla/framework.joomla.org/blob/master/www/index.php#L85 +try +{ + $app = $container->get(Joomla\Application\AbstractApplication::class); + // Set the application as global app + \Kumwe\CMS\Factory::$application = $app; + // Execute the application. + $app->execute(); +} +catch (\Throwable $e) +{ + error_log($e); + + if (!headers_sent()) + { + header('HTTP/1.1 500 Internal Server Error', null, 500); + header('Content-Type: text/html; charset=utf-8'); + } + + echo 'Application Error

Application Error

An error occurred while executing the application: ' . $e->getMessage() . '

'; + + exit(1); +} +// I am just playing around... ((ewɘ))yn purring \ No newline at end of file diff --git a/administrator/includes/defines.php b/administrator/includes/defines.php new file mode 100644 index 0000000..8b323e2 --- /dev/null +++ b/administrator/includes/defines.php @@ -0,0 +1,24 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +defined('_LEXEC') or die; + +// Global definitions +$parts = explode(DIRECTORY_SEPARATOR, LPATH_BASE); +array_pop($parts); + +// Defines. +define('LPATH_ROOT', implode(DIRECTORY_SEPARATOR, $parts)); +define('LPATH_SITE', LPATH_ROOT); +define('LPATH_CONFIGURATION', LPATH_ROOT); +define('LPATH_ADMINISTRATOR', LPATH_ROOT . DIRECTORY_SEPARATOR . 'administrator'); +define('LPATH_LIBRARIES', LPATH_ROOT . DIRECTORY_SEPARATOR . 'libraries'); +define('LPATH_INSTALLATION', LPATH_ROOT . DIRECTORY_SEPARATOR . 'installation'); +define('LPATH_TEMPLATES', LPATH_ROOT . DIRECTORY_SEPARATOR . 'templates/admin'); diff --git a/administrator/includes/framework.php b/administrator/includes/framework.php new file mode 100644 index 0000000..92a054d --- /dev/null +++ b/administrator/includes/framework.php @@ -0,0 +1,35 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +defined('_LEXEC') or die; + +// System includes +// source: https://github.com/joomla/joomla-cms/blob/4.1-dev/includes/framework.php#L14 +require_once LPATH_LIBRARIES . '/bootstrap.php'; + +// Installation check, and check on removal of the installation directory. +// source: https://github.com/joomla/joomla-cms/blob/4.1-dev/includes/framework.php#L17 +if (!file_exists(LPATH_CONFIGURATION . '/config.php') + || (filesize(LPATH_CONFIGURATION . '/config.php') < 10) + || (file_exists(LPATH_INSTALLATION . '/index.php'))) +{ + if (file_exists(LPATH_INSTALLATION . '/index.php')) + { + header('Location: ../installation/index.php'); + + exit; + } + else + { + echo 'No configuration file found and no installation code available. Exiting...'; + + exit; + } +} diff --git a/administrator/includes/index.html b/administrator/includes/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/administrator/includes/index.html @@ -0,0 +1 @@ + diff --git a/administrator/index.php b/administrator/index.php new file mode 100644 index 0000000..ab344e2 --- /dev/null +++ b/administrator/index.php @@ -0,0 +1,35 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +// NOTE: This file should remain compatible with PHP 5.2 to allow us to run our PHP minimum check and show a friendly error message +// source: https://github.com/joomla/joomla-cms/blob/4.1-dev/index.php#L9 + +// Define the application's minimum supported PHP version as a constant, so it can be referenced within the application. +define('KUMWE_MINIMUM_PHP', '7.2.5'); + +if (version_compare(PHP_VERSION, KUMWE_MINIMUM_PHP, '<')) +{ + die( + str_replace( + '{{phpversion}}', + KUMWE_MINIMUM_PHP, + file_get_contents(dirname(__FILE__) . '/../templates/system/incompatible.html') + ) + ); +} + +/** + * Constant that is checked in included files to prevent direct access. + */ +define('_LEXEC', 1); + +// We must setup some house rules, since we can't have all +// this code just doing what it wants can we.... <>yn growling +require_once dirname(__FILE__) . '/includes/app.php'; diff --git a/administrator/media/css/index.html b/administrator/media/css/index.html new file mode 100644 index 0000000..fa6d84e --- /dev/null +++ b/administrator/media/css/index.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/administrator/media/css/template.css b/administrator/media/css/template.css new file mode 100644 index 0000000..e69de29 diff --git a/administrator/media/index.html b/administrator/media/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/administrator/media/index.html @@ -0,0 +1 @@ + diff --git a/administrator/media/js/index.html b/administrator/media/js/index.html new file mode 100644 index 0000000..fa6d84e --- /dev/null +++ b/administrator/media/js/index.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/administrator/media/js/template.js b/administrator/media/js/template.js new file mode 100644 index 0000000..e69de29 diff --git a/administrator/media/mix-manifest.json b/administrator/media/mix-manifest.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/administrator/media/mix-manifest.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/administrator/media/sri-manifest.json b/administrator/media/sri-manifest.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/administrator/media/sri-manifest.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..b87da7b --- /dev/null +++ b/composer.json @@ -0,0 +1,72 @@ +{ + "name": "kumwe/cms", + "type": "project", + "description": "Kumwe CMS", + "keywords": [ + "kumwe", + "cms" + ], + "homepage": "https://github.com/Kumwe/cms", + "license": "GPL-2.0", + "config": { + "optimize-autoloader": true, + "platform": { + "php": "7.2.5" + }, + "vendor-dir": "libraries/vendor", + "github-protocols": ["https"], + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } + }, + "autoload": { + "psr-4": { + "Kumwe\\CMS\\": "libraries/src/" + } + }, + "require": { + "php": "^7.2.5", + "ext-json": "*", + "joomla/application": "~2.0", + "joomla/archive": "~2.0", + "joomla/authentication": "~2.0", + "joomla/console": "~2.0", + "joomla/controller": "~2.0", + "joomla/crypt": "~2.0", + "joomla/data": "~2.0", + "joomla/database": "~2.0", + "joomla/di": "~2.0", + "joomla/event": "~2.0", + "joomla/filter": "~2.0", + "joomla/filesystem": "~2.0", + "joomla/http": "~2.0", + "joomla/input": "~2.0", + "joomla/model": "~2.0", + "joomla/preload": "~2.0", + "joomla/ldap": "~2.0", + "joomla/oauth1": "~2.0", + "joomla/oauth2": "~2.0", + "joomla/registry": "~2.0", + "joomla/renderer": "~2.0", + "joomla/router": "~2.0", + "joomla/session": "~2.0", + "joomla/string": "~2.0", + "joomla/uri": "~2.0", + "joomla/utilities": "~2.0", + "algo26-matthias/idna-convert": "~3.0", + "joomla/view": "~2.0", + "laminas/laminas-diactoros": "^2.3", + "monolog/monolog": "^2.1", + "psr/link": "^1.0", + "ramsey/uuid": "^4.0.1", + "robmorgan/phinx": "^0.12.3", + "defuse/php-encryption": "^2.0", + "symfony/asset": "^5.1.2", + "symfony/process": "^5.1.2", + "symfony/web-link": "^5.1.2", + "symfony/yaml": "^5.1.2", + "theiconic/php-ga-measurement-protocol": "^2.7.2", + "twig/twig": "^2.13", + "phpmailer/phpmailer": "~6.0" + } +} \ No newline at end of file diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..055576b --- /dev/null +++ b/composer.lock @@ -0,0 +1,5398 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "a873adca971fe4f2f619f54cdbfad67c", + "packages": [ + { + "name": "algo26-matthias/idna-convert", + "version": "v3.0.5", + "source": { + "type": "git", + "url": "https://github.com/algo26-matthias/idna-convert.git", + "reference": "9cbcfa17ecfed54387ca2ed29acb2773f1870a5e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/algo26-matthias/idna-convert/zipball/9cbcfa17ecfed54387ca2ed29acb2773f1870a5e", + "reference": "9cbcfa17ecfed54387ca2ed29acb2773f1870a5e", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "jakeasmith/http_build_url": "^1", + "php": ">=7.2.0" + }, + "require-dev": { + "phpunit/phpunit": "^8.0 || ^9.0" + }, + "suggest": { + "ext-iconv": "Install ext/iconv for using input / output other than UTF-8 or ISO-8859-1", + "ext-mbstring": "Install ext/mbstring for using input / output other than UTF-8 or ISO-8859-1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Algo26\\IdnaConvert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1+" + ], + "authors": [ + { + "name": "Matthias Sommerfeld", + "email": "matthias.sommerfeld@algo26.de", + "role": "Developer" + } + ], + "description": "A library for encoding and decoding internationalized domain names", + "homepage": "http://idnaconv.net/", + "keywords": [ + "idn", + "idna", + "php" + ], + "support": { + "issues": "https://github.com/algo26-matthias/idna-convert/issues", + "source": "https://github.com/algo26-matthias/idna-convert/tree/v3.0.5" + }, + "time": "2020-10-05T05:49:30+00:00" + }, + { + "name": "brick/math", + "version": "0.9.3", + "source": { + "type": "git", + "url": "https://github.com/brick/math.git", + "reference": "ca57d18f028f84f777b2168cd1911b0dee2343ae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/math/zipball/ca57d18f028f84f777b2168cd1911b0dee2343ae", + "reference": "ca57d18f028f84f777b2168cd1911b0dee2343ae", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^7.5.15 || ^8.5 || ^9.0", + "vimeo/psalm": "4.9.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\Math\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Arbitrary-precision arithmetic library", + "keywords": [ + "Arbitrary-precision", + "BigInteger", + "BigRational", + "arithmetic", + "bigdecimal", + "bignum", + "brick", + "math" + ], + "support": { + "issues": "https://github.com/brick/math/issues", + "source": "https://github.com/brick/math/tree/0.9.3" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/brick/math", + "type": "tidelift" + } + ], + "time": "2021-08-15T20:50:18+00:00" + }, + { + "name": "cakephp/core", + "version": "4.3.9", + "source": { + "type": "git", + "url": "https://github.com/cakephp/core.git", + "reference": "499f17738d40560ec077d7d2039c9af4969c6b17" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cakephp/core/zipball/499f17738d40560ec077d7d2039c9af4969c6b17", + "reference": "499f17738d40560ec077d7d2039c9af4969c6b17", + "shasum": "" + }, + "require": { + "cakephp/utility": "^4.0", + "php": ">=7.2.0" + }, + "suggest": { + "cakephp/cache": "To use Configure::store() and restore().", + "cakephp/event": "To use PluginApplicationInterface or plugin applications.", + "league/container": "To use Container and ServiceProvider classes" + }, + "type": "library", + "autoload": { + "files": [ + "functions.php" + ], + "psr-4": { + "Cake\\Core\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/core/graphs/contributors" + } + ], + "description": "CakePHP Framework Core classes", + "homepage": "https://cakephp.org", + "keywords": [ + "cakephp", + "core", + "framework" + ], + "support": { + "forum": "https://stackoverflow.com/tags/cakephp", + "irc": "irc://irc.freenode.org/cakephp", + "issues": "https://github.com/cakephp/cakephp/issues", + "source": "https://github.com/cakephp/core" + }, + "time": "2022-03-10T13:13:51+00:00" + }, + { + "name": "cakephp/database", + "version": "4.3.9", + "source": { + "type": "git", + "url": "https://github.com/cakephp/database.git", + "reference": "f565f9d296817692031852d142ee28e0d48f4dd9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cakephp/database/zipball/f565f9d296817692031852d142ee28e0d48f4dd9", + "reference": "f565f9d296817692031852d142ee28e0d48f4dd9", + "shasum": "" + }, + "require": { + "cakephp/core": "^4.0", + "cakephp/datasource": "^4.0", + "php": ">=7.2.0" + }, + "suggest": { + "cakephp/i18n": "If you are using locale-aware datetime formats or Chronos types." + }, + "type": "library", + "autoload": { + "psr-4": { + "Cake\\Database\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/database/graphs/contributors" + } + ], + "description": "Flexible and powerful Database abstraction library with a familiar PDO-like API", + "homepage": "https://cakephp.org", + "keywords": [ + "abstraction", + "cakephp", + "database", + "database abstraction", + "pdo" + ], + "support": { + "forum": "https://stackoverflow.com/tags/cakephp", + "irc": "irc://irc.freenode.org/cakephp", + "issues": "https://github.com/cakephp/cakephp/issues", + "source": "https://github.com/cakephp/database" + }, + "time": "2022-03-29T14:10:04+00:00" + }, + { + "name": "cakephp/datasource", + "version": "4.3.9", + "source": { + "type": "git", + "url": "https://github.com/cakephp/datasource.git", + "reference": "106ac9cb54a852f286dd2fe21e131c51f4a3d7fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cakephp/datasource/zipball/106ac9cb54a852f286dd2fe21e131c51f4a3d7fb", + "reference": "106ac9cb54a852f286dd2fe21e131c51f4a3d7fb", + "shasum": "" + }, + "require": { + "cakephp/core": "^4.0", + "php": ">=7.2.0", + "psr/log": "^1.0 || ^2.0", + "psr/simple-cache": "^1.0 || ^2.0" + }, + "suggest": { + "cakephp/cache": "If you decide to use Query caching.", + "cakephp/collection": "If you decide to use ResultSetInterface.", + "cakephp/utility": "If you decide to use EntityTrait." + }, + "type": "library", + "autoload": { + "psr-4": { + "Cake\\Datasource\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/datasource/graphs/contributors" + } + ], + "description": "Provides connection managing and traits for Entities and Queries that can be reused for different datastores", + "homepage": "https://cakephp.org", + "keywords": [ + "cakephp", + "connection management", + "datasource", + "entity", + "query" + ], + "support": { + "forum": "https://stackoverflow.com/tags/cakephp", + "irc": "irc://irc.freenode.org/cakephp", + "issues": "https://github.com/cakephp/cakephp/issues", + "source": "https://github.com/cakephp/datasource" + }, + "time": "2022-05-10T07:45:43+00:00" + }, + { + "name": "cakephp/utility", + "version": "4.3.9", + "source": { + "type": "git", + "url": "https://github.com/cakephp/utility.git", + "reference": "3d352060ca3e49c81c3fd2bdb092ee345d8f4e38" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cakephp/utility/zipball/3d352060ca3e49c81c3fd2bdb092ee345d8f4e38", + "reference": "3d352060ca3e49c81c3fd2bdb092ee345d8f4e38", + "shasum": "" + }, + "require": { + "cakephp/core": "^4.0", + "php": ">=7.2.0" + }, + "suggest": { + "ext-intl": "To use Text::transliterate() or Text::slug()", + "lib-ICU": "To use Text::transliterate() or Text::slug()" + }, + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Cake\\Utility\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/utility/graphs/contributors" + } + ], + "description": "CakePHP Utility classes such as Inflector, String, Hash, and Security", + "homepage": "https://cakephp.org", + "keywords": [ + "cakephp", + "hash", + "inflector", + "security", + "string", + "utility" + ], + "support": { + "forum": "https://stackoverflow.com/tags/cakephp", + "irc": "irc://irc.freenode.org/cakephp", + "issues": "https://github.com/cakephp/cakephp/issues", + "source": "https://github.com/cakephp/utility" + }, + "time": "2022-01-28T18:02:00+00:00" + }, + { + "name": "composer/ca-bundle", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/composer/ca-bundle.git", + "reference": "4c679186f2aca4ab6a0f1b0b9cf9252decb44d0b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/4c679186f2aca4ab6a0f1b0b9cf9252decb44d0b", + "reference": "4c679186f2aca4ab6a0f1b0b9cf9252decb44d0b", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "ext-pcre": "*", + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.55", + "psr/log": "^1.0", + "symfony/phpunit-bridge": "^4.2 || ^5", + "symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\CaBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.", + "keywords": [ + "cabundle", + "cacert", + "certificate", + "ssl", + "tls" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/ca-bundle/issues", + "source": "https://github.com/composer/ca-bundle/tree/1.3.1" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2021-10-28T20:44:15+00:00" + }, + { + "name": "defuse/php-encryption", + "version": "v2.3.1", + "source": { + "type": "git", + "url": "https://github.com/defuse/php-encryption.git", + "reference": "77880488b9954b7884c25555c2a0ea9e7053f9d2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/defuse/php-encryption/zipball/77880488b9954b7884c25555c2a0ea9e7053f9d2", + "reference": "77880488b9954b7884c25555c2a0ea9e7053f9d2", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "paragonie/random_compat": ">= 2", + "php": ">=5.6.0" + }, + "require-dev": { + "phpunit/phpunit": "^4|^5|^6|^7|^8|^9" + }, + "bin": [ + "bin/generate-defuse-key" + ], + "type": "library", + "autoload": { + "psr-4": { + "Defuse\\Crypto\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Hornby", + "email": "taylor@defuse.ca", + "homepage": "https://defuse.ca/" + }, + { + "name": "Scott Arciszewski", + "email": "info@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "Secure PHP Encryption Library", + "keywords": [ + "aes", + "authenticated encryption", + "cipher", + "crypto", + "cryptography", + "encrypt", + "encryption", + "openssl", + "security", + "symmetric key cryptography" + ], + "support": { + "issues": "https://github.com/defuse/php-encryption/issues", + "source": "https://github.com/defuse/php-encryption/tree/v2.3.1" + }, + "time": "2021-04-09T23:57:26+00:00" + }, + { + "name": "fig/link-util", + "version": "1.1.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/link-util.git", + "reference": "5d7b8d04ed3393b4b59968ca1e906fb7186d81e8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/link-util/zipball/5d7b8d04ed3393b4b59968ca1e906fb7186d81e8", + "reference": "5d7b8d04ed3393b4b59968ca1e906fb7186d81e8", + "shasum": "" + }, + "require": { + "php": ">=5.5.0", + "psr/link": "~1.0@dev" + }, + "provide": { + "psr/link-implementation": "1.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.1", + "squizlabs/php_codesniffer": "^2.3.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Fig\\Link\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common utility implementations for HTTP links", + "keywords": [ + "http", + "http-link", + "link", + "psr", + "psr-13", + "rest" + ], + "support": { + "issues": "https://github.com/php-fig/link-util/issues", + "source": "https://github.com/php-fig/link-util/tree/1.1.2" + }, + "time": "2021-02-03T23:36:04+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.4.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "ac1ec1cd9b5624694c3a40be801d94137afb12b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/ac1ec1cd9b5624694c3a40be801d94137afb12b4", + "reference": "ac1ec1cd9b5624694c3a40be801d94137afb12b4", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.5", + "guzzlehttp/psr7": "^1.8.3 || ^2.1", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "ext-curl": "*", + "php-http/client-integration-tests": "^3.0", + "phpunit/phpunit": "^8.5.5 || ^9.3.5", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.4-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.4.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2022-03-20T14:16:28+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "1.5.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "fe752aedc9fd8fcca3fe7ad05d419d32998a06da" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/fe752aedc9fd8fcca3fe7ad05d419d32998a06da", + "reference": "fe752aedc9fd8fcca3fe7ad05d419d32998a06da", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "symfony/phpunit-bridge": "^4.4 || ^5.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.5-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/1.5.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2021-10-22T20:56:57+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.2.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "c94a94f120803a18554c1805ef2e539f8285f9a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/c94a94f120803a18554c1805ef2e539f8285f9a2", + "reference": "c94a94f120803a18554c1805ef2e539f8285f9a2", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "http-interop/http-factory-tests": "^0.9", + "phpunit/phpunit": "^8.5.8 || ^9.3.10" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.2.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2022-03-20T21:55:58+00:00" + }, + { + "name": "jakeasmith/http_build_url", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/jakeasmith/http_build_url.git", + "reference": "93c273e77cb1edead0cf8bcf8cd2003428e74e37" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jakeasmith/http_build_url/zipball/93c273e77cb1edead0cf8bcf8cd2003428e74e37", + "reference": "93c273e77cb1edead0cf8bcf8cd2003428e74e37", + "shasum": "" + }, + "type": "library", + "autoload": { + "files": [ + "src/http_build_url.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jake A. Smith", + "email": "theman@jakeasmith.com" + } + ], + "description": "Provides functionality for http_build_url() to environments without pecl_http.", + "support": { + "issues": "https://github.com/jakeasmith/http_build_url/issues", + "source": "https://github.com/jakeasmith/http_build_url" + }, + "time": "2017-05-01T15:36:40+00:00" + }, + { + "name": "joomla/application", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/joomla-framework/application.git", + "reference": "e7b950d2d1358c0baac95a8633a60de20a1e82ab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/joomla-framework/application/zipball/e7b950d2d1358c0baac95a8633a60de20a1e82ab", + "reference": "e7b950d2d1358c0baac95a8633a60de20a1e82ab", + "shasum": "" + }, + "require": { + "joomla/event": "^2.0", + "joomla/registry": "^1.4.5|^2.0", + "laminas/laminas-diactoros": "^2.2.2", + "php": "^7.2.5", + "psr/http-message": "^1.0", + "psr/log": "^1.0", + "symfony/deprecation-contracts": "^2.1" + }, + "conflict": { + "joomla/di": "<1.5", + "joomla/input": "<1.2", + "joomla/router": "<2.0", + "joomla/session": "<2.0", + "joomla/uri": "<1.1" + }, + "require-dev": { + "joomla/coding-standards": "^3.0@dev", + "joomla/controller": "^1.0|^2.0", + "joomla/di": "^1.5|^2.0", + "joomla/input": "^1.2|^2.0", + "joomla/router": "^2.0", + "joomla/session": "^2.0", + "joomla/test": "^2.0", + "joomla/uri": "^1.1|^2.0", + "phpunit/phpunit": "^8.5|^9.0", + "symfony/phpunit-bridge": "^3.4.26|^4.1.12|^4.2.7|^5.0" + }, + "suggest": { + "joomla/controller": "^1.0|^2.0 To support resolving ControllerInterface objects in ControllerResolverInterface, install joomla/controller", + "joomla/input": "^1.2|^2.0 To use WebApplicationInterface, install joomla/input", + "joomla/router": "^2.0 To use WebApplication or ControllerResolverInterface implementations, install joomla/router", + "joomla/session": "^2.0 To use SessionAwareWebApplicationInterface, install joomla/session", + "joomla/uri": "^1.1|^2.0 To use AbstractWebApplication, install joomla/uri", + "psr/container": "^1.0 To use the ContainerControllerResolver, install any PSR-11 compatible container" + }, + "type": "joomla-package", + "extra": { + "branch-alias": { + "dev-2.0-dev": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Joomla\\Application\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "Joomla Application Package", + "homepage": "https://github.com/joomla-framework/application", + "keywords": [ + "application", + "framework", + "joomla" + ], + "support": { + "issues": "https://github.com/joomla-framework/application/issues", + "source": "https://github.com/joomla-framework/application/tree/2.0.1" + }, + "funding": [ + { + "url": "https://community.joomla.org/sponsorship-campaigns.html", + "type": "custom" + }, + { + "url": "https://github.com/joomla", + "type": "github" + } + ], + "time": "2021-12-10T12:11:13+00:00" + }, + { + "name": "joomla/archive", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/joomla-framework/archive.git", + "reference": "cedda2cf21c388c590b8a110df25db6197765b8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/joomla-framework/archive/zipball/cedda2cf21c388c590b8a110df25db6197765b8c", + "reference": "cedda2cf21c388c590b8a110df25db6197765b8c", + "shasum": "" + }, + "require": { + "joomla/filesystem": "^2.0", + "php": "^7.2.5|^8.0" + }, + "require-dev": { + "joomla/coding-standards": "^3.0@dev", + "joomla/test": "^2.0", + "phpunit/phpunit": "^8.5|^9.0" + }, + "suggest": { + "ext-bz2": "To extract bzip2 compressed packages", + "ext-zip": "To extract zip compressed packages", + "ext-zlib": "To extract gzip or zip compressed packages" + }, + "type": "joomla-package", + "extra": { + "branch-alias": { + "dev-2.0-dev": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Joomla\\Archive\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "Joomla Archive Package", + "homepage": "https://github.com/joomla-framework/archive", + "keywords": [ + "archive", + "framework", + "joomla" + ], + "support": { + "issues": "https://github.com/joomla-framework/archive/issues", + "source": "https://github.com/joomla-framework/archive/tree/2.0.1" + }, + "funding": [ + { + "url": "https://community.joomla.org/sponsorship-campaigns.html", + "type": "custom" + }, + { + "url": "https://github.com/joomla", + "type": "github" + } + ], + "time": "2022-03-29T13:03:06+00:00" + }, + { + "name": "joomla/authentication", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/joomla-framework/authentication.git", + "reference": "73d77db3b5d31300ffc0f147936cb420d4dffd96" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/joomla-framework/authentication/zipball/73d77db3b5d31300ffc0f147936cb420d4dffd96", + "reference": "73d77db3b5d31300ffc0f147936cb420d4dffd96", + "shasum": "" + }, + "require": { + "php": "^7.2.5|^8.0" + }, + "conflict": { + "joomla/database": "<2.0" + }, + "require-dev": { + "joomla/coding-standards": "^3.0@dev", + "joomla/database": "^2.0", + "joomla/input": "^1.0|^2.0", + "phpunit/phpunit": "^8.5|^9.0", + "symfony/phpunit-bridge": "^3.4|^4.4|^5.0" + }, + "suggest": { + "joomla/database": "Required if you want to use Joomla\\Authentication\\Strategies\\DatabaseStrategy", + "joomla/input": "Required if you want to use classes in the Joomla\\Authentication\\Strategies namespace" + }, + "type": "joomla-package", + "extra": { + "branch-alias": { + "dev-2.0-dev": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Joomla\\Authentication\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "Joomla Authentication Package", + "homepage": "https://github.com/joomla-framework/authentication", + "keywords": [ + "Authentication", + "framework", + "joomla" + ], + "support": { + "issues": "https://github.com/joomla-framework/authentication/issues", + "source": "https://github.com/joomla-framework/authentication/tree/2.0.0" + }, + "funding": [ + { + "url": "https://community.joomla.org/sponsorship-campaigns.html", + "type": "custom" + }, + { + "url": "https://github.com/joomla", + "type": "github" + } + ], + "time": "2021-08-10T18:44:21+00:00" + }, + { + "name": "joomla/console", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/joomla-framework/console.git", + "reference": "9db90c5b99e84a48cbaaf14c4c0d881b4d92480d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/joomla-framework/console/zipball/9db90c5b99e84a48cbaaf14c4c0d881b4d92480d", + "reference": "9db90c5b99e84a48cbaaf14c4c0d881b4d92480d", + "shasum": "" + }, + "require": { + "joomla/application": "^2.0", + "joomla/event": "^2.0", + "joomla/string": "^2.0", + "php": "^7.2.5", + "symfony/console": "^3.4|^4.4|^5.0" + }, + "require-dev": { + "joomla/coding-standards": "^2.0@alpha", + "joomla/test": "^2.0", + "phpunit/phpunit": "^8.5|^9.0", + "psr/container": "^1.0" + }, + "suggest": { + "psr/container-implementation": "To use the ContainerLoader" + }, + "type": "joomla-package", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Joomla\\Console\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "Joomla Console Package", + "homepage": "https://github.com/joomla-framework/console", + "keywords": [ + "console", + "framework", + "joomla" + ], + "support": { + "issues": "https://github.com/joomla-framework/console/issues", + "source": "https://github.com/joomla-framework/console/tree/2.0.0" + }, + "funding": [ + { + "url": "https://community.joomla.org/sponsorship-campaigns.html", + "type": "custom" + }, + { + "url": "https://github.com/joomla", + "type": "github" + } + ], + "time": "2021-08-10T20:31:15+00:00" + }, + { + "name": "joomla/controller", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/joomla-framework/controller.git", + "reference": "85b26e4b4521bceb346783bd342cb8c7a3f8c1be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/joomla-framework/controller/zipball/85b26e4b4521bceb346783bd342cb8c7a3f8c1be", + "reference": "85b26e4b4521bceb346783bd342cb8c7a3f8c1be", + "shasum": "" + }, + "require": { + "php": "^7.2.5|^8.0" + }, + "require-dev": { + "joomla/application": "^1.0|^2.0", + "joomla/coding-standards": "^2.0@alpha", + "joomla/input": "^1.0|^2.0", + "phpunit/phpunit": "^8.5|^9.0" + }, + "suggest": { + "joomla/application": "The joomla/application package is required to use Joomla\\Controller\\AbstractController", + "joomla/input": "The joomla/input package is required to use Joomla\\Controller\\AbstractController" + }, + "type": "joomla-package", + "extra": { + "branch-alias": { + "dev-2.0-dev": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Joomla\\Controller\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "Joomla Controller Package", + "homepage": "https://github.com/joomla-framework/controller", + "keywords": [ + "controller", + "framework", + "joomla" + ], + "support": { + "issues": "https://github.com/joomla-framework/controller/issues", + "source": "https://github.com/joomla-framework/controller/tree/2.0.0" + }, + "funding": [ + { + "url": "https://community.joomla.org/sponsorship-campaigns.html", + "type": "custom" + }, + { + "url": "https://github.com/joomla", + "type": "github" + } + ], + "time": "2021-08-16T20:21:42+00:00" + }, + { + "name": "joomla/crypt", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/joomla-framework/crypt.git", + "reference": "db9e5c4f8b42df5dee0a3698404affe631fdaba4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/joomla-framework/crypt/zipball/db9e5c4f8b42df5dee0a3698404affe631fdaba4", + "reference": "db9e5c4f8b42df5dee0a3698404affe631fdaba4", + "shasum": "" + }, + "require": { + "php": "^7.2.5|^8.0" + }, + "conflict": { + "defuse/php-encryption": "<2.0" + }, + "require-dev": { + "defuse/php-encryption": "^2.0", + "joomla/coding-standards": "^2.0@alpha", + "paragonie/sodium_compat": "^1.0", + "phpunit/phpunit": "^8.5|^9.0", + "symfony/phpunit-bridge": "^4.4|^5.0", + "symfony/polyfill-util": "^1.0" + }, + "suggest": { + "defuse/php-encryption": "To use Crypto cipher", + "ext-openssl": "To use the OpenSSL cipher", + "ext-sodium": "To use the Sodium cipher", + "paragonie/sodium_compat": "To use Sodium cipher if neither ext/sodium or ext/libsodium are available" + }, + "type": "joomla-package", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Joomla\\Crypt\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "Joomla Crypt Package", + "homepage": "https://github.com/joomla-framework/crypt", + "keywords": [ + "crypt", + "framework", + "joomla" + ], + "support": { + "issues": "https://github.com/joomla-framework/crypt/issues", + "source": "https://github.com/joomla-framework/crypt/tree/2.0.0" + }, + "funding": [ + { + "url": "https://community.joomla.org/sponsorship-campaigns.html", + "type": "custom" + }, + { + "url": "https://github.com/joomla", + "type": "github" + } + ], + "time": "2021-08-10T18:46:07+00:00" + }, + { + "name": "joomla/data", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/joomla-framework/data.git", + "reference": "6327825f48ba517d8f35179ac8f7868522d3a23f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/joomla-framework/data/zipball/6327825f48ba517d8f35179ac8f7868522d3a23f", + "reference": "6327825f48ba517d8f35179ac8f7868522d3a23f", + "shasum": "" + }, + "require": { + "joomla/registry": "^1.4.5|^2.0", + "php": "^7.2.5" + }, + "require-dev": { + "joomla/coding-standards": "^3.0@dev", + "joomla/test": "^2.0", + "phpunit/phpunit": "^8.5|^9.0" + }, + "type": "joomla-package", + "extra": { + "branch-alias": { + "dev-2.0-dev": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Joomla\\Data\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "Joomla Data Package", + "homepage": "https://github.com/joomla-framework/data", + "keywords": [ + "data", + "framework", + "joomla" + ], + "support": { + "issues": "https://github.com/joomla-framework/data/issues", + "source": "https://github.com/joomla-framework/data/tree/2.0.0" + }, + "funding": [ + { + "url": "https://community.joomla.org/sponsorship-campaigns.html", + "type": "custom" + }, + { + "url": "https://github.com/joomla", + "type": "github" + } + ], + "time": "2021-08-10T18:47:10+00:00" + }, + { + "name": "joomla/database", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/joomla-framework/database.git", + "reference": "194415339358b3ded43d5f68446b4fa93e18c3d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/joomla-framework/database/zipball/194415339358b3ded43d5f68446b4fa93e18c3d3", + "reference": "194415339358b3ded43d5f68446b4fa93e18c3d3", + "shasum": "" + }, + "require": { + "joomla/event": "^2.0", + "php": "^7.2.5|^8.0", + "symfony/deprecation-contracts": "^2.1" + }, + "require-dev": { + "joomla/archive": "^1.0|^2.0", + "joomla/coding-standards": "^2.0@alpha", + "joomla/console": "^2.0", + "joomla/di": "^1.0|^2.0", + "joomla/filesystem": "^1.3|^2.0", + "joomla/registry": "^1.4.5|^2.0", + "joomla/test": "^2.0", + "phpunit/phpunit": "^8.5|^9.0", + "psr/log": "^1.1", + "symfony/phpunit-bridge": "^4.4|^5.0" + }, + "suggest": { + "ext-mysqli": "To connect to a MySQL database via MySQLi", + "ext-pdo": "To connect to a MySQL, PostgreSQL, or SQLite database via PDO", + "ext-sqlsrv": "To connect to a SQL Server database", + "joomla/archive": "To use the ExportCommand class, install joomla/archive", + "joomla/console": "To use the ExportCommand and ImportCommand classes, install joomla/console", + "joomla/di": "To use the Database ServiceProviderInterface objects, install joomla/di.", + "joomla/filesystem": "To use the ExportCommand and ImportCommand classes, install joomla/filesystem", + "joomla/registry": "To use the Database ServiceProviderInterface objects, install joomla/registry.", + "psr/log": "To use the LoggingMonitor, install psr/log." + }, + "type": "joomla-package", + "extra": { + "branch-alias": { + "dev-2.0-dev": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Joomla\\Database\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "Joomla Database Package", + "homepage": "https://github.com/joomla-framework/database", + "keywords": [ + "database", + "framework", + "joomla" + ], + "support": { + "issues": "https://github.com/joomla-framework/database/issues", + "source": "https://github.com/joomla-framework/database/tree/2.1.0" + }, + "funding": [ + { + "url": "https://community.joomla.org/sponsorship-campaigns.html", + "type": "custom" + }, + { + "url": "https://github.com/joomla", + "type": "github" + } + ], + "time": "2022-03-02T16:36:31+00:00" + }, + { + "name": "joomla/di", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/joomla-framework/di.git", + "reference": "22ef18207e8945c8247aa2372bddbe76383bd0bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/joomla-framework/di/zipball/22ef18207e8945c8247aa2372bddbe76383bd0bc", + "reference": "22ef18207e8945c8247aa2372bddbe76383bd0bc", + "shasum": "" + }, + "require": { + "php": "^7.2.5|~8", + "psr/container": "^1.0", + "symfony/deprecation-contracts": "^2.1" + }, + "provide": { + "psr/container-implementation": "~1.0" + }, + "require-dev": { + "joomla/coding-standards": "^3.0@dev", + "joomla/test": "^2.0", + "phpunit/phpunit": "^8.5|^9.0" + }, + "type": "joomla-package", + "extra": { + "branch-alias": { + "dev-2.0-dev": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Joomla\\DI\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "Joomla DI Package", + "homepage": "https://github.com/joomla-framework/di", + "keywords": [ + "container", + "dependency injection", + "di", + "framework", + "ioc", + "joomla" + ], + "support": { + "issues": "https://github.com/joomla-framework/di/issues", + "source": "https://github.com/joomla-framework/di/tree/2.0.0" + }, + "funding": [ + { + "url": "https://community.joomla.org/sponsorship-campaigns.html", + "type": "custom" + }, + { + "url": "https://github.com/joomla", + "type": "github" + } + ], + "time": "2021-04-06T22:33:15+00:00" + }, + { + "name": "joomla/event", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/joomla-framework/event.git", + "reference": "dc19eae9a6cbffb608d4719f4eeb986e785692bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/joomla-framework/event/zipball/dc19eae9a6cbffb608d4719f4eeb986e785692bd", + "reference": "dc19eae9a6cbffb608d4719f4eeb986e785692bd", + "shasum": "" + }, + "require": { + "php": "^7.2.5", + "symfony/deprecation-contracts": "^2.1" + }, + "require-dev": { + "joomla/coding-standards": "^3.0@dev", + "joomla/console": "^2.0", + "phpunit/phpunit": "^8.5|^9.0", + "psr/container": "^1.0" + }, + "suggest": { + "joomla/console": "If you want to use the DebugEventDispatcherCommand class, please install joomla/console", + "psr/container-implementation": "If you want to use the LazyServiceEventListener class, please install a PSR-11 container" + }, + "type": "joomla-package", + "extra": { + "branch-alias": { + "dev-2.0-dev": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Joomla\\Event\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "Joomla Event Package", + "homepage": "https://github.com/joomla-framework/event", + "keywords": [ + "event", + "framework", + "joomla" + ], + "support": { + "issues": "https://github.com/joomla-framework/event/issues", + "source": "https://github.com/joomla-framework/event/tree/2.0.1" + }, + "funding": [ + { + "url": "https://community.joomla.org/sponsorship-campaigns.html", + "type": "custom" + }, + { + "url": "https://github.com/joomla", + "type": "github" + } + ], + "time": "2021-12-10T11:50:27+00:00" + }, + { + "name": "joomla/filesystem", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/joomla-framework/filesystem.git", + "reference": "d991e618da69e557a84ea97e6a601afec28ae8cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/joomla-framework/filesystem/zipball/d991e618da69e557a84ea97e6a601afec28ae8cf", + "reference": "d991e618da69e557a84ea97e6a601afec28ae8cf", + "shasum": "" + }, + "require": { + "php": "^7.2.5|^8.0" + }, + "require-dev": { + "joomla/coding-standards": "^3.0@dev", + "joomla/test": "^2.0", + "mikey179/vfsstream": "^1.1", + "phpunit/phpunit": "^8.5|^9.0" + }, + "type": "joomla-package", + "extra": { + "branch-alias": { + "dev-2.0-dev": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Joomla\\Filesystem\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "Joomla Filesystem Package", + "homepage": "https://github.com/joomla/joomla-framework-filesystem", + "keywords": [ + "filesystem", + "framework", + "joomla" + ], + "support": { + "issues": "https://github.com/joomla-framework/filesystem/issues", + "source": "https://github.com/joomla-framework/filesystem/tree/2.0.1" + }, + "funding": [ + { + "url": "https://community.joomla.org/sponsorship-campaigns.html", + "type": "custom" + }, + { + "url": "https://github.com/joomla", + "type": "github" + } + ], + "time": "2022-03-29T12:43:57+00:00" + }, + { + "name": "joomla/filter", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/joomla-framework/filter.git", + "reference": "137ca3f8925c4529a113735404b873fad0a1305f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/joomla-framework/filter/zipball/137ca3f8925c4529a113735404b873fad0a1305f", + "reference": "137ca3f8925c4529a113735404b873fad0a1305f", + "shasum": "" + }, + "require": { + "joomla/string": "^1.3|^2.0", + "php": "^7.2.5" + }, + "conflict": { + "joomla/language": "<2.0" + }, + "require-dev": { + "joomla/coding-standards": "^3.0@dev", + "joomla/language": "^2.0", + "phpunit/phpunit": "^8.5|^9.0" + }, + "suggest": { + "joomla/language": "Required only if you want to use `OutputFilter::stringURLSafe`." + }, + "type": "joomla-package", + "extra": { + "branch-alias": { + "dev-2.0-dev": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Joomla\\Filter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "Joomla Filter Package", + "homepage": "https://github.com/joomla-framework/filter", + "keywords": [ + "filter", + "framework", + "joomla" + ], + "support": { + "issues": "https://github.com/joomla-framework/filter/issues", + "source": "https://github.com/joomla-framework/filter/tree/2.0.1" + }, + "funding": [ + { + "url": "https://community.joomla.org/sponsorship-campaigns.html", + "type": "custom" + }, + { + "url": "https://github.com/joomla", + "type": "github" + } + ], + "time": "2022-02-15T21:33:06+00:00" + }, + { + "name": "joomla/http", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/joomla-framework/http.git", + "reference": "95f46a39dec738f73839e61c035be4fa597e822a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/joomla-framework/http/zipball/95f46a39dec738f73839e61c035be4fa597e822a", + "reference": "95f46a39dec738f73839e61c035be4fa597e822a", + "shasum": "" + }, + "require": { + "composer/ca-bundle": "^1.0", + "joomla/uri": "^1.0|^2.0", + "laminas/laminas-diactoros": "^2.2.2", + "php": "^7.2.5", + "psr/http-client": "^1.0", + "psr/http-message": "^1.0" + }, + "require-dev": { + "joomla/coding-standards": "^2.0@alpha", + "joomla/test": "^2.0", + "phpunit/phpunit": "^8.5|^9.0" + }, + "suggest": { + "ext-curl": "To use cURL for HTTP connections" + }, + "type": "joomla-package", + "extra": { + "branch-alias": { + "dev-2.0-dev": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Joomla\\Http\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "Joomla HTTP Package", + "homepage": "https://github.com/joomla-framework/http", + "keywords": [ + "framework", + "http", + "joomla" + ], + "support": { + "issues": "https://github.com/joomla-framework/http/issues", + "source": "https://github.com/joomla-framework/http/tree/2.0.0" + }, + "funding": [ + { + "url": "https://community.joomla.org/sponsorship-campaigns.html", + "type": "custom" + }, + { + "url": "https://github.com/joomla", + "type": "github" + } + ], + "time": "2021-08-16T19:52:50+00:00" + }, + { + "name": "joomla/input", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/joomla-framework/input.git", + "reference": "147229d2e0c5ac7db6f972d19c9faa922575e5c2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/joomla-framework/input/zipball/147229d2e0c5ac7db6f972d19c9faa922575e5c2", + "reference": "147229d2e0c5ac7db6f972d19c9faa922575e5c2", + "shasum": "" + }, + "require": { + "joomla/filter": "^1.0|^2.0", + "php": "^7.2.5", + "symfony/deprecation-contracts": "^2.1" + }, + "require-dev": { + "joomla/coding-standards": "^3.0@dev", + "joomla/test": "^2.0", + "phpunit/phpunit": "^8.5|^9.0" + }, + "type": "joomla-package", + "extra": { + "branch-alias": { + "dev-2.0-dev": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Joomla\\Input\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "Joomla Input Package", + "homepage": "https://github.com/joomla-framework/input", + "keywords": [ + "framework", + "input", + "joomla" + ], + "support": { + "issues": "https://github.com/joomla-framework/input/issues", + "source": "https://github.com/joomla-framework/input/tree/2.0.3" + }, + "funding": [ + { + "url": "https://community.joomla.org/sponsorship-campaigns.html", + "type": "custom" + }, + { + "url": "https://github.com/joomla", + "type": "github" + } + ], + "time": "2022-04-06T20:02:40+00:00" + }, + { + "name": "joomla/ldap", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/joomla-framework/ldap.git", + "reference": "b02ec8a59297b517b0b843b07971aa2e7bbe91d2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/joomla-framework/ldap/zipball/b02ec8a59297b517b0b843b07971aa2e7bbe91d2", + "reference": "b02ec8a59297b517b0b843b07971aa2e7bbe91d2", + "shasum": "" + }, + "require": { + "ext-ldap": "*", + "php": "~7.0" + }, + "require-dev": { + "joomla/coding-standards": "~2.0@alpha", + "joomla/registry": "^1.4.5|~2.0", + "phpunit/phpunit": "~6.3" + }, + "type": "joomla-package", + "extra": { + "branch-alias": { + "dev-2.0-dev": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Joomla\\Ldap\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "Joomla LDAP Package", + "homepage": "https://github.com/joomla-framework/ldap", + "keywords": [ + "framework", + "joomla", + "ldap" + ], + "support": { + "issues": "https://github.com/joomla-framework/ldap/issues", + "source": "https://github.com/joomla-framework/ldap/tree/2.0.0" + }, + "funding": [ + { + "url": "https://community.joomla.org/sponsorship-campaigns.html", + "type": "custom" + }, + { + "url": "https://github.com/joomla", + "type": "github" + } + ], + "time": "2021-08-16T20:01:23+00:00" + }, + { + "name": "joomla/model", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/joomla-framework/model.git", + "reference": "e874ca6ea7f463d28f7620dc242973747dc8f0f0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/joomla-framework/model/zipball/e874ca6ea7f463d28f7620dc242973747dc8f0f0", + "reference": "e874ca6ea7f463d28f7620dc242973747dc8f0f0", + "shasum": "" + }, + "require": { + "php": "^7.2.5" + }, + "require-dev": { + "joomla/coding-standards": "^3.0@dev", + "joomla/database": "^1.0|^2.0", + "joomla/registry": "^1.4.5|^2.0", + "phpunit/phpunit": "^8.5|^9.0" + }, + "suggest": { + "joomla/database": "^1.0|^2.0 Allows using database models", + "joomla/registry": "^1.4.5|^2.0 Allows using stateful models" + }, + "type": "joomla-package", + "extra": { + "branch-alias": { + "dev-2.0-dev": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Joomla\\Model\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "Joomla Model Package", + "homepage": "https://github.com/joomla-framework/model", + "keywords": [ + "framework", + "joomla", + "model" + ], + "support": { + "issues": "https://github.com/joomla-framework/model/issues", + "source": "https://github.com/joomla-framework/model/tree/2.0.0" + }, + "funding": [ + { + "url": "https://community.joomla.org/sponsorship-campaigns.html", + "type": "custom" + }, + { + "url": "https://github.com/joomla", + "type": "github" + } + ], + "time": "2021-08-16T20:22:12+00:00" + }, + { + "name": "joomla/oauth1", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/joomla-framework/oauth1.git", + "reference": "89559f79ff0c3fef73f806fd66814ae8bb1cb655" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/joomla-framework/oauth1/zipball/89559f79ff0c3fef73f806fd66814ae8bb1cb655", + "reference": "89559f79ff0c3fef73f806fd66814ae8bb1cb655", + "shasum": "" + }, + "require": { + "joomla/application": "^2.0", + "joomla/http": "^1.2.2|^2.0", + "joomla/input": "^1.2|^2.0", + "joomla/registry": "^1.4.5|^2.0", + "joomla/session": "^2.0", + "joomla/uri": "^1.1|^2.0", + "php": "^7.2.5" + }, + "require-dev": { + "joomla/coding-standards": "^3.0@dev", + "joomla/event": "^2.0", + "joomla/test": "^2.0", + "phpunit/phpunit": "^8.5|^9.0" + }, + "type": "joomla-package", + "extra": { + "branch-alias": { + "dev-2.0-dev": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Joomla\\OAuth1\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "Joomla OAuth1 Package", + "homepage": "https://github.com/joomla-framework/oauth1", + "keywords": [ + "framework", + "joomla", + "oauth1" + ], + "support": { + "issues": "https://github.com/joomla-framework/oauth1/issues", + "source": "https://github.com/joomla-framework/oauth1/tree/2.0.0" + }, + "funding": [ + { + "url": "https://community.joomla.org/sponsorship-campaigns.html", + "type": "custom" + }, + { + "url": "https://github.com/joomla", + "type": "github" + } + ], + "time": "2021-08-16T19:58:37+00:00" + }, + { + "name": "joomla/oauth2", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/joomla-framework/oauth2.git", + "reference": "1e6fd0affea9f96376e580ec050145e874b399cb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/joomla-framework/oauth2/zipball/1e6fd0affea9f96376e580ec050145e874b399cb", + "reference": "1e6fd0affea9f96376e580ec050145e874b399cb", + "shasum": "" + }, + "require": { + "joomla/application": "^2.0", + "joomla/http": "^1.2.2|^2.0", + "joomla/input": "^1.2|^2.0", + "joomla/session": "^1.0|^2.0", + "joomla/uri": "^1.0|^2.0", + "php": "^7.2.5" + }, + "require-dev": { + "joomla/coding-standards": "^3.0@dev", + "phpunit/phpunit": "^8.5|^9.0" + }, + "type": "joomla-package", + "extra": { + "branch-alias": { + "dev-2.0-dev": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Joomla\\OAuth2\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "Joomla OAuth2 Package", + "homepage": "https://github.com/joomla-framework/oauth2", + "keywords": [ + "framework", + "joomla", + "oauth2" + ], + "support": { + "issues": "https://github.com/joomla-framework/oauth2/issues", + "source": "https://github.com/joomla-framework/oauth2/tree/2.0.0" + }, + "funding": [ + { + "url": "https://community.joomla.org/sponsorship-campaigns.html", + "type": "custom" + }, + { + "url": "https://github.com/joomla", + "type": "github" + } + ], + "time": "2021-08-16T19:59:30+00:00" + }, + { + "name": "joomla/preload", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/joomla-framework/preload.git", + "reference": "dcd6f3424e3d02e2b47761464e33826b2b6ae7e0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/joomla-framework/preload/zipball/dcd6f3424e3d02e2b47761464e33826b2b6ae7e0", + "reference": "dcd6f3424e3d02e2b47761464e33826b2b6ae7e0", + "shasum": "" + }, + "require": { + "fig/link-util": "^1.0", + "php": "^7.2.5", + "psr/link": "^1.0" + }, + "conflict": { + "joomla/application": "<2.0", + "joomla/event": "<2.0" + }, + "require-dev": { + "joomla/application": "^2.0", + "joomla/coding-standards": "^2.0@alpha", + "joomla/di": "^1.5|^2.0", + "joomla/event": "^2.0", + "joomla/uri": "^2.0", + "phpunit/phpunit": "^8.2", + "symfony/web-link": "^3.4|^4.4|^5.0" + }, + "suggest": { + "joomla/application": "To use the PreloadSubscriber event listener, install `^2.0`", + "joomla/di": "To use the PreloadProvider service provider, install `^1.5|^2.0`", + "joomla/event": "To use the PreloadSubscriber event listener, install `^2.0`", + "symfony/web-link": "To use the PreloadSubscriber event listener, install `^3.4|^4.4|^5.0`" + }, + "type": "joomla-package", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Joomla\\Preload\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "Joomla Preload Package", + "homepage": "https://github.com/joomla-framework/preload", + "keywords": [ + "console", + "framework", + "joomla" + ], + "support": { + "issues": "https://github.com/joomla-framework/preload/issues", + "source": "https://github.com/joomla-framework/preload/tree/2.0.0" + }, + "funding": [ + { + "url": "https://community.joomla.org/sponsorship-campaigns.html", + "type": "custom" + }, + { + "url": "https://github.com/joomla", + "type": "github" + } + ], + "time": "2021-08-16T20:23:39+00:00" + }, + { + "name": "joomla/registry", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/joomla-framework/registry.git", + "reference": "4fcfa060f1ec101ec8311770a1d1c166eba6e367" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/joomla-framework/registry/zipball/4fcfa060f1ec101ec8311770a1d1c166eba6e367", + "reference": "4fcfa060f1ec101ec8311770a1d1c166eba6e367", + "shasum": "" + }, + "require": { + "joomla/utilities": "^1.4.1|^2.0", + "php": "^7.2.5" + }, + "conflict": { + "symfony/yaml": "<3.4" + }, + "require-dev": { + "joomla/coding-standards": "^3.0@dev", + "phpunit/phpunit": "^8.5|^9.0", + "symfony/yaml": "^3.4|^4.4|^5.0" + }, + "suggest": { + "symfony/yaml": "Install symfony/yaml if you require YAML support." + }, + "type": "joomla-package", + "extra": { + "branch-alias": { + "dev-2.0-dev": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Joomla\\Registry\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "Joomla Registry Package", + "homepage": "https://github.com/joomla-framework/registry", + "keywords": [ + "framework", + "joomla", + "registry" + ], + "support": { + "issues": "https://github.com/joomla-framework/registry/issues", + "source": "https://github.com/joomla-framework/registry/tree/2.0.1" + }, + "funding": [ + { + "url": "https://community.joomla.org/sponsorship-campaigns.html", + "type": "custom" + }, + { + "url": "https://github.com/joomla", + "type": "github" + } + ], + "time": "2021-12-10T11:52:55+00:00" + }, + { + "name": "joomla/renderer", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/joomla-framework/renderer.git", + "reference": "482896d9b10a3a17bf87cbb531d2ebfb463a3df7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/joomla-framework/renderer/zipball/482896d9b10a3a17bf87cbb531d2ebfb463a3df7", + "reference": "482896d9b10a3a17bf87cbb531d2ebfb463a3df7", + "shasum": "" + }, + "require": { + "php": "^7.2.5" + }, + "conflict": { + "illuminate/view": "<6.0", + "league/plates": "<3.0", + "mustache/mustache": "<2.3", + "symfony/templating": "<3.4", + "twig/twig": "<1.34 || >=2.0,<2.4" + }, + "require-dev": { + "illuminate/events": "^6.0|^7.0", + "illuminate/filesystem": "^6.0|^7.0", + "illuminate/view": "^6.0|^7.0", + "joomla/coding-standards": "^2.0@alpha", + "joomla/test": "^2.0", + "league/plates": "^3.0", + "mustache/mustache": "^2.3", + "phpunit/phpunit": "^8.5|^9.0", + "symfony/templating": "^3.4|^4.4|^5.0", + "twig/twig": "^1.34|^2.4|^3.0" + }, + "suggest": { + "illuminate/view": "Install ^6.0|^7.0 if you are using Laravel's Blade template engine.", + "league/plates": "Install ^3.0 if you are using the Plates template engine.", + "mustache/mustache": "Install ^2.3 if you are using the Mustache template engine.", + "symfony/templating": "Install ^3.4|^4.4|^5.0 if you are using Symfony's PHP template component.", + "twig/twig": "Install ^1.34|^2.4|^3.0 if you are using the Twig template engine." + }, + "type": "joomla-package", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Joomla\\Renderer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "description": "Joomla Renderer Package", + "homepage": "https://github.com/joomla-framework/renderer", + "keywords": [ + "framework", + "joomla", + "renderer" + ], + "support": { + "issues": "https://github.com/joomla-framework/renderer/issues", + "source": "https://github.com/joomla-framework/renderer/tree/2.0.0" + }, + "funding": [ + { + "url": "https://community.joomla.org/sponsorship-campaigns.html", + "type": "custom" + }, + { + "url": "https://github.com/joomla", + "type": "github" + } + ], + "time": "2021-08-16T20:20:59+00:00" + }, + { + "name": "joomla/router", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/joomla-framework/router.git", + "reference": "8dfb320fde8ed2c914c6e52df1e7266e9b25379a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/joomla-framework/router/zipball/8dfb320fde8ed2c914c6e52df1e7266e9b25379a", + "reference": "8dfb320fde8ed2c914c6e52df1e7266e9b25379a", + "shasum": "" + }, + "require": { + "php": "^7.2.5|^8.0" + }, + "conflict": { + "jeremeamia/superclosure": "<2.4" + }, + "require-dev": { + "jeremeamia/superclosure": "^2.4", + "joomla/coding-standards": "^2.0@alpha", + "joomla/console": "^2.0", + "phpunit/phpunit": "^8.5|^9.0" + }, + "suggest": { + "jeremeamia/superclosure": "If you use Closure based controllers and want to be able to serialize the router, please install jeremeamia/superclosure", + "joomla/console": "If you want to use the DebugRouterCommand class, please install joomla/console" + }, + "type": "joomla-package", + "extra": { + "branch-alias": { + "dev-2.0-dev": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Joomla\\Router\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "Joomla Router Package", + "homepage": "https://github.com/joomla-framework/router", + "keywords": [ + "framework", + "joomla", + "router" + ], + "support": { + "issues": "https://github.com/joomla-framework/router/issues", + "source": "https://github.com/joomla-framework/router/tree/2.0.0" + }, + "funding": [ + { + "url": "https://community.joomla.org/sponsorship-campaigns.html", + "type": "custom" + }, + { + "url": "https://github.com/joomla", + "type": "github" + } + ], + "time": "2021-08-16T20:04:57+00:00" + }, + { + "name": "joomla/session", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/joomla-framework/session.git", + "reference": "a7bb708a988530ce90c95e33efbc56432cf56c07" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/joomla-framework/session/zipball/a7bb708a988530ce90c95e33efbc56432cf56c07", + "reference": "a7bb708a988530ce90c95e33efbc56432cf56c07", + "shasum": "" + }, + "require": { + "php": "^7.2.5", + "symfony/deprecation-contracts": "^2.1" + }, + "conflict": { + "joomla/database": "<2.0", + "joomla/event": "<2.0", + "joomla/input": "<2.0" + }, + "require-dev": { + "joomla/coding-standards": "^3.0@dev", + "joomla/console": "^2.0", + "joomla/database": "^2.0", + "joomla/event": "^2.0", + "joomla/input": "^2.0", + "joomla/test": "^2.0", + "joomla/utilities": "^2.0", + "phpunit/phpunit": "^8.5|^9.0" + }, + "suggest": { + "ext-apcu": "To use APCu cache as a session handler", + "ext-memcached": "To use a Memcached server as a session handler", + "ext-redis": "To use a Redis server as a session handler", + "ext-session": "To use the Joomla\\Session\\Storage\\NativeStorage storage class.", + "ext-wincache": "To use WinCache as a session handler", + "joomla/console": "Install joomla/console if you want to use the CreateSessionTableCommand class.", + "joomla/database": "Install joomla/database if you want to use a database connection managed with Joomla\\Database\\DatabaseDriver as a session handler.", + "joomla/event": "The joomla/event package is required to use Joomla\\Session\\Session.", + "joomla/input": "The joomla/input package is required to use Address and Forwarded session validators." + }, + "type": "joomla-package", + "extra": { + "branch-alias": { + "dev-2.0-dev": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Joomla\\Session\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "Joomla Session Package", + "homepage": "https://github.com/joomla-framework/session", + "keywords": [ + "framework", + "joomla", + "session" + ], + "support": { + "issues": "https://github.com/joomla-framework/session/issues", + "source": "https://github.com/joomla-framework/session/tree/2.0.1" + }, + "funding": [ + { + "url": "https://community.joomla.org/sponsorship-campaigns.html", + "type": "custom" + }, + { + "url": "https://github.com/joomla", + "type": "github" + } + ], + "time": "2021-12-11T19:55:26+00:00" + }, + { + "name": "joomla/string", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/joomla-framework/string.git", + "reference": "778682c04a1909323da6a453a5b3030d7a7a2fa9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/joomla-framework/string/zipball/778682c04a1909323da6a453a5b3030d7a7a2fa9", + "reference": "778682c04a1909323da6a453a5b3030d7a7a2fa9", + "shasum": "" + }, + "require": { + "php": "^7.2.5", + "symfony/deprecation-contracts": "^2.1" + }, + "conflict": { + "doctrine/inflector": "<1.2" + }, + "require-dev": { + "doctrine/inflector": "1.2", + "joomla/coding-standards": "^3.0@dev", + "joomla/test": "^2.0", + "phpunit/phpunit": "^8.5|^9.0" + }, + "suggest": { + "doctrine/inflector": "To use the string inflector", + "ext-mbstring": "For improved processing" + }, + "type": "joomla-package", + "extra": { + "branch-alias": { + "dev-2.0-dev": "2.0-dev" + } + }, + "autoload": { + "files": [ + "src/phputf8/utf8.php", + "src/phputf8/ord.php", + "src/phputf8/str_ireplace.php", + "src/phputf8/str_pad.php", + "src/phputf8/str_split.php", + "src/phputf8/strcasecmp.php", + "src/phputf8/strcspn.php", + "src/phputf8/stristr.php", + "src/phputf8/strrev.php", + "src/phputf8/strspn.php", + "src/phputf8/trim.php", + "src/phputf8/ucfirst.php", + "src/phputf8/ucwords.php", + "src/phputf8/utils/ascii.php", + "src/phputf8/utils/validation.php" + ], + "psr-4": { + "Joomla\\String\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "Joomla String Package", + "homepage": "https://github.com/joomla-framework/string", + "keywords": [ + "framework", + "joomla", + "string" + ], + "support": { + "issues": "https://github.com/joomla-framework/string/issues", + "source": "https://github.com/joomla-framework/string/tree/2.0.0" + }, + "funding": [ + { + "url": "https://community.joomla.org/sponsorship-campaigns.html", + "type": "custom" + }, + { + "url": "https://github.com/joomla", + "type": "github" + } + ], + "time": "2021-08-10T18:57:12+00:00" + }, + { + "name": "joomla/uri", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/joomla-framework/uri.git", + "reference": "755f1cf80e2463d9a162563e607154c64083184b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/joomla-framework/uri/zipball/755f1cf80e2463d9a162563e607154c64083184b", + "reference": "755f1cf80e2463d9a162563e607154c64083184b", + "shasum": "" + }, + "require": { + "php": "^7.2.5|^8.0" + }, + "require-dev": { + "joomla/coding-standards": "^2.0@alpha", + "phpunit/phpunit": "^8.5|^9.0" + }, + "type": "joomla-package", + "extra": { + "branch-alias": { + "dev-2.0-dev": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Joomla\\Uri\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "Joomla Uri Package", + "homepage": "https://github.com/joomla-framework/uri", + "keywords": [ + "framework", + "joomla", + "uri" + ], + "support": { + "issues": "https://github.com/joomla-framework/uri/issues", + "source": "https://github.com/joomla-framework/uri/tree/2.0.2" + }, + "funding": [ + { + "url": "https://community.joomla.org/sponsorship-campaigns.html", + "type": "custom" + }, + { + "url": "https://github.com/joomla", + "type": "github" + } + ], + "time": "2022-04-21T09:39:04+00:00" + }, + { + "name": "joomla/utilities", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/joomla-framework/utilities.git", + "reference": "f5d4fcf778abbbf1c099814297c87ef177e8b268" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/joomla-framework/utilities/zipball/f5d4fcf778abbbf1c099814297c87ef177e8b268", + "reference": "f5d4fcf778abbbf1c099814297c87ef177e8b268", + "shasum": "" + }, + "require": { + "joomla/string": "^1.3|^2.0", + "php": "^7.2.5|^8.0" + }, + "require-dev": { + "joomla/coding-standards": "^2.0@alpha", + "phpunit/phpunit": "^8.5|^9.0" + }, + "type": "joomla-package", + "extra": { + "branch-alias": { + "dev-2.0-dev": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Joomla\\Utilities\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "Joomla Utilities Package", + "homepage": "https://github.com/joomla-framework/utilities", + "keywords": [ + "framework", + "joomla", + "utilities" + ], + "support": { + "issues": "https://github.com/joomla-framework/utilities/issues", + "source": "https://github.com/joomla-framework/utilities/tree/2.0.0" + }, + "funding": [ + { + "url": "https://community.joomla.org/sponsorship-campaigns.html", + "type": "custom" + }, + { + "url": "https://github.com/joomla", + "type": "github" + } + ], + "time": "2021-08-16T20:13:00+00:00" + }, + { + "name": "joomla/view", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/joomla-framework/view.git", + "reference": "3b43c84eba02c037190de3c796c81b31883694ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/joomla-framework/view/zipball/3b43c84eba02c037190de3c796c81b31883694ef", + "reference": "3b43c84eba02c037190de3c796c81b31883694ef", + "shasum": "" + }, + "require": { + "php": "^7.2.5|^8.0" + }, + "require-dev": { + "joomla/coding-standards": "^2.0@alpha", + "joomla/renderer": "^2.0", + "phpunit/phpunit": "^8.5|^9.0" + }, + "suggest": { + "joomla/renderer": "Required to use Joomla\\View\\BaseHtmlView" + }, + "type": "joomla-package", + "extra": { + "branch-alias": { + "dev-2.0-dev": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Joomla\\View\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "Joomla View Package", + "homepage": "https://github.com/joomla-framework/view", + "keywords": [ + "framework", + "joomla", + "view" + ], + "support": { + "issues": "https://github.com/joomla-framework/view/issues", + "source": "https://github.com/joomla-framework/view/tree/2.0.0" + }, + "funding": [ + { + "url": "https://community.joomla.org/sponsorship-campaigns.html", + "type": "custom" + }, + { + "url": "https://github.com/joomla", + "type": "github" + } + ], + "time": "2021-08-16T20:22:32+00:00" + }, + { + "name": "laminas/laminas-diactoros", + "version": "2.4.1", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-diactoros.git", + "reference": "36ef09b73e884135d2059cc498c938e90821bb57" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-diactoros/zipball/36ef09b73e884135d2059cc498c938e90821bb57", + "reference": "36ef09b73e884135d2059cc498c938e90821bb57", + "shasum": "" + }, + "require": { + "laminas/laminas-zendframework-bridge": "^1.0", + "php": "^7.1", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0" + }, + "conflict": { + "phpspec/prophecy": "<1.9.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "replace": { + "zendframework/zend-diactoros": "^2.2.1" + }, + "require-dev": { + "ext-curl": "*", + "ext-dom": "*", + "ext-gd": "*", + "ext-libxml": "*", + "http-interop/http-factory-tests": "^0.5.0", + "laminas/laminas-coding-standard": "~1.0.0", + "php-http/psr7-integration-tests": "^1.0", + "phpunit/phpunit": "^7.5.18" + }, + "type": "library", + "extra": { + "laminas": { + "config-provider": "Laminas\\Diactoros\\ConfigProvider", + "module": "Laminas\\Diactoros" + } + }, + "autoload": { + "files": [ + "src/functions/create_uploaded_file.php", + "src/functions/marshal_headers_from_sapi.php", + "src/functions/marshal_method_from_sapi.php", + "src/functions/marshal_protocol_version_from_sapi.php", + "src/functions/marshal_uri_from_sapi.php", + "src/functions/normalize_server.php", + "src/functions/normalize_uploaded_files.php", + "src/functions/parse_cookie_header.php", + "src/functions/create_uploaded_file.legacy.php", + "src/functions/marshal_headers_from_sapi.legacy.php", + "src/functions/marshal_method_from_sapi.legacy.php", + "src/functions/marshal_protocol_version_from_sapi.legacy.php", + "src/functions/marshal_uri_from_sapi.legacy.php", + "src/functions/normalize_server.legacy.php", + "src/functions/normalize_uploaded_files.legacy.php", + "src/functions/parse_cookie_header.legacy.php" + ], + "psr-4": { + "Laminas\\Diactoros\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "PSR HTTP Message implementations", + "homepage": "https://laminas.dev", + "keywords": [ + "http", + "laminas", + "psr", + "psr-17", + "psr-7" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-diactoros/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-diactoros/issues", + "rss": "https://github.com/laminas/laminas-diactoros/releases.atom", + "source": "https://github.com/laminas/laminas-diactoros" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2020-09-03T14:29:41+00:00" + }, + { + "name": "laminas/laminas-zendframework-bridge", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-zendframework-bridge.git", + "reference": "6ede70583e101030bcace4dcddd648f760ddf642" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-zendframework-bridge/zipball/6ede70583e101030bcace4dcddd648f760ddf642", + "reference": "6ede70583e101030bcace4dcddd648f760ddf642", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.1 || ^9.3", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "extra": { + "laminas": { + "module": "Laminas\\ZendFrameworkBridge" + } + }, + "autoload": { + "files": [ + "src/autoload.php" + ], + "psr-4": { + "Laminas\\ZendFrameworkBridge\\": "src//" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Alias legacy ZF class names to Laminas Project equivalents.", + "keywords": [ + "ZendFramework", + "autoloading", + "laminas", + "zf" + ], + "support": { + "forum": "https://discourse.laminas.dev/", + "issues": "https://github.com/laminas/laminas-zendframework-bridge/issues", + "rss": "https://github.com/laminas/laminas-zendframework-bridge/releases.atom", + "source": "https://github.com/laminas/laminas-zendframework-bridge" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2020-09-14T14:23:00+00:00" + }, + { + "name": "monolog/monolog", + "version": "2.6.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "247918972acd74356b0a91dfaa5adcaec069b6c0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/247918972acd74356b0a91dfaa5adcaec069b6c0", + "reference": "247918972acd74356b0a91dfaa5adcaec069b6c0", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "1.0.0 || 2.0.0 || 3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^2.4.9 || ^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2", + "guzzlehttp/guzzle": "^7.4", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "php-console/php-console": "^3.1.3", + "phpspec/prophecy": "^1.15", + "phpstan/phpstan": "^0.12.91", + "phpunit/phpunit": "^8.5.14", + "predis/predis": "^1.1", + "rollbar/rollbar": "^1.3 || ^2 || ^3", + "ruflin/elastica": "^7", + "swiftmailer/swiftmailer": "^5.3|^6.0", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "php-console/php-console": "Allow sending log messages to Google Chrome", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/2.6.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2022-05-10T09:36:00+00:00" + }, + { + "name": "paragonie/random_compat", + "version": "v9.99.100", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", + "shasum": "" + }, + "require": { + "php": ">= 7" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "polyfill", + "pseudorandom", + "random" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/random_compat/issues", + "source": "https://github.com/paragonie/random_compat" + }, + "time": "2020-10-15T08:29:30+00:00" + }, + { + "name": "phpmailer/phpmailer", + "version": "v6.6.0", + "source": { + "type": "git", + "url": "https://github.com/PHPMailer/PHPMailer.git", + "reference": "e43bac82edc26ca04b36143a48bde1c051cfd5b1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/e43bac82edc26ca04b36143a48bde1c051cfd5b1", + "reference": "e43bac82edc26ca04b36143a48bde1c051cfd5b1", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-filter": "*", + "ext-hash": "*", + "php": ">=5.5.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", + "doctrine/annotations": "^1.2", + "php-parallel-lint/php-console-highlighter": "^0.5.0", + "php-parallel-lint/php-parallel-lint": "^1.3.1", + "phpcompatibility/php-compatibility": "^9.3.5", + "roave/security-advisories": "dev-latest", + "squizlabs/php_codesniffer": "^3.6.2", + "yoast/phpunit-polyfills": "^1.0.0" + }, + "suggest": { + "ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses", + "hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication", + "league/oauth2-google": "Needed for Google XOAUTH2 authentication", + "psr/log": "For optional PSR-3 debug logging", + "stevenmaguire/oauth2-microsoft": "Needed for Microsoft XOAUTH2 authentication", + "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPMailer\\PHPMailer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-only" + ], + "authors": [ + { + "name": "Marcus Bointon", + "email": "phpmailer@synchromedia.co.uk" + }, + { + "name": "Jim Jagielski", + "email": "jimjag@gmail.com" + }, + { + "name": "Andy Prevost", + "email": "codeworxtech@users.sourceforge.net" + }, + { + "name": "Brent R. Matzelle" + } + ], + "description": "PHPMailer is a full-featured email creation and transfer class for PHP", + "support": { + "issues": "https://github.com/PHPMailer/PHPMailer/issues", + "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.6.0" + }, + "funding": [ + { + "url": "https://github.com/Synchro", + "type": "github" + } + ], + "time": "2022-02-28T15:31:21+00:00" + }, + { + "name": "psr/container", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf", + "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/1.1.1" + }, + "time": "2021-03-05T17:36:06+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", + "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client/tree/master" + }, + "time": "2020-06-29T06:28:15+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "shasum": "" + }, + "require": { + "php": ">=7.0.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory/tree/master" + }, + "time": "2019-04-30T12:38:16+00:00" + }, + { + "name": "psr/http-message", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/master" + }, + "time": "2016-08-06T14:39:51+00:00" + }, + { + "name": "psr/link", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/link.git", + "reference": "eea8e8662d5cd3ae4517c9b864493f59fca95562" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/link/zipball/eea8e8662d5cd3ae4517c9b864493f59fca95562", + "reference": "eea8e8662d5cd3ae4517c9b864493f59fca95562", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Link\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for HTTP links", + "keywords": [ + "http", + "http-link", + "link", + "psr", + "psr-13", + "rest" + ], + "support": { + "source": "https://github.com/php-fig/link/tree/master" + }, + "time": "2016-10-28T16:06:13+00:00" + }, + { + "name": "psr/log", + "version": "1.1.4", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.4" + }, + "time": "2021-05-03T11:20:27+00:00" + }, + { + "name": "psr/simple-cache", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/master" + }, + "time": "2017-10-23T01:57:42+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "ramsey/collection", + "version": "1.1.4", + "source": { + "type": "git", + "url": "https://github.com/ramsey/collection.git", + "reference": "ab2237657ad99667a5143e32ba2683c8029563d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/collection/zipball/ab2237657ad99667a5143e32ba2683c8029563d4", + "reference": "ab2237657ad99667a5143e32ba2683c8029563d4", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8" + }, + "require-dev": { + "captainhook/captainhook": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", + "ergebnis/composer-normalize": "^2.6", + "fakerphp/faker": "^1.5", + "hamcrest/hamcrest-php": "^2", + "jangregor/phpstan-prophecy": "^0.8", + "mockery/mockery": "^1.3", + "phpstan/extension-installer": "^1", + "phpstan/phpstan": "^0.12.32", + "phpstan/phpstan-mockery": "^0.12.5", + "phpstan/phpstan-phpunit": "^0.12.11", + "phpunit/phpunit": "^8.5 || ^9", + "psy/psysh": "^0.10.4", + "slevomat/coding-standard": "^6.3", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Ramsey\\Collection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "A PHP 7.2+ library for representing and manipulating collections.", + "keywords": [ + "array", + "collection", + "hash", + "map", + "queue", + "set" + ], + "support": { + "issues": "https://github.com/ramsey/collection/issues", + "source": "https://github.com/ramsey/collection/tree/1.1.4" + }, + "funding": [ + { + "url": "https://github.com/ramsey", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/ramsey/collection", + "type": "tidelift" + } + ], + "time": "2021-07-30T00:58:27+00:00" + }, + { + "name": "ramsey/uuid", + "version": "4.2.3", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "fc9bb7fb5388691fd7373cd44dcb4d63bbcf24df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/fc9bb7fb5388691fd7373cd44dcb4d63bbcf24df", + "reference": "fc9bb7fb5388691fd7373cd44dcb4d63bbcf24df", + "shasum": "" + }, + "require": { + "brick/math": "^0.8 || ^0.9", + "ext-json": "*", + "php": "^7.2 || ^8.0", + "ramsey/collection": "^1.0", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-php80": "^1.14" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "captainhook/captainhook": "^5.10", + "captainhook/plugin-composer": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", + "doctrine/annotations": "^1.8", + "ergebnis/composer-normalize": "^2.15", + "mockery/mockery": "^1.3", + "moontoast/math": "^1.1", + "paragonie/random-lib": "^2", + "php-mock/php-mock": "^2.2", + "php-mock/php-mock-mockery": "^1.3", + "php-parallel-lint/php-parallel-lint": "^1.1", + "phpbench/phpbench": "^1.0", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^0.12", + "phpstan/phpstan-mockery": "^0.12", + "phpstan/phpstan-phpunit": "^0.12", + "phpunit/phpunit": "^8.5 || ^9", + "slevomat/coding-standard": "^7.0", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.9" + }, + "suggest": { + "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", + "ext-ctype": "Enables faster processing of character classification using ctype functions.", + "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", + "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.x-dev" + }, + "captainhook": { + "force-install": true + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Ramsey\\Uuid\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "support": { + "issues": "https://github.com/ramsey/uuid/issues", + "source": "https://github.com/ramsey/uuid/tree/4.2.3" + }, + "funding": [ + { + "url": "https://github.com/ramsey", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/ramsey/uuid", + "type": "tidelift" + } + ], + "time": "2021-09-25T23:10:38+00:00" + }, + { + "name": "robmorgan/phinx", + "version": "0.12.10", + "source": { + "type": "git", + "url": "https://github.com/cakephp/phinx.git", + "reference": "ad056cff354fc67fedf9bf96c441c2b428afad0c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cakephp/phinx/zipball/ad056cff354fc67fedf9bf96c441c2b428afad0c", + "reference": "ad056cff354fc67fedf9bf96c441c2b428afad0c", + "shasum": "" + }, + "require": { + "cakephp/database": "^4.0", + "php": ">=7.2", + "psr/container": "^1.0 || ^2.0", + "symfony/config": "^3.4|^4.0|^5.0|^6.0", + "symfony/console": "^3.4|^4.0|^5.0|^6.0" + }, + "require-dev": { + "cakephp/cakephp-codesniffer": "^4.0", + "ext-json": "*", + "ext-pdo": "*", + "phpunit/phpunit": "^8.5|^9.3", + "sebastian/comparator": ">=1.2.3", + "symfony/yaml": "^3.4|^4.0|^5.0" + }, + "suggest": { + "ext-json": "Install if using JSON configuration format", + "ext-pdo": "PDO extension is needed", + "symfony/yaml": "Install if using YAML configuration format" + }, + "bin": [ + "bin/phinx" + ], + "type": "library", + "autoload": { + "psr-4": { + "Phinx\\": "src/Phinx/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Rob Morgan", + "email": "robbym@gmail.com", + "homepage": "https://robmorgan.id.au", + "role": "Lead Developer" + }, + { + "name": "Woody Gilk", + "email": "woody.gilk@gmail.com", + "homepage": "https://shadowhand.me", + "role": "Developer" + }, + { + "name": "Richard Quadling", + "email": "rquadling@gmail.com", + "role": "Developer" + }, + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/phinx/graphs/contributors", + "role": "Developer" + } + ], + "description": "Phinx makes it ridiculously easy to manage the database migrations for your PHP app.", + "homepage": "https://phinx.org", + "keywords": [ + "database", + "database migrations", + "db", + "migrations", + "phinx" + ], + "support": { + "issues": "https://github.com/cakephp/phinx/issues", + "source": "https://github.com/cakephp/phinx/tree/0.12.10" + }, + "time": "2022-01-21T19:53:14+00:00" + }, + { + "name": "symfony/asset", + "version": "v5.4.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/asset.git", + "reference": "4affdca3da5f380caa27a338269b36ac288b3981" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/asset/zipball/4affdca3da5f380caa27a338269b36ac288b3981", + "reference": "4affdca3da5f380caa27a338269b36ac288b3981", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-php80": "^1.16" + }, + "conflict": { + "symfony/http-foundation": "<5.3" + }, + "require-dev": { + "symfony/http-client": "^4.4|^5.0|^6.0", + "symfony/http-foundation": "^5.3|^6.0", + "symfony/http-kernel": "^4.4|^5.0|^6.0" + }, + "suggest": { + "symfony/http-foundation": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Asset\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Manages URL generation and versioning of web assets such as CSS stylesheets, JavaScript files and image files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/asset/tree/v5.4.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-03-18T16:00:30+00:00" + }, + { + "name": "symfony/config", + "version": "v5.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "9f8964f56f7234f8ace16f66cb3fbae950c04e68" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/9f8964f56f7234f8ace16f66cb3fbae950c04e68", + "reference": "9f8964f56f7234f8ace16f66cb3fbae950c04e68", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/filesystem": "^4.4|^5.0|^6.0", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-php80": "^1.16", + "symfony/polyfill-php81": "^1.22" + }, + "conflict": { + "symfony/finder": "<4.4" + }, + "require-dev": { + "symfony/event-dispatcher": "^4.4|^5.0|^6.0", + "symfony/finder": "^4.4|^5.0|^6.0", + "symfony/messenger": "^4.4|^5.0|^6.0", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/yaml": "^4.4|^5.0|^6.0" + }, + "suggest": { + "symfony/yaml": "To use the yaml reference dumper" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Config\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/config/tree/v5.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-04-12T16:02:29+00:00" + }, + { + "name": "symfony/console", + "version": "v5.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "ffe3aed36c4d60da2cf1b0a1cee6b8f2e5fa881b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/ffe3aed36c4d60da2cf1b0a1cee6b8f2e5fa881b", + "reference": "ffe3aed36c4d60da2cf1b0a1cee6b8f2e5fa881b", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php73": "^1.9", + "symfony/polyfill-php80": "^1.16", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/string": "^5.1|^6.0" + }, + "conflict": { + "psr/log": ">=3", + "symfony/dependency-injection": "<4.4", + "symfony/dotenv": "<5.1", + "symfony/event-dispatcher": "<4.4", + "symfony/lock": "<4.4", + "symfony/process": "<4.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0" + }, + "require-dev": { + "psr/log": "^1|^2", + "symfony/config": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/event-dispatcher": "^4.4|^5.0|^6.0", + "symfony/lock": "^4.4|^5.0|^6.0", + "symfony/process": "^4.4|^5.0|^6.0", + "symfony/var-dumper": "^4.4|^5.0|^6.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/lock": "", + "symfony/process": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v5.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-04-12T16:02:29+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v2.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e8b495ea28c1d97b5e0c121748d6f9b53d075c66", + "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-01-02T09:53:40+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v5.4.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "3a4442138d80c9f7b600fb297534ac718b61d37f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/3a4442138d80c9f7b600fb297534ac718b61d37f", + "reference": "3a4442138d80c9f7b600fb297534ac718b61d37f", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8", + "symfony/polyfill-php80": "^1.16" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v5.4.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-04-01T12:33:59+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "30885182c981ab175d4d034db0f6f469898070ab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/30885182c981ab175d4d034db0f6f469898070ab", + "reference": "30885182c981ab175d4d034db0f6f469898070ab", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-10-20T20:35:02+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "81b86b50cf841a64252b439e738e97f4a34e2783" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/81b86b50cf841a64252b439e738e97f4a34e2783", + "reference": "81b86b50cf841a64252b439e738e97f4a34e2783", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-11-23T21:10:46+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8590a5f561694770bdcd3f9b5c69dde6945028e8", + "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-02-19T12:13:01+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/0abb51d2f102e00a4eefcf46ba7fec406d245825", + "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-11-30T18:21:41+00:00" + }, + { + "name": "symfony/polyfill-php72", + "version": "v1.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php72.git", + "reference": "9a142215a36a3888e30d0a9eeea9766764e96976" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/9a142215a36a3888e30d0a9eeea9766764e96976", + "reference": "9a142215a36a3888e30d0a9eeea9766764e96976", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php72\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php72/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-05-27T09:17:38+00:00" + }, + { + "name": "symfony/polyfill-php73", + "version": "v1.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "cc5db0e22b3cb4111010e48785a97f670b350ca5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/cc5db0e22b3cb4111010e48785a97f670b350ca5", + "reference": "cc5db0e22b3cb4111010e48785a97f670b350ca5", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php73\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php73/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-06-05T21:20:04+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "4407588e0d3f1f52efb65fbe92babe41f37fe50c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/4407588e0d3f1f52efb65fbe92babe41f37fe50c", + "reference": "4407588e0d3f1f52efb65fbe92babe41f37fe50c", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-03-04T08:16:47+00:00" + }, + { + "name": "symfony/polyfill-php81", + "version": "v1.25.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "5de4ba2d41b15f9bd0e19b2ab9674135813ec98f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/5de4ba2d41b15f9bd0e19b2ab9674135813ec98f", + "reference": "5de4ba2d41b15f9bd0e19b2ab9674135813ec98f", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php81\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php81/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-09-13T13:58:11+00:00" + }, + { + "name": "symfony/process", + "version": "v5.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "597f3fff8e3e91836bb0bd38f5718b56ddbde2f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/597f3fff8e3e91836bb0bd38f5718b56ddbde2f3", + "reference": "597f3fff8e3e91836bb0bd38f5718b56ddbde2f3", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-php80": "^1.16" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v5.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-04-08T05:07:18+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v2.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "24d9dc654b83e91aa59f9d167b131bc3b5bea24c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/24d9dc654b83e91aa59f9d167b131bc3b5bea24c", + "reference": "24d9dc654b83e91aa59f9d167b131bc3b5bea24c", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/container": "^1.1", + "symfony/deprecation-contracts": "^2.1|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "suggest": { + "symfony/service-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v2.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-03-13T20:07:29+00:00" + }, + { + "name": "symfony/string", + "version": "v5.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "3c061a76bff6d6ea427d85e12ad1bb8ed8cd43e8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/3c061a76bff6d6ea427d85e12ad1bb8ed8cd43e8", + "reference": "3c061a76bff6d6ea427d85e12ad1bb8ed8cd43e8", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php80": "~1.15" + }, + "conflict": { + "symfony/translation-contracts": ">=3.0" + }, + "require-dev": { + "symfony/error-handler": "^4.4|^5.0|^6.0", + "symfony/http-client": "^4.4|^5.0|^6.0", + "symfony/translation-contracts": "^1.1|^2", + "symfony/var-exporter": "^4.4|^5.0|^6.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v5.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-04-19T10:40:37+00:00" + }, + { + "name": "symfony/web-link", + "version": "v5.4.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/web-link.git", + "reference": "8b9b073390359549fec5f5d797f23bbe9e2997a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/web-link/zipball/8b9b073390359549fec5f5d797f23bbe9e2997a5", + "reference": "8b9b073390359549fec5f5d797f23bbe9e2997a5", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/link": "^1.0", + "symfony/polyfill-php80": "^1.16" + }, + "conflict": { + "symfony/http-kernel": "<5.3" + }, + "provide": { + "psr/link-implementation": "1.0" + }, + "require-dev": { + "symfony/http-kernel": "^5.3|^6.0" + }, + "suggest": { + "symfony/http-kernel": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\WebLink\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "dunglas@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Manages links between resources", + "homepage": "https://symfony.com", + "keywords": [ + "dns-prefetch", + "http", + "http2", + "link", + "performance", + "prefetch", + "preload", + "prerender", + "psr13", + "push" + ], + "support": { + "source": "https://github.com/symfony/web-link/tree/v5.4.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-01-02T09:53:40+00:00" + }, + { + "name": "symfony/yaml", + "version": "v5.4.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "e80f87d2c9495966768310fc531b487ce64237a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/e80f87d2c9495966768310fc531b487ce64237a2", + "reference": "e80f87d2c9495966768310fc531b487ce64237a2", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<5.3" + }, + "require-dev": { + "symfony/console": "^5.3|^6.0" + }, + "suggest": { + "symfony/console": "For validating YAML files using the lint command" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v5.4.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-01-26T16:32:32+00:00" + }, + { + "name": "theiconic/php-ga-measurement-protocol", + "version": "v2.9.0", + "source": { + "type": "git", + "url": "https://github.com/theiconic/php-ga-measurement-protocol.git", + "reference": "6136c2f2ef159045402ef985843db0ad0f136125" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theiconic/php-ga-measurement-protocol/zipball/6136c2f2ef159045402ef985843db0ad0f136125", + "reference": "6136c2f2ef159045402ef985843db0ad0f136125", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^6.0 || ^7.0", + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "4.7.*", + "satooshi/php-coveralls": "1.0.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "TheIconic\\Tracking\\GoogleAnalytics\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "THE ICONIC ENGINEERING TEAM", + "email": "engineering@theiconic.com.au" + } + ], + "description": "Send data to Google Analytics from the server using PHP. This library fully implements GA measurement protocol.", + "support": { + "issues": "https://github.com/theiconic/php-ga-measurement-protocol/issues", + "source": "https://github.com/theiconic/php-ga-measurement-protocol/tree/v2.9.0" + }, + "time": "2020-09-24T23:37:47+00:00" + }, + { + "name": "twig/twig", + "version": "v2.15.1", + "source": { + "type": "git", + "url": "https://github.com/twigphp/Twig.git", + "reference": "3b7cedb2f736899a7dbd0ba3d6da335a015f5cc4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/3b7cedb2f736899a7dbd0ba3d6da335a015f5cc4", + "reference": "3b7cedb2f736899a7dbd0ba3d6da335a015f5cc4", + "shasum": "" + }, + "require": { + "php": ">=7.1.3", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-mbstring": "^1.3", + "symfony/polyfill-php72": "^1.8" + }, + "require-dev": { + "psr/container": "^1.0", + "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.15-dev" + } + }, + "autoload": { + "psr-0": { + "Twig_": "lib/" + }, + "psr-4": { + "Twig\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + }, + { + "name": "Twig Team", + "role": "Contributors" + }, + { + "name": "Armin Ronacher", + "email": "armin.ronacher@active-4.com", + "role": "Project Founder" + } + ], + "description": "Twig, the flexible, fast, and secure template language for PHP", + "homepage": "https://twig.symfony.com", + "keywords": [ + "templating" + ], + "support": { + "issues": "https://github.com/twigphp/Twig/issues", + "source": "https://github.com/twigphp/Twig/tree/v2.15.1" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2022-05-17T05:46:24+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "^7.2.5", + "ext-json": "*" + }, + "platform-dev": [], + "platform-overrides": { + "php": "7.2.5" + }, + "plugin-api-version": "2.2.0" +} diff --git a/config.php.example b/config.php.example new file mode 100644 index 0000000..6d875f3 --- /dev/null +++ b/config.php.example @@ -0,0 +1,95 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + * ------------------------------------------------------------------------- + * THIS SHOULD ONLY BE USED AS A LAST RESORT WHEN THE WEB INSTALLER FAILS + * + * If you are installing Kumwe! manually ie not using the web browser installer + * then rename this file to config.php eg + * + * UNIX -> mv config.php.example config.php + * Windows -> rename config.php.example config.php + * + * Now edit this file and configure the parameters for your site and + * database. + * + * Finally move this file to the root folder of your Kumwe installation eg + * + * UNIX -> mv config.php ../ + * Windows -> copy config.php ../ + * + * SOURCE: https://github.com/joomla/joomla-cms/blob/4.1-dev/installation/configuration.php-dist + * + */ +class LConfig +{ + public $sitename = 'Kumwe!'; // Name of Kumwe site + + /* Database Settings */ + public $dbtype = 'mysqli'; // Normally mysqli + public $host = 'localhost'; // This is normally set to localhost + public $user = ''; // Database username + public $password = ''; // Database password + public $db = ''; // Database name + public $dbprefix = 'kumwe_'; // LEAVE THIS UNCHANGED FOR NOW + public $dbencryption = 0; + public $dbsslverifyservercert = false; + public $dbsslkey = ''; + public $dbsslcert = ''; + public $dbsslca = ''; + public $dbsslcipher = ''; + + /* Server Settings */ + public $secret = 'EiT9pmiqMycmQ6xx'; // Use something very secure. For example on linux the following command `cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w ${1:-16} | head -n 1` + public $helpurl = 'https://help.kumwe.net/proxy?keyref=Help{major}{minor}:{keyref}&lang={langcode}'; + public $tmp_path = '/tmp'; // This path needs to be writable by Kumwe! + + /* Locale Settings */ + public $offset = 'UTC'; + + /* Session settings */ + public $lifetime = 15; // Session time + public $session_handler = 'database'; + public $session_filesystem_path = ''; + public $session_memcached_server_host = 'localhost'; + public $session_memcached_server_port = 11211; + public $session_metadata = true; + public $session_redis_persist = 1; + public $session_redis_server_auth = ''; + public $session_redis_server_db = 0; + public $session_redis_server_host = 'localhost'; + public $session_redis_server_port = 6379; + + /* Mail Settings */ + public $mailonline = true; + public $mailer = 'mail'; + public $mailfrom = ''; + public $fromname = ''; + public $massmailoff = false; + public $replyto = ''; + public $replytoname = ''; + public $sendmail = '/usr/sbin/sendmail'; + public $smtpauth = false; + public $smtpuser = ''; + public $smtppass = ''; + public $smtphost = 'localhost'; + public $smtpsecure = 'none'; + public $smtpport = 25; + + /* Meta Settings */ + public $MetaDesc = 'Kumwe! - the dynamic portal engine and content management system'; + public $MetaAuthor = true; + public $MetaVersion = false; + public $MetaRights = ''; + public $robots = ''; + public $sitename_pagetitles = 0; + + /* Cookie Settings */ + public $cookie_domain = ''; + public $cookie_path = ''; +} diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..63cd6a18d680c75f4b5819e79765e9bc55edb328 GIT binary patch literal 1150 zcmds1%}N4M7(G*FqfqLoGvoiD;twh!1A-!z>82Ku)n*JAwaCIvJwmN=?*j$(AVyCR zZ38=ZOi?%Po#EVjzwbNW&$$c(NPN9sa5Zr608{`uf-ZWder^C`@B35O_J%H_(FltW zGKa$<3WWmN?Y7kEbkOZ~ZS@I0$z*ano6RDh&+ig$x|C9>WVBi>SF_o)d4WIxnx^SK zpYOr%_nVW+1o3!05sSr+h^nx2xm-A%PM<}iQH#DoCX;c|2il#}}4~N4&@(VmmBEMWNOXRoN-xPBghT&n29uehP)OPYL=6d#c zJPp${U1DhRHP&)7W)i2hT210iVosseQK?jHZJ|&Ikw}Csbc?(1-_}@fK)sLL?^tJ# zu=%SC`$3jvut$S9=1gLqOV)}|SBW~~oC}s^t)|Xx-nr1VL+%A}OB^_zP85s9IcMF0 zazPD#=AN*%%zIgvvJ~(4`&h(Ma6=7#wwU4fi~ox;g}xE5z>5rgB!KZD@JZe`@k7k` GV*dlRWnf|e literal 0 HcmV?d00001 diff --git a/htaccess.txt b/htaccess.txt new file mode 100644 index 0000000..88eeca7 --- /dev/null +++ b/htaccess.txt @@ -0,0 +1,52 @@ +########################################### +# ======= Enable the Rewrite Engine ======= + +RewriteEngine On + +########################################### + + +########################################### +# ======= No directory listings ======= + +IndexIgnore * +Options +FollowSymLinks +Options -Indexes + +########################################### + + +########################################### +# ======== Remove multiple slashes ======== + +RewriteCond %{HTTP_HOST} !="" +RewriteCond %{THE_REQUEST} ^[A-Z]+\s//+(.*)\sHTTP/[0-9.]+$ [OR] +RewriteCond %{THE_REQUEST} ^[A-Z]+\s(.*/)/+\sHTTP/[0-9.]+$ +RewriteRule .* http://%{HTTP_HOST}/%1 [R=301,L] + +########################################### + + +########################################### +# ======== Remove trailing slashes ======== + +RewriteCond %{REQUEST_FILENAME} !-d +RewriteRule ^(.*)/$ /$1 [R=301,L] + +########################################### + + +########################################### +# ======== SEF URL Routing ======== + +# If the request is not for a static asset +RewriteCond %{REQUEST_URI} !^/media/ + +# Or for a file that exists in the web directory +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d + +# Rewrite the request to run the application +RewriteRule (.*) index.php + +########################################### diff --git a/includes/app.php b/includes/app.php new file mode 100644 index 0000000..b8ba7f9 --- /dev/null +++ b/includes/app.php @@ -0,0 +1,96 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +defined('_LEXEC') or die; + +// Option to override defines from root folder +// source: https://github.com/joomla/joomla-cms/blob/4.1-dev/includes/app.php#L15 +if (file_exists(dirname(__DIR__) . '/defines.php')) +{ + include_once dirname(__DIR__) . '/defines.php'; +} + +// Load the default defines +// source: https://github.com/joomla/joomla-cms/blob/4.1-dev/includes/app.php#L20 +if (!defined('_LDEFINES')) +{ + define('LPATH_BASE', dirname(__DIR__)); + require_once LPATH_BASE . '/includes/defines.php'; +} + +// Check for presence of vendor dependencies not included in the git repository +// source: https://github.com/joomla/joomla-cms/blob/4.1-dev/includes/app.php#L26 +if (!file_exists(LPATH_LIBRARIES . '/vendor/autoload.php')) +{ + echo file_get_contents(LPATH_ROOT . '/templates/system/build_incomplete.html'); + + exit; +} + +// Load configuration (or install) +// source: https://github.com/joomla/joomla-cms/blob/4.1-dev/includes/app.php#L34 +require_once LPATH_BASE . '/includes/framework.php'; + +// Wrap in a try/catch so we can display an error if need be +try +{ + $container = (new Joomla\DI\Container) + ->registerServiceProvider(new Kumwe\CMS\Service\SiteApplicationProvider) + ->registerServiceProvider(new Kumwe\CMS\Service\ConfigurationProvider(LPATH_CONFIGURATION . '/config.php')) + ->registerServiceProvider(new Kumwe\CMS\Service\InputProvider) + ->registerServiceProvider(new Joomla\Database\Service\DatabaseProvider) + ->registerServiceProvider(new Kumwe\CMS\Service\EventProvider) + ->registerServiceProvider(new Kumwe\CMS\Service\HttpProvider) + ->registerServiceProvider(new Kumwe\CMS\Service\LoggingProvider) + ->registerServiceProvider(new Joomla\Preload\Service\PreloadProvider) + ->registerServiceProvider(new Kumwe\CMS\Service\SiteTemplatingProvider); + + // Alias the web application to Kumwe's base application class as this is the primary application for the environment + $container->alias(Joomla\Application\AbstractApplication::class, Joomla\Application\AbstractWebApplication::class); + + // Alias the web logger to the PSR-3 interface as this is the primary logger for the environment + $container->alias(Monolog\Logger::class, 'monolog.logger.application.web') + ->alias(Psr\Log\LoggerInterface::class, 'monolog.logger.application.web'); +} +catch (\Throwable $e) +{ + error_log($e); + + header('HTTP/1.1 500 Internal Server Error', null, 500); + echo 'Container Initialization Error

Container Initialization Error

An error occurred while creating the DI container: ' . $e->getMessage() . '

'; + + exit(1); +} + +// Execute the application +// source: https://github.com/joomla/framework.joomla.org/blob/master/www/index.php#L85 +try +{ + $app = $container->get(Joomla\Application\AbstractApplication::class); + // Set the application as global app + \Kumwe\CMS\Factory::$application = $app; + // Execute the application. + $app->execute(); +} +catch (\Throwable $e) +{ + error_log($e); + + if (!headers_sent()) + { + header('HTTP/1.1 500 Internal Server Error', null, 500); + header('Content-Type: text/html; charset=utf-8'); + } + + echo 'Application Error

Application Error

An error occurred while executing the application: ' . $e->getMessage() . '

'; + + exit(1); +} +// I am just playing around... ((ewɘ))yn purring \ No newline at end of file diff --git a/includes/defines.php b/includes/defines.php new file mode 100644 index 0000000..07e05a6 --- /dev/null +++ b/includes/defines.php @@ -0,0 +1,23 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +defined('_LEXEC') or die; + +// Global definitions +$parts = explode(DIRECTORY_SEPARATOR, LPATH_BASE); + +// Defines. +define('LPATH_ROOT', implode(DIRECTORY_SEPARATOR, $parts)); +define('LPATH_SITE', LPATH_ROOT); +define('LPATH_CONFIGURATION', LPATH_ROOT); +define('LPATH_ADMINISTRATOR', LPATH_ROOT . DIRECTORY_SEPARATOR . 'administrator'); +define('LPATH_LIBRARIES', LPATH_ROOT . DIRECTORY_SEPARATOR . 'libraries'); +define('LPATH_INSTALLATION', LPATH_ROOT . DIRECTORY_SEPARATOR . 'installation'); +define('LPATH_TEMPLATES', LPATH_ROOT . DIRECTORY_SEPARATOR . 'templates/site'); diff --git a/includes/framework.php b/includes/framework.php new file mode 100644 index 0000000..ac945c1 --- /dev/null +++ b/includes/framework.php @@ -0,0 +1,35 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +defined('_LEXEC') or die; + +// System includes +// source: https://github.com/joomla/joomla-cms/blob/4.1-dev/includes/framework.php#L14 +require_once LPATH_LIBRARIES . '/bootstrap.php'; + +// Installation check, and check on removal of the installation directory. +// source: https://github.com/joomla/joomla-cms/blob/4.1-dev/includes/framework.php#L17 +if (!file_exists(LPATH_CONFIGURATION . '/config.php') + || (filesize(LPATH_CONFIGURATION . '/config.php') < 10) + || (file_exists(LPATH_INSTALLATION . '/index.php'))) +{ + if (file_exists(LPATH_INSTALLATION . '/index.php')) + { + header('Location: ' . substr($_SERVER['REQUEST_URI'], 0, strpos($_SERVER['REQUEST_URI'], 'index.php')) . 'installation/index.php'); + + exit; + } + else + { + echo 'No configuration file found and no installation code available. Exiting...'; + + exit; + } +} diff --git a/includes/index.html b/includes/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/includes/index.html @@ -0,0 +1 @@ + diff --git a/index.php b/index.php new file mode 100644 index 0000000..4412df9 --- /dev/null +++ b/index.php @@ -0,0 +1,35 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +// NOTE: This file should remain compatible with PHP 5.2 to allow us to run our PHP minimum check and show a friendly error message +// source: https://github.com/joomla/joomla-cms/blob/4.1-dev/index.php#L9 + +// Define the application's minimum supported PHP version as a constant, so it can be referenced within the application. +define('KUMWE_MINIMUM_PHP', '7.2.5'); + +if (version_compare(PHP_VERSION, KUMWE_MINIMUM_PHP, '<')) +{ + die( + str_replace( + '{{phpversion}}', + KUMWE_MINIMUM_PHP, + file_get_contents(dirname(__FILE__) . '/templates/system/incompatible.html') + ) + ); +} + +/** + * Constant that is checked in included files to prevent direct access. + */ +define('_LEXEC', 1); + +// We must setup some house rules, since we can't have all +// this code just doing what it wants can we?... <>yn growling +require_once dirname(__FILE__) . '/includes/app.php'; diff --git a/installation/includes/app.php b/installation/includes/app.php new file mode 100644 index 0000000..88bb3c2 --- /dev/null +++ b/installation/includes/app.php @@ -0,0 +1,31 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +defined('_LEXEC') or die; + +// Option to override defines from root folder +// source: https://github.com/joomla/joomla-cms/blob/4.1-dev/includes/app.php#L15 +if (file_exists(dirname(__DIR__) . '/defines.php')) +{ + include_once dirname(__DIR__) . '/defines.php'; +} + +// Load the default defines +// source: https://github.com/joomla/joomla-cms/blob/4.1-dev/includes/app.php#L20 +if (!defined('_LDEFINES')) +{ + define('LPATH_BASE', dirname(__DIR__)); + require_once LPATH_BASE . '/includes/defines.php'; +} + +// I have not yet had time to finish this part of the application (CMS) +echo file_get_contents(LPATH_ROOT . '/templates/system/install_notice.html'); + +exit; diff --git a/installation/includes/defines.php b/installation/includes/defines.php new file mode 100644 index 0000000..92cd774 --- /dev/null +++ b/installation/includes/defines.php @@ -0,0 +1,23 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +defined('_LEXEC') or die; + +// Global definitions +$parts = explode(DIRECTORY_SEPARATOR, LPATH_BASE); +array_pop($parts); + +// Defines. +define('LPATH_ROOT', implode(DIRECTORY_SEPARATOR, $parts)); +define('LPATH_SITE', LPATH_ROOT); +define('LPATH_CONFIGURATION', LPATH_ROOT); +define('LPATH_ADMINISTRATOR', LPATH_ROOT . DIRECTORY_SEPARATOR . 'administrator'); +define('LPATH_LIBRARIES', LPATH_ROOT . DIRECTORY_SEPARATOR . 'libraries'); +define('LPATH_INSTALLATION', LPATH_ROOT . DIRECTORY_SEPARATOR . 'installation'); diff --git a/installation/includes/framework.php b/installation/includes/framework.php new file mode 100644 index 0000000..5112ba4 --- /dev/null +++ b/installation/includes/framework.php @@ -0,0 +1,41 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +defined('_LEXEC') or die; + +// System includes +// source: https://github.com/joomla/joomla-cms/blob/4.1-dev/administrator/includes/framework.php#L14 +require_once LPATH_LIBRARIES . '/bootstrap.php'; + +// Installation check, and check on removal of the installation directory. +// source: https://github.com/joomla/joomla-cms/blob/4.1-dev/administrator/includes/framework.php#L17 +if (!file_exists(LPATH_CONFIGURATION . '/config.php') + || (filesize(LPATH_CONFIGURATION . '/config.php') < 10) + || (file_exists(LPATH_INSTALLATION . '/index.php'))) +{ + if (file_exists(LPATH_INSTALLATION . '/index.php')) + { + header('Location: ../installation/index.php'); + + exit; + } + else + { + echo 'No configuration file found and no installation code available. Exiting...'; + + exit; + } +} + +// Pre-Load configuration. Don't remove the Output Buffering due to BOM issues. +// source: https://github.com/joomla/joomla-cms/blob/4.1-dev/administrator/includes/framework.php#L36 +ob_start(); +require_once LPATH_CONFIGURATION . '/config.php'; +ob_end_clean(); diff --git a/installation/index.php b/installation/index.php new file mode 100644 index 0000000..86feb08 --- /dev/null +++ b/installation/index.php @@ -0,0 +1,35 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +// NOTE: This file should remain compatible with PHP 5.2 to allow us to run our PHP minimum check and show a friendly error message +// source: https://github.com/joomla/joomla-cms/blob/4.1-dev/index.php#L9 + +// Define the application's minimum supported PHP version as a constant, so it can be referenced within the application. +define('KUMWE_MINIMUM_PHP', '7.2.5'); + +if (version_compare(PHP_VERSION, KUMWE_MINIMUM_PHP, '<')) +{ + die( + str_replace( + '{{phpversion}}', + KUMWE_MINIMUM_PHP, + file_get_contents(dirname(__FILE__) . '/../templates/system/incompatible.html') + ) + ); +} + +/** + * Constant that is checked in included files to prevent direct access. + */ +define('_LEXEC', 1); + +// We must setup some house rules, since we can't have all +// this code just doing what it wants can we.... <>yn growling +require_once dirname(__FILE__) . '/includes/app.php'; \ No newline at end of file diff --git a/libraries/.htaccess b/libraries/.htaccess new file mode 100644 index 0000000..9afb1a1 --- /dev/null +++ b/libraries/.htaccess @@ -0,0 +1,9 @@ +# Apache 2.4+ + + Require all denied + + +# Apache 2.0-2.2 + + Deny from all + diff --git a/libraries/bootstrap.php b/libraries/bootstrap.php new file mode 100644 index 0000000..861790c --- /dev/null +++ b/libraries/bootstrap.php @@ -0,0 +1,48 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +defined('_LEXEC') or die; + +// Set the platform root path as a constant if necessary. +// source: https://github.com/joomla/joomla-cms/blob/4.1-dev/libraries/bootstrap.php#L12 +defined('LPATH_PLATFORM') or define('LPATH_PLATFORM', __DIR__); + +// Detect the native operating system type. +$os = strtoupper(substr(PHP_OS, 0, 3)); + +defined('IS_WIN') or define('IS_WIN', ($os === 'WIN')); +defined('IS_UNIX') or define('IS_UNIX', (($os !== 'MAC') && ($os !== 'WIN'))); + +// Import the library loader if necessary. +if (!class_exists('LLoader')) +{ + require_once LPATH_PLATFORM . '/loader.php'; + + // If JLoader still does not exist panic. + if (!class_exists('LLoader')) + { + throw new RuntimeException('Kumwe Platform not loaded.'); + } +} + +// Setup the autoloaders. +LLoader::setup(); + +// Create the Composer autoloader +/** @var \Composer\Autoload\ClassLoader $loader */ +$loader = require LPATH_LIBRARIES . '/vendor/autoload.php'; + +// We need to pull our decorated class loader into memory before unregistering Composer's loader +class_exists('\\Kumwe\\CMS\\Autoload\\ClassLoader'); + +$loader->unregister(); + +// Decorate Composer autoloader +spl_autoload_register([new \Kumwe\CMS\Autoload\ClassLoader($loader), 'loadClass'], true, true); diff --git a/libraries/loader.php b/libraries/loader.php new file mode 100644 index 0000000..2f5d7b1 --- /dev/null +++ b/libraries/loader.php @@ -0,0 +1,599 @@ + + * + * @copyright (C) 2005 Open Source Matters, Inc. + * @license GNU General Public License version 2; see LICENSE.txt + **/ + +defined('LPATH_PLATFORM') or die; + +/** + * Static class to handle loading of libraries. + * + * @since 1.0.0 + */ +abstract class LLoader +{ + /** + * Container for already imported library paths. + * + * @var array + * @since 1.7.0 + */ + protected static $classes = array(); + + /** + * Container for already imported library paths. + * + * @var array + * @since 1.7.0 + */ + protected static $imported = array(); + + /** + * Container for registered library class prefixes and path lookups. + * + * @var array + * @since 3.0.0 + */ + protected static $prefixes = array(); + + /** + * Holds proxy classes and the class names the proxy. + * + * @var array + * @since 3.2 + */ + protected static $classAliases = array(); + + /** + * Holds the inverse lookup for proxy classes and the class names the proxy. + * + * @var array + * @since 3.4 + */ + protected static $classAliasesInverse = array(); + + /** + * Container for namespace => path map. + * + * @var array + * @since 3.1.4 + */ + protected static $namespaces = array(); + + /** + * Holds a reference for all deprecated aliases (mainly for use by a logging platform). + * + * @var array + * @since 3.6.3 + */ + protected static $deprecatedAliases = array(); + + /** + * The root folders where extensions can be found. + * + * @var array + * @since 4.0.0 + */ + protected static $extensionRootFolders = array(); + + /** + * Method to get the list of registered classes and their respective file paths for the autoloader. + * + * @return array The array of class => path values for the autoloader. + * + * @since 1.7.0 + */ + public static function getClassList() + { + return self::$classes; + } + + /** + * Method to get the list of deprecated class aliases. + * + * @return array An associative array with deprecated class alias data. + * + * @since 3.6.3 + */ + public static function getDeprecatedAliases() + { + return self::$deprecatedAliases; + } + + /** + * Method to get the list of registered namespaces. + * + * @return array The array of namespace => path values for the autoloader. + * + * @since 3.1.4 + */ + public static function getNamespaces() + { + return self::$namespaces; + } + + /** + * Load the file for a class. + * + * @param string $class The class to be loaded. + * + * @return boolean True on success + * + * @since 1.7.0 + */ + public static function load($class) + { + // Sanitize class name. + $key = strtolower($class); + + // If the class already exists do nothing. + if (class_exists($class, false)) + { + return true; + } + + // If the class is registered include the file. + if (isset(self::$classes[$key])) + { + $found = (bool) include_once self::$classes[$key]; + + if ($found) + { + self::loadAliasFor($class); + } + + // If the class doesn't exists, we probably have a class alias available + if (!class_exists($class, false)) + { + // Search the alias class, first none namespaced and then namespaced + $original = array_search($class, self::$classAliases) ? : array_search('\\' . $class, self::$classAliases); + + // When we have an original and the class exists an alias should be created + if ($original && class_exists($original, false)) + { + class_alias($original, $class); + } + } + + return true; + } + + return false; + } + + /** + * Register a class prefix with lookup path. This will allow developers to register library + * packages with different class prefixes to the system autoloader. More than one lookup path + * may be registered for the same class prefix, but if this method is called with the reset flag + * set to true then any registered lookups for the given prefix will be overwritten with the current + * lookup path. When loaded, prefix paths are searched in a "last in, first out" order. + * + * @param string $prefix The class prefix to register. + * @param string $path Absolute file path to the library root where classes with the given prefix can be found. + * @param boolean $reset True to reset the prefix with only the given lookup path. + * @param boolean $prepend If true, push the path to the beginning of the prefix lookup paths array. + * + * @return void + * + * @throws RuntimeException + * + * @since 3.0.0 + */ + public static function registerPrefix($prefix, $path, $reset = false, $prepend = false) + { + // Verify the library path exists. + if (!is_dir($path)) + { + $path = (str_replace(LPATH_ROOT, '', $path) == $path) ? basename($path) : str_replace(LPATH_ROOT, '', $path); + + throw new RuntimeException('Library path ' . $path . ' cannot be found.', 500); + } + + // If the prefix is not yet registered or we have an explicit reset flag then set set the path. + if ($reset || !isset(self::$prefixes[$prefix])) + { + self::$prefixes[$prefix] = array($path); + } + // Otherwise we want to simply add the path to the prefix. + else + { + if ($prepend) + { + array_unshift(self::$prefixes[$prefix], $path); + } + else + { + self::$prefixes[$prefix][] = $path; + } + } + } + + /** + * Offers the ability for "just in time" usage of `class_alias()`. + * You cannot overwrite an existing alias. + * + * @param string $alias The alias name to register. + * @param string $original The original class to alias. + * @param string|boolean $version The version in which the alias will no longer be present. + * + * @return boolean True if registration was successful. False if the alias already exists. + * + * @since 3.2 + */ + public static function registerAlias($alias, $original, $version = false) + { + // PHP is case insensitive so support all kind of alias combination + $lowercasedAlias = strtolower($alias); + + if (!isset(self::$classAliases[$lowercasedAlias])) + { + self::$classAliases[$lowercasedAlias] = $original; + + $original = self::stripFirstBackslash($original); + + if (!isset(self::$classAliasesInverse[$original])) + { + self::$classAliasesInverse[$original] = array($lowercasedAlias); + } + else + { + self::$classAliasesInverse[$original][] = $lowercasedAlias; + } + + // If given a version, log this alias as deprecated + if ($version) + { + self::$deprecatedAliases[] = array('old' => $alias, 'new' => $original, 'version' => $version); + } + + return true; + } + + return false; + } + + /** + * Register a namespace to the autoloader. When loaded, namespace paths are searched in a "last in, first out" order. + * + * @param string $namespace A case sensitive Namespace to register. + * @param string $path A case sensitive absolute file path to the library root where classes of the given namespace can be found. + * @param boolean $reset True to reset the namespace with only the given lookup path. + * @param boolean $prepend If true, push the path to the beginning of the namespace lookup paths array. + * + * @return void + * + * @throws RuntimeException + * + * @since 3.1.4 + */ + public static function registerNamespace($namespace, $path, $reset = false, $prepend = false) + { + // Verify the library path exists. + if (!is_dir($path)) + { + $path = (str_replace(LPATH_ROOT, '', $path) == $path) ? basename($path) : str_replace(LPATH_ROOT, '', $path); + + throw new RuntimeException('Library path ' . $path . ' cannot be found.', 500); + } + + // Trim leading and trailing backslashes from namespace, allowing "\Parent\Child", "Parent\Child\" and "\Parent\Child\" to be treated the same way. + $namespace = trim($namespace, '\\'); + + // If the namespace is not yet registered or we have an explicit reset flag then set the path. + if ($reset || !isset(self::$namespaces[$namespace])) + { + self::$namespaces[$namespace] = array($path); + } + + // Otherwise we want to simply add the path to the namespace. + else + { + if ($prepend) + { + array_unshift(self::$namespaces[$namespace], $path); + } + else + { + self::$namespaces[$namespace][] = $path; + } + } + } + + /** + * Method to setup the autoloaders for the Kumwe Platform. + * Since the SPL autoloaders are called in a queue we will add our explicit + * class-registration based loader first, then fall back on the autoloader based on conventions. + * This will allow people to register a class in a specific location and override platform libraries + * as was previously possible. + * + * @param boolean $enablePsr True to enable autoloading based on PSR-0. + * @param boolean $enablePrefixes True to enable prefix based class loading (needed to auto load the Kumwe core). + * @param boolean $enableClasses True to enable class map based class loading (needed to auto load the Kumwe core). + * + * @return void + * + * @since 3.1.4 + */ + public static function setup($enablePsr = true, $enablePrefixes = true, $enableClasses = true) + { + if ($enableClasses) + { + // Register the class map based autoloader. + spl_autoload_register(array('LLoader', 'load')); + } + + if ($enablePrefixes) + { + // Register the prefix autoloader. + spl_autoload_register(array('LLoader', '_autoload')); + } + + if ($enablePsr) + { + // Register the PSR based autoloader. + spl_autoload_register(array('LLoader', 'loadByPsr')); + spl_autoload_register(array('LLoader', 'loadByAlias')); + } + } + + /** + * Method to autoload classes that are namespaced to the PSR-4 standard. + * + * @param string $class The fully qualified class name to autoload. + * + * @return boolean True on success, false otherwise. + * + * @since 3.7.0 + * @deprecated 5.0 Use LLoader::loadByPsr instead + */ + public static function loadByPsr4($class) + { + return self::loadByPsr($class); + } + + /** + * Method to autoload classes that are namespaced to the PSR-4 standard. + * + * @param string $class The fully qualified class name to autoload. + * + * @return boolean True on success, false otherwise. + * + * @since 4.0.0 + */ + public static function loadByPsr($class) + { + $class = self::stripFirstBackslash($class); + + // Find the location of the last NS separator. + $pos = strrpos($class, '\\'); + + // If one is found, we're dealing with a NS'd class. + if ($pos !== false) + { + $classPath = str_replace('\\', DIRECTORY_SEPARATOR, substr($class, 0, $pos)) . DIRECTORY_SEPARATOR; + $className = substr($class, $pos + 1); + } + // If not, no need to parse path. + else + { + $classPath = null; + $className = $class; + } + + $classPath .= $className . '.php'; + + // Loop through registered namespaces until we find a match. + foreach (self::$namespaces as $ns => $paths) + { + if (strpos($class, "{$ns}\\") === 0) + { + $nsPath = trim(str_replace('\\', DIRECTORY_SEPARATOR, $ns), DIRECTORY_SEPARATOR); + + // Loop through paths registered to this namespace until we find a match. + foreach ($paths as $path) + { + $classFilePath = realpath($path . DIRECTORY_SEPARATOR . substr_replace($classPath, '', 0, strlen($nsPath) + 1)); + + // We do not allow files outside the namespace root to be loaded + if (strpos($classFilePath, realpath($path)) !== 0) + { + continue; + } + + // We check for class_exists to handle case-sensitive file systems + if (is_file($classFilePath) && !class_exists($class, false)) + { + $found = (bool) include_once $classFilePath; + + if ($found) + { + self::loadAliasFor($class); + } + + return $found; + } + } + } + } + + return false; + } + + /** + * Method to autoload classes that have been aliased using the registerAlias method. + * + * @param string $class The fully qualified class name to autoload. + * + * @return boolean True on success, false otherwise. + * + * @since 3.2 + */ + public static function loadByAlias($class) + { + $class = strtolower(self::stripFirstBackslash($class)); + + if (isset(self::$classAliases[$class])) + { + // Force auto-load of the regular class + class_exists(self::$classAliases[$class], true); + + // Normally this shouldn't execute as the autoloader will execute applyAliasFor when the regular class is + // auto-loaded above. + if (!class_exists($class, false) && !interface_exists($class, false)) + { + class_alias(self::$classAliases[$class], $class); + } + } + } + + /** + * Applies a class alias for an already loaded class, if a class alias was created for it. + * + * @param string $class We'll look for and register aliases for this (real) class name + * + * @return void + * + * @since 3.4 + */ + public static function applyAliasFor($class) + { + $class = self::stripFirstBackslash($class); + + if (isset(self::$classAliasesInverse[$class])) + { + foreach (self::$classAliasesInverse[$class] as $alias) + { + class_alias($class, $alias); + } + } + } + + /** + * Autoload a class based on name. + * + * @param string $class The class to be loaded. + * + * @return boolean True if the class was loaded, false otherwise. + * + * @since 1.7.3 + */ + public static function _autoload($class) + { + foreach (self::$prefixes as $prefix => $lookup) + { + $chr = strlen($prefix) < strlen($class) ? $class[strlen($prefix)] : 0; + + if (strpos($class, $prefix) === 0 && ($chr === strtoupper($chr))) + { + return self::_load(substr($class, strlen($prefix)), $lookup); + } + } + + return false; + } + + /** + * Load a class based on name and lookup array. + * + * @param string $class The class to be loaded (without prefix). + * @param array $lookup The array of base paths to use for finding the class file. + * + * @return boolean True if the class was loaded, false otherwise. + * + * @since 3.0.0 + */ + private static function _load($class, $lookup) + { + // Split the class name into parts separated by camelCase. + $parts = preg_split('/(?<=[a-z0-9])(?=[A-Z])/x', $class); + $partsCount = count($parts); + + foreach ($lookup as $base) + { + // Generate the path based on the class name parts. + $path = realpath($base . '/' . implode('/', array_map('strtolower', $parts)) . '.php'); + + // Load the file if it exists and is in the lookup path. + if (strpos($path, realpath($base)) === 0 && is_file($path)) + { + $found = (bool) include_once $path; + + if ($found) + { + self::loadAliasFor($class); + } + + return $found; + } + + // Backwards compatibility patch + + // If there is only one part we want to duplicate that part for generating the path. + if ($partsCount === 1) + { + // Generate the path based on the class name parts. + $path = realpath($base . '/' . implode('/', array_map('strtolower', array($parts[0], $parts[0]))) . '.php'); + + // Load the file if it exists and is in the lookup path. + if (strpos($path, realpath($base)) === 0 && is_file($path)) + { + $found = (bool) include_once $path; + + if ($found) + { + self::loadAliasFor($class); + } + + return $found; + } + } + } + + return false; + } + + /** + * Loads the aliases for the given class. + * + * @param string $class The class. + * + * @return void + * + * @since 3.8.0 + */ + private static function loadAliasFor($class) + { + if (!array_key_exists($class, self::$classAliasesInverse)) + { + return; + } + + foreach (self::$classAliasesInverse[$class] as $alias) + { + // Force auto-load of the alias class + class_exists($alias, true); + } + } + + /** + * Strips the first backslash from the given class if present. + * + * @param string $class The class to strip the first prefix from. + * + * @return string The striped class name. + * + * @since 3.8.0 + */ + private static function stripFirstBackslash($class) + { + return $class && $class[0] === '\\' ? substr($class, 1) : $class; + } +} diff --git a/libraries/src/Application/AdminApplication.php b/libraries/src/Application/AdminApplication.php new file mode 100644 index 0000000..24081b9 --- /dev/null +++ b/libraries/src/Application/AdminApplication.php @@ -0,0 +1,90 @@ +controllerResolver = $controllerResolver; + $this->router = $router; + + // Call the constructor as late as possible (it runs `initialise`). + parent::__construct($input, $config, $client, $response); + } + + /** + * Method to run the application routines. + * + * @return void + */ + protected function doExecute(): void + { + $route = $this->router->parseRoute($this->get('uri.route'), $this->input->getMethod()); + + // Add variables to the input if not already set + foreach ($route->getRouteVariables() as $key => $value) + { + $this->input->def($key, $value); + } + + \call_user_func($this->controllerResolver->resolve($route)); + } +} diff --git a/libraries/src/Application/IdentityAwareInterface.php b/libraries/src/Application/IdentityAwareInterface.php new file mode 100644 index 0000000..77c7bd4 --- /dev/null +++ b/libraries/src/Application/IdentityAwareInterface.php @@ -0,0 +1,57 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Application; + +use Kumwe\CMS\User\User; +use Kumwe\CMS\User\UserFactoryInterface; + +interface IdentityAwareInterface +{ + /** + * Get the application identity. + * + * @return User + * + * @since 1.0.0 + */ + public function getIdentity(): User; + + /** + * Allows the application to load a custom or default identity. + * + * @param User $identity An optional identity object. If omitted, a null user object is created. + * + * @return $this + * + * @since 1.0.0 + */ + public function loadIdentity(User $identity = null): IdentityAwareInterface; + + /** + * Set the user factory to use. + * + * @param UserFactoryInterface $userFactory The user factory to use + * + * @return void + * + * @since 1.0.0 + */ + public function setUserFactory(UserFactoryInterface $userFactory); + + /** + * Get the user factory to use. + * + * @return UserFactoryInterface + * + * @since 1.0.0 + */ + public function getUserFactory(): UserFactoryInterface; +} diff --git a/libraries/src/Application/IdentityAwareTrait.php b/libraries/src/Application/IdentityAwareTrait.php new file mode 100644 index 0000000..edc8275 --- /dev/null +++ b/libraries/src/Application/IdentityAwareTrait.php @@ -0,0 +1,91 @@ + + * @license GNU General Public License version 2 or later; see LICENSE + */ + +namespace Kumwe\CMS\Application; + +use Kumwe\CMS\User\User; +use Kumwe\CMS\User\UserFactoryInterface; + +/** + * Trait for application classes which are identity (user) aware + * + * @since 1.0.0 + */ +trait IdentityAwareTrait +{ + /** + * The application identity object. + * + * @var User + * @since 1.0.0 + */ + protected $identity; + + /** + * UserFactoryInterface + * + * @var UserFactoryInterface + * @since 1.0.0 + */ + private $userFactory; + + /** + * Get the application identity. + * + * @return User + * + * @since 1.0.0 + */ + public function getIdentity(): User + { + return $this->identity; + } + + /** + * Allows the application to load a custom or default identity. + * + * @param User $identity An optional identity object. If omitted, a null user object is created. + * + * @return IdentityAwareInterface + * + * @since 1.0.0 + */ + public function loadIdentity(User $identity = null): IdentityAwareInterface + { + $this->identity = $identity ?: $this->userFactory->loadUserById(0); + + return $this; + } + + /** + * Set the user factory to use. + * + * @param UserFactoryInterface $userFactory The user factory to use + * + * @return void + * + * @since 1.0.0 + */ + public function setUserFactory(UserFactoryInterface $userFactory) + { + $this->userFactory = $userFactory; + } + + /** + * Get the user factory to use. + * + * @return UserFactoryInterface + * + * @since 1.0.0 + */ + public function getUserFactory(): UserFactoryInterface + { + return $this->userFactory; + } +} + diff --git a/libraries/src/Application/SessionMessageAwareInterface.php b/libraries/src/Application/SessionMessageAwareInterface.php new file mode 100644 index 0000000..d7c173b --- /dev/null +++ b/libraries/src/Application/SessionMessageAwareInterface.php @@ -0,0 +1,44 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Application; + +/** + * Application Session Message Aware Interface + * + * @since 1.0.0 + */ +interface SessionMessageAwareInterface +{ + const MSG_INFO = 'info'; + + /** + * Enqueue a system message. + * + * @param string $msg The message to enqueue. + * @param string $type The message type. Default is message. + * + * @return void + * + * @since 3.2 + */ + public function enqueueMessage(string $msg, string $type = self::MSG_INFO); + + /** + * Get the system message queue. + * + * @param boolean $clear Clear the messages currently attached to the application object + * + * @return array The system message queue. + * + * @since 3.2 + */ + public function getMessageQueue(bool $clear = false): array; +} diff --git a/libraries/src/Application/SessionMessageAwareTrait.php b/libraries/src/Application/SessionMessageAwareTrait.php new file mode 100644 index 0000000..b7b6faa --- /dev/null +++ b/libraries/src/Application/SessionMessageAwareTrait.php @@ -0,0 +1,88 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Application; + +use Joomla\Filter\InputFilter as InputFilterAlias; +use Kumwe\CMS\Filter\InputFilter; + +/** + * Trait for application classes which are identity (user) aware + * + * @since 1.0.0 + */ +trait SessionMessageAwareTrait +{ + /** + * Enqueue a system message. + * + * @param string $msg The message to enqueue. + * @param string $type The message type. Default is message. + * + * @return void + * + * @since 1.0.0 + */ + public function enqueueMessage(string $msg, string $type = self::MSG_INFO) + { + // Don't add empty messages. + if ($msg === null || trim($msg) === '') + { + return; + } + + $inputFilter = InputFilter::getInstance( + [], + [], + InputFilterAlias::ONLY_BLOCK_DEFINED_TAGS, + InputFilterAlias::ONLY_BLOCK_DEFINED_ATTRIBUTES + ); + + // Build the message array and apply the HTML InputFilter with the default blacklist to the message + $message = array( + 'message' => $inputFilter->clean($msg, 'html'), + 'type' => $inputFilter->clean(strtolower($type), 'cmd'), + ); + + // For empty queue, if messages exists in the session, enqueue them first. + $messages = $this->getMessageQueue(); + + if (!\in_array($message, $messages)) + { + // Enqueue the message. + $messages[] = $message; + + // update the session + $this->getSession()->set('application.queue', $messages); + } + } + + /** + * Get the system message queue. + * + * @param boolean $clear Clear the messages currently attached to the application object + * + * @return array The system message queue. + * + * @since 1.0.0 + */ + public function getMessageQueue(bool $clear = false): array + { + // Get messages from Session + $sessionQueue = $this->getSession()->get('application.queue', []); + + if ($clear) + { + $this->getSession()->set('application.queue', []); + } + + return $sessionQueue; + } +} diff --git a/libraries/src/Application/SiteApplication.php b/libraries/src/Application/SiteApplication.php new file mode 100644 index 0000000..96d4e66 --- /dev/null +++ b/libraries/src/Application/SiteApplication.php @@ -0,0 +1,86 @@ +controllerResolver = $controllerResolver; + $this->router = $router; + + // Call the constructor as late as possible (it runs `initialise`). + parent::__construct($input, $config, $client, $response); + } + + /** + * Method to run the application routines. + * + * @return void + */ + protected function doExecute(): void + { + $route = $this->router->parseRoute($this->get('uri.route'), $this->input->getMethod()); + + // Add variables to the input if not already set + foreach ($route->getRouteVariables() as $key => $value) + { + $this->input->def($key, $value); + } + + \call_user_func($this->controllerResolver->resolve($route)); + } +} \ No newline at end of file diff --git a/libraries/src/Asset/MixPathPackage.php b/libraries/src/Asset/MixPathPackage.php new file mode 100644 index 0000000..0544194 --- /dev/null +++ b/libraries/src/Asset/MixPathPackage.php @@ -0,0 +1,71 @@ +decoratedPackage = $decoratedPackage; + } + + /** + * Returns an absolute or root-relative public path. + * + * @param string $path A path + * + * @return string The public path + */ + public function getUrl($path) + { + if ($this->isAbsoluteUrl($path)) + { + return $path; + } + + $versionedPath = $this->getVersionStrategy()->applyVersion("/$path"); + + if ($versionedPath === $path) + { + return $this->decoratedPackage->getUrl($path); + } + + return $this->getBasePath() . ltrim($versionedPath, '/'); + } +} diff --git a/libraries/src/Autoload/ClassLoader.php b/libraries/src/Autoload/ClassLoader.php new file mode 100644 index 0000000..5c04218 --- /dev/null +++ b/libraries/src/Autoload/ClassLoader.php @@ -0,0 +1,63 @@ + + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Autoload; + +\defined('_LEXEC') or die; + +use Composer\Autoload\ClassLoader as ComposerClassLoader; + +/** + * Decorate Composer ClassLoader for Kumwe! + * + * For backward compatibility due to class aliasing in the CMS, the loadClass() method was modified to call + * the LLoader::applyAliasFor() method. + * + * @since 3.4 + */ +class ClassLoader +{ + /** + * The Composer class loader + * + * @var ComposerClassLoader + * @since 3.4 + */ + private $loader; + + /** + * Constructor + * + * @param ComposerClassLoader $loader Composer autoloader + * + * @since 3.4 + */ + public function __construct(ComposerClassLoader $loader) + { + $this->loader = $loader; + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * + * @return boolean|null True if loaded, null otherwise + * + * @since 3.4 + */ + public function loadClass($class) + { + if ($result = $this->loader->loadClass($class)) + { + \LLoader::applyAliasFor($class); + } + + return $result; + } +} diff --git a/libraries/src/Controller/DashboardController.php b/libraries/src/Controller/DashboardController.php new file mode 100644 index 0000000..018def0 --- /dev/null +++ b/libraries/src/Controller/DashboardController.php @@ -0,0 +1,91 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Controller; + +use Joomla\Application\AbstractApplication; +use Joomla\Controller\AbstractController; +use Joomla\Input\Input; +use Joomla\Uri\Uri; +use Kumwe\CMS\Controller\Util\AccessInterface; +use Kumwe\CMS\Controller\Util\AccessTrait; +use Kumwe\CMS\Controller\Util\CheckTokenInterface; +use Kumwe\CMS\Controller\Util\CheckTokenTrait; +use Kumwe\CMS\View\Admin\DashboardHtmlView; +use Laminas\Diactoros\Response\HtmlResponse; + +/** + * Controller handling the requests + * + * @method \Kumwe\CMS\Application\AdminApplication getApplication() Get the application object. + * @property-read \Kumwe\CMS\Application\AdminApplication $app Application object + */ +class DashboardController extends AbstractController implements AccessInterface, CheckTokenInterface +{ + use AccessTrait, CheckTokenTrait; + + /** + * The view object. + * + * @var DashboardHtmlView + */ + private $view; + + /** + * Constructor. + * + * @param DashboardHtmlView $view The view object. + * @param Input|null $input The input object. + * @param AbstractApplication|null $app The application object. + */ + public function __construct(DashboardHtmlView $view, Input $input = null, AbstractApplication $app = null) + { + parent::__construct($input, $app); + + $this->view = $view; + } + + /** + * Execute the controller. + * + * @return boolean + * @throws \Exception + */ + public function execute(): bool + { + // Do not Enable browser caching + $this->getApplication()->allowCache(false); + + $task = $this->getInput()->getString('task', ''); + $id = $this->getInput()->getInt('id', 0); + + $this->view->setActiveDashboard($task); + $this->view->setActiveId($id); + + // validate form token + if ('access' === $task || 'signup' === $task) + { + $this->checkToken(); + } + + // check if user is allowed to access + if ($this->allow($task)) + { + $this->getApplication()->setResponse(new HtmlResponse($this->view->render())); + } + else + { + // go to set page + $this->_redirect(); + } + + return true; + } +} diff --git a/libraries/src/Controller/ItemController.php b/libraries/src/Controller/ItemController.php new file mode 100644 index 0000000..c186aef --- /dev/null +++ b/libraries/src/Controller/ItemController.php @@ -0,0 +1,272 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Controller; + +use Joomla\Application\AbstractApplication; +use Joomla\Controller\AbstractController; +use Joomla\Filter\InputFilter as InputFilterAlias; +use Joomla\Input\Input; +use Kumwe\CMS\Controller\Util\AccessInterface; +use Kumwe\CMS\Controller\Util\AccessTrait; +use Kumwe\CMS\Controller\Util\CheckTokenInterface; +use Kumwe\CMS\Controller\Util\CheckTokenTrait; +use Kumwe\CMS\Date\Date; +use Kumwe\CMS\Factory; +use Kumwe\CMS\Filter\InputFilter; +use Kumwe\CMS\Model\ItemModel; +use Kumwe\CMS\User\User; +use Kumwe\CMS\User\UserFactoryInterface; +use Kumwe\CMS\View\Admin\ItemHtmlView; +use Laminas\Diactoros\Response\HtmlResponse; + +/** + * Controller handling the requests + * + * @method \Kumwe\CMS\Application\AdminApplication getApplication() Get the application object. + * @property-read \Kumwe\CMS\Application\AdminApplication $app Application object + */ +class ItemController extends AbstractController implements AccessInterface, CheckTokenInterface +{ + use AccessTrait, CheckTokenTrait; + + /** + * The view object. + * + * @var ItemHtmlView + */ + private $view; + + /** + * The model object. + * + * @var ItemModel + */ + private $model; + + /** + * @var InputFilter + */ + private $inputFilter; + + /** + * @var User + */ + private $user; + + /** + * Constructor. + * + * @param ItemModel $model The model object. + * @param ItemHtmlView $view The view object. + * @param Input|null $input The input object. + * @param AbstractApplication|null $app The application object. + * @param User|null $user + */ + public function __construct( + ItemModel $model, + $view, + Input $input = null, + AbstractApplication $app = null, + User $user = null) + { + parent::__construct($input, $app); + + $this->model = $model; + $this->view = $view; + $this->user = ($user) ?: Factory::getContainer()->get(UserFactoryInterface::class)->getUser(); + $this->inputFilter = InputFilter::getInstance( + [], + [], + InputFilterAlias::ONLY_BLOCK_DEFINED_TAGS, + InputFilterAlias::ONLY_BLOCK_DEFINED_ATTRIBUTES + ); + } + + /** + * Execute the controller. + * + * @return boolean + * @throws \Exception + */ + public function execute(): bool + { + // Do not Enable browser caching + $this->getApplication()->allowCache(false); + + $method = $this->getInput()->getMethod(); + $task = $this->getInput()->getString('task', ''); + $id = $this->getInput()->getInt('id', 0); + + // if task is delete + if ('delete' === $task) + { + // check that the user does not delete him/her self + if ($this->allow('item') && $this->user->get('access.item.delete', false)) + { + if ($id > 0 && $this->model->linked($id)) + { + $this->getApplication()->enqueueMessage('This item is still linked to a menu, first remove it from the menu.', 'error'); + } + elseif ($id > 0 && $this->model->delete($id)) + { + $this->getApplication()->enqueueMessage('Item was deleted!', 'success'); + } + else + { + $this->getApplication()->enqueueMessage('Item could not be deleted!', 'error'); + } + } + else + { + $this->getApplication()->enqueueMessage('You do not have permission to delete this item!', 'error'); + } + // go to set page + $this->_redirect('items'); + + return true; + } + + if ('POST' === $method) + { + // check permissions + $update = ($id > 0 && $this->user->get('access.item.update', false)); + $create = ($id == 0 && $this->user->get('access.item.create', false)); + + if ( $create || $update ) + { + $id = $this->setItem(); + } + else + { + // not allowed creating item + if ($id == 0) + { + $this->getApplication()->enqueueMessage('You do not have permission to create items!', 'error'); + } + // not allowed updating item + if ($id > 0) + { + $this->getApplication()->enqueueMessage('You do not have permission to update the item details!', 'error'); + } + } + } + + // check permissions + $read = ($id > 0 && $this->user->get('access.item.read', false)); + $create = ($id == 0 && $this->user->get('access.item.create', false)); + + // check if user is allowed to access + if ($this->allow('item') && ( $read || $create )) + { + // set values for view + $this->view->setActiveId($id); + $this->view->setActiveView('item'); + + $this->getApplication()->setResponse(new HtmlResponse($this->view->render())); + } + else + { + // not allowed creating item + if ($id == 0 && !$create) + { + $this->getApplication()->enqueueMessage('You do not have permission to create items!', 'error'); + } + // not allowed read item + if ($id > 0 && !$read) + { + $this->getApplication()->enqueueMessage('You do not have permission to read the item details!', 'error'); + } + + // go to set page + $this->_redirect('items'); + } + + return true; + } + + /** + * Set an item + * + * + * @return int + * @throws \Exception + */ + protected function setItem(): int + { + // always check the post token + $this->checkToken(); + // get the post + $post = $this->getInput()->getInputForRequestMethod(); + + // we get all the needed items + $tempItem = []; + $tempItem['id'] = $post->getInt('item_id', 0); + $tempItem['title'] = $post->getString('title', ''); + $tempItem['fulltext'] = $this->inputFilter->clean($post->getRaw('fulltext', ''), 'html'); + $tempItem['created_by_alias'] = $post->getString('created_by_alias', ''); + $tempItem['state'] = $post->getInt('state', 1); + $tempItem['metakey'] = $post->getString('metakey', ''); + $tempItem['metadesc'] = $post->getString('metadesc', ''); + $tempItem['metadata'] = $post->getString('metadata', ''); + $tempItem['publish_up'] = $post->getString('publish_up', ''); + $tempItem['publish_down'] = $post->getString('publish_down', ''); + $tempItem['featured'] = $post->getInt('featured', 0); + + // check that we have a Title + $can_save = true; + if (empty($tempItem['title'])) + { + // we show a warning message + $tempItem['title'] = ''; + $this->getApplication()->enqueueMessage('Title field is required.', 'error'); + $can_save = false; + } + // we actually can also not continue if we don't have content + if (empty($tempItem['fulltext'])) + { + // we show a warning message + $tempItem['fulltext'] = ''; + $this->getApplication()->enqueueMessage('Content field is required.', 'error'); + $can_save = false; + } + // can we save the item + if ($can_save) + { + /** @var \Kumwe\CMS\User\User $user */ + $user = Factory::getContainer()->get(UserFactoryInterface::class)->getUser(); + + $user_id = (int) $user->get('id', 0); + $today = (new Date())->toSql(); + + return $this->model->setItem( + $tempItem['id'], + $tempItem['title'], + $tempItem['fulltext'], + $tempItem['state'], + $today, + $user_id, + $tempItem['created_by_alias'], + $today, + $user_id, + $tempItem['publish_up'], + $tempItem['publish_down'], + $tempItem['metakey'], + $tempItem['metadesc'], + $tempItem['metadata'], + $tempItem['featured']); + } + + // add to model the post values + $this->model->tempItem = $tempItem; + + return $tempItem['id']; + } +} diff --git a/libraries/src/Controller/ItemsController.php b/libraries/src/Controller/ItemsController.php new file mode 100644 index 0000000..72ec480 --- /dev/null +++ b/libraries/src/Controller/ItemsController.php @@ -0,0 +1,94 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Controller; + +use Joomla\Application\AbstractApplication; +use Joomla\Controller\AbstractController; +use Joomla\Input\Input; +use Laminas\Diactoros\Response\HtmlResponse; +use Kumwe\CMS\Controller\Util\AccessInterface; +use Kumwe\CMS\Controller\Util\AccessTrait; +use Kumwe\CMS\Controller\Util\CheckTokenInterface; +use Kumwe\CMS\Controller\Util\CheckTokenTrait; +use Kumwe\CMS\Factory; +use Kumwe\CMS\User\User; +use Kumwe\CMS\User\UserFactoryInterface; +use Kumwe\CMS\View\Admin\ItemsHtmlView; + +/** + * Controller handling the requests + * + * @method \Kumwe\CMS\Application\AdminApplication getApplication() Get the application object. + * @property-read \Kumwe\CMS\Application\AdminApplication $app Application object + */ +class ItemsController extends AbstractController implements AccessInterface, CheckTokenInterface +{ + use AccessTrait, CheckTokenTrait; + + /** + * The view object. + * + * @var ItemsHtmlView + */ + private $view; + + /** + * @var User + */ + private $user; + + /** + * Constructor. + * + * @param ItemsHtmlView $view The view object. + * @param Input|null $input The input object. + * @param AbstractApplication|null $app The application object. + * @param User|null $user + */ + public function __construct( + ItemsHtmlView $view, + Input $input = null, + AbstractApplication $app = null, + User $user = null) + { + parent::__construct($input, $app); + + $this->view = $view; + $this->user = ($user) ?: Factory::getContainer()->get(UserFactoryInterface::class)->getUser(); + } + + /** + * Execute the controller. + * + * @return boolean + * @throws \Exception + */ + public function execute(): bool + { + // Do not Enable browser caching + $this->getApplication()->allowCache(false); + + $this->view->setActiveView('items'); + + // check if user is allowed to access + if ($this->allow('items') && $this->user->get('access.item.read', false)) + { + $this->getApplication()->setResponse(new HtmlResponse($this->view->render())); + } + else + { + // go to set page + $this->_redirect(); + } + + return true; + } +} diff --git a/libraries/src/Controller/LoginController.php b/libraries/src/Controller/LoginController.php new file mode 100644 index 0000000..7f56f93 --- /dev/null +++ b/libraries/src/Controller/LoginController.php @@ -0,0 +1,95 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Controller; + +use Joomla\Application\AbstractApplication; +use Joomla\Controller\AbstractController; +use Joomla\Input\Input; +use Joomla\Renderer\RendererInterface; +use Laminas\Diactoros\Response\HtmlResponse; +use Kumwe\CMS\View\Admin\DashboardHtmlView; + +/** + * Controller handling the requests + * + * @method \Kumwe\CMS\Application\SiteApplication getApplication() Get the application object. + * @property-read \Kumwe\CMS\Application\SiteApplication $app Application object + */ +class LoginController extends AbstractController +{ + /** + * The template renderer. + * + * @var RendererInterface + */ + private $renderer; + + /** + * The view object. + * + * @var DashboardHtmlView + */ + private $view; + + /** + * Constructor. + * + * @param DashboardHtmlView $view The view object. + * @param RendererInterface $renderer The template renderer. + * @param Input $input The input object. + * @param AbstractApplication $app The application object. + */ + public function __construct(DashboardHtmlView $view, RendererInterface $renderer, Input $input = null, AbstractApplication $app = null) + { + parent::__construct($input, $app); + + $this->view = $view; + $this->renderer = $renderer; + } + + /** + * Execute the controller. + * + * @return boolean + * @throws \Exception + */ + public function execute(): bool + { + // Do not Enable browser caching + $this->getApplication()->allowCache(false); + + $task = $this->getInput()->getString('account', null); + + /** @var \Kumwe\CMS\Application\AdminApplication $app */ + $app = $this->getApplication(); + + /** @var \Kumwe\CMS\User\UserFactory $userFactory */ + $userFactory = $app->getUserFactory(); + + // if the user is logged in we go to dashboard + if ($userFactory->active()) + { + $this->view->setActiveDashboard('dashboard'); + $this->view->setActiveId(0); + $this->getApplication()->setResponse(new HtmlResponse($this->view->render())); + } + elseif ('signup' === $task) + { + $this->getApplication()->setResponse(new HtmlResponse($this->renderer->render('signup.twig'))); + } + else + { + $this->getApplication()->setResponse(new HtmlResponse($this->renderer->render('login.twig'))); + } + + return true; + } +} diff --git a/libraries/src/Controller/MenuController.php b/libraries/src/Controller/MenuController.php new file mode 100644 index 0000000..50a3042 --- /dev/null +++ b/libraries/src/Controller/MenuController.php @@ -0,0 +1,249 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Controller; + +use Joomla\Application\AbstractApplication; +use Joomla\Controller\AbstractController; +use Joomla\Input\Input; +use Laminas\Diactoros\Response\HtmlResponse; +use Kumwe\CMS\Controller\Util\AccessInterface; +use Kumwe\CMS\Controller\Util\AccessTrait; +use Kumwe\CMS\Controller\Util\CheckTokenInterface; +use Kumwe\CMS\Controller\Util\CheckTokenTrait; +use Kumwe\CMS\Factory; +use Kumwe\CMS\Filter\InputFilter; +use Kumwe\CMS\Model\MenuModel; +use Kumwe\CMS\User\User; +use Kumwe\CMS\User\UserFactoryInterface; +use Kumwe\CMS\View\Admin\MenuHtmlView; + +/** + * Controller handling the requests + * + * @method \Kumwe\CMS\Application\AdminApplication getApplication() Get the application object. + * @property-read \Kumwe\CMS\Application\AdminApplication $app Application object + */ +class MenuController extends AbstractController implements AccessInterface, CheckTokenInterface +{ + use AccessTrait, CheckTokenTrait; + + /** + * The view object. + * + * @var MenuHtmlView + */ + private $view; + + /** + * The model object. + * + * @var MenuModel + */ + private $model; + + /** + * @var InputFilter + */ + private $inputFilter; + + /** + * @var User + */ + private $user; + + /** + * Constructor. + * + * @param MenuModel $model The model object. + * @param MenuHtmlView $view The view object. + * @param Input|null $input The input object. + * @param AbstractApplication|null $app The application object. + */ + public function __construct( + MenuModel $model, + MenuHtmlView $view, + Input $input = null, + AbstractApplication $app = null, + User $user = null) + { + parent::__construct($input, $app); + + $this->model = $model; + $this->view = $view; + $this->user = ($user) ?: Factory::getContainer()->get(UserFactoryInterface::class)->getUser(); + } + + /** + * Execute the controller. + * + * @return boolean + * @throws \Exception + */ + public function execute(): bool + { + // Do not Enable browser caching + $this->getApplication()->allowCache(false); + + $method = $this->getInput()->getMethod(); + $task = $this->getInput()->getString('task', ''); + $id = $this->getInput()->getInt('id', 0); + + // if task is delete + if ('delete' === $task) + { + if ($this->allow('menu') && $this->user->get('access.menu.delete', false)) + { + if ($this->model->delete($id)) + { + $this->getApplication()->enqueueMessage('Menu was deleted!', 'success'); + } + else + { + $this->getApplication()->enqueueMessage('Menu could not be deleted!', 'error'); + } + } + else + { + $this->getApplication()->enqueueMessage('You do not have permission to delete this menu!', 'error'); + } + // go to set page + $this->_redirect('menus'); + + return true; + } + + if ('POST' === $method) + { + // check permissions + $update = ($id > 0 && $this->user->get('access.menu.update', false)); + $create = ($id == 0 && $this->user->get('access.menu.create', false)); + + if ( $create || $update ) + { + $id = $this->setItem(); + } + else + { + // not allowed creating menu + if ($id == 0) + { + $this->getApplication()->enqueueMessage('You do not have permission to create menus!', 'error'); + } + // not allowed updating menu + if ($id > 0) + { + $this->getApplication()->enqueueMessage('You do not have permission to update the menu details!', 'error'); + } + } + } + + // check permissions + $read = ($id > 0 && $this->user->get('access.menu.read', false)); + $create = ($id == 0 && $this->user->get('access.menu.create', false)); + + // check if user is allowed to access + if ($this->allow('menu') && ( $read || $create )) + { + // set values for view + $this->view->setActiveId($id); + $this->view->setActiveView('menu'); + + $this->getApplication()->setResponse(new HtmlResponse($this->view->render())); + } + else + { + // not allowed creating menu + if ($id == 0 && !$create) + { + $this->getApplication()->enqueueMessage('You do not have permission to create menus!', 'error'); + } + // not allowed read menu + if ($id > 0 && !$read) + { + $this->getApplication()->enqueueMessage('You do not have permission to read the menu details!', 'error'); + } + + // go to set page + $this->_redirect('menus'); + } + + return true; + } + + /** + * Set an item + * + * + * @return int + * @throws \Exception + */ + protected function setItem(): int + { + // always check the post token + $this->checkToken(); + // get the post + $post = $this->getInput()->getInputForRequestMethod(); + + // we get all the needed items + $tempItem = []; + $tempItem['id'] = $post->getInt('menu_id', 0); + $tempItem['title'] = $post->getString('title', ''); + $tempItem['alias'] = $post->getString('alias', ''); + $tempItem['path'] = $post->getString('path', ''); + $tempItem['item_id'] = $post->getInt('item_id', 0); + $tempItem['published'] = $post->getInt('published', 1); + $tempItem['publish_up'] = $post->getString('publish_up', ''); + $tempItem['publish_down'] = $post->getString('publish_down', ''); + $tempItem['position'] = $post->getString('position', 'center'); + $tempItem['home'] = $post->getInt('home', 0); + $tempItem['parent_id'] = $post->getInt('parent_id', 0); + + // check that we have a Title + $can_save = true; + if (empty($tempItem['title'])) + { + // we show a warning message + $tempItem['title'] = ''; + $this->getApplication()->enqueueMessage('Title field is required.', 'error'); + $can_save = false; + } + // we actually can also not continue if we don't have content + if (empty($tempItem['item_id']) || $tempItem['item_id'] == 0) + { + // we show a warning message + $tempItem['item_id'] = 0; + $this->getApplication()->enqueueMessage('Item field is required.', 'error'); + $can_save = false; + } + + // can we save the item + if ($can_save) + { + return $this->model->setItem( + $tempItem['id'], + $tempItem['title'], + $tempItem['alias'], + $tempItem['item_id'], + $tempItem['path'], + $tempItem['published'], + $tempItem['publish_up'], + $tempItem['publish_down'], + $tempItem['position'], + $tempItem['home'], + $tempItem['parent_id']); + } + + // add to model the post values + $this->model->tempItem = $tempItem; + + return $tempItem['id']; + } +} diff --git a/libraries/src/Controller/MenusController.php b/libraries/src/Controller/MenusController.php new file mode 100644 index 0000000..b016c18 --- /dev/null +++ b/libraries/src/Controller/MenusController.php @@ -0,0 +1,94 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Controller; + +use Joomla\Application\AbstractApplication; +use Joomla\Controller\AbstractController; +use Joomla\Input\Input; +use Laminas\Diactoros\Response\HtmlResponse; +use Kumwe\CMS\Controller\Util\AccessInterface; +use Kumwe\CMS\Controller\Util\AccessTrait; +use Kumwe\CMS\Controller\Util\CheckTokenInterface; +use Kumwe\CMS\Controller\Util\CheckTokenTrait; +use Kumwe\CMS\Factory; +use Kumwe\CMS\User\User; +use Kumwe\CMS\User\UserFactoryInterface; +use Kumwe\CMS\View\Admin\MenusHtmlView; + +/** + * Controller handling the requests + * + * @method \Kumwe\CMS\Application\AdminApplication getApplication() Get the application object. + * @property-read \Kumwe\CMS\Application\AdminApplication $app Application object + */ +class MenusController extends AbstractController implements AccessInterface, CheckTokenInterface +{ + use AccessTrait, CheckTokenTrait; + + /** + * The view object. + * + * @var MenusHtmlView + */ + private $view; + + /** + * @var User + */ + private $user; + + /** + * Constructor. + * + * @param MenusHtmlView $view The view object. + * @param Input|null $input The input object. + * @param AbstractApplication|null $app The application object. + * @param User|null $user The user object. + */ + public function __construct( + MenusHtmlView $view, + Input $input = null, + AbstractApplication $app = null, + User $user = null) + { + parent::__construct($input, $app); + + $this->view = $view; + $this->user = ($user) ?: Factory::getContainer()->get(UserFactoryInterface::class)->getUser(); + } + + /** + * Execute the controller. + * + * @return boolean + * @throws \Exception + */ + public function execute(): bool + { + // Do not Enable browser caching + $this->getApplication()->allowCache(false); + + $this->view->setActiveView('menus'); + + // check if user is allowed to access + if ($this->allow('menus') && $this->user->get('access.menu.read', false)) + { + $this->getApplication()->setResponse(new HtmlResponse($this->view->render())); + } + else + { + // go to set page + $this->_redirect(); + } + + return true; + } +} diff --git a/libraries/src/Controller/PageController.php b/libraries/src/Controller/PageController.php new file mode 100644 index 0000000..e8f530a --- /dev/null +++ b/libraries/src/Controller/PageController.php @@ -0,0 +1,105 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Controller; + +use Joomla\Application\AbstractApplication; +use Joomla\Controller\AbstractController; +use Joomla\Input\Input; +use Joomla\Uri\Uri; +use Kumwe\CMS\Utilities\StringHelper; +use Kumwe\CMS\View\Site\PageHtmlView; +use Laminas\Diactoros\Response\HtmlResponse; +use Laminas\Diactoros\Response\RedirectResponse; + +/** + * Controller handling the requests + * + * @method \Kumwe\CMS\Application\SiteApplication getApplication() Get the application object. + * @property-read \Kumwe\CMS\Application\SiteApplication $app Application object + */ +class PageController extends AbstractController +{ + /** + * The view object. + * + * @var PageHtmlView + */ + private $view; + + /** + * Constructor. + * + * @param PageHtmlView $view The view object. + * @param Input|null $input The input object. + * @param AbstractApplication|null $app The application object. + */ + public function __construct(PageHtmlView $view, Input $input = null, AbstractApplication $app = null) + { + parent::__construct($input, $app); + + $this->view = $view; + } + + /** + * Execute the controller. + * + * @return boolean + */ + public function execute(): bool + { + // Disable all cache for now + $this->getApplication()->allowCache(false); + + // get the root name + $root = $this->getInput()->getString('root', ''); + // start building the full path + $path = []; + $path[] = $root; + // set a mad depth TODO: we should limit the menu depth to 6 or something + $depth = range(1,20); + // load the whole path + foreach ($depth as $page) + { + $page = StringHelper::numbers($page); + // check if there is a value + $result = $this->getInput()->getString($page, false); + if ($result) + { + $path[] = $result; + } + else + { + // first false means we are at the end of the line + break; + } + } + // set the final path + $path = implode('/', $path); + + // if for some reason the view value is administrator + if ('administrator' === $root) + { + // get uri request to get host + $uri = new Uri($this->getApplication()->get('uri.request')); + + // Redirect to the administrator area + $this->getApplication()->setResponse(new RedirectResponse($uri->getScheme() . '://' . $uri->getHost() . '/administrator/', 301)); + } + else + { + $this->view->setPage($path); + + $this->getApplication()->setResponse(new HtmlResponse($this->view->render())); + } + + return true; + } +} diff --git a/libraries/src/Controller/UserController.php b/libraries/src/Controller/UserController.php new file mode 100644 index 0000000..e1a314f --- /dev/null +++ b/libraries/src/Controller/UserController.php @@ -0,0 +1,452 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Controller; + +use Exception; +use Joomla\Application\AbstractApplication; +use Joomla\Controller\AbstractController; +use Joomla\Input\Input; +use Joomla\Authentication\Password\BCryptHandler; +use Laminas\Diactoros\Response\HtmlResponse; +use Kumwe\CMS\Controller\Util\AccessInterface; +use Kumwe\CMS\Controller\Util\AccessTrait; +use Kumwe\CMS\Controller\Util\CheckTokenInterface; +use Kumwe\CMS\Controller\Util\CheckTokenTrait; +use Kumwe\CMS\Date\Date; +use Kumwe\CMS\Factory; +use Kumwe\CMS\Model\UserModel; +use Kumwe\CMS\User\User; +use Kumwe\CMS\User\UserFactoryInterface; +use Kumwe\CMS\View\Admin\UserHtmlView; + +/** + * Controller handling the requests + * + * @method \Kumwe\CMS\Application\AdminApplication getApplication() Get the application object. + * @property-read \Kumwe\CMS\Application\AdminApplication $app Application object + */ +class UserController extends AbstractController implements AccessInterface, CheckTokenInterface +{ + use AccessTrait, CheckTokenTrait; + + /** + * The view object. + * + * @var UserHtmlView + */ + private $view; + + /** + * The model object. + * + * @var UserModel + */ + private $model; + + /** + * @var BCryptHandler + */ + private $secure; + + /** + * @var User + */ + private $user; + + /** + * Constructor. + * + * @param UserModel $model The model object. + * @param UserHtmlView $view The view object. + * @param Input|null $input The input object. + * @param User|null $user The current user. + * @param AbstractApplication|null $app The application object. + */ + public function __construct( + UserModel $model, + UserHtmlView $view, + Input $input = null, + AbstractApplication $app = null, + User $user = null, + BCryptHandler $secure = null) + { + parent::__construct($input, $app); + + $this->model = $model; + $this->view = $view; + $this->user = ($user) ?: Factory::getContainer()->get(UserFactoryInterface::class)->getUser(); + $this->secure = ($secure) ?: new BCryptHandler(); + } + + /** + * Execute the controller. + * + * @return boolean + * @throws Exception + */ + public function execute(): bool + { + // Do not Enable browser caching + $this->getApplication()->allowCache(false); + + $method = $this->getInput()->getMethod(); + $task = $this->getInput()->getString('task', ''); + $id = $this->getInput()->getInt('id', 0); + + // if task is delete + if ('delete' === $task) + { + // check that the user does not delete him/her self + if ($this->allow('user') && $this->user->get('access.user.delete', false)) + { + // get the current user being deleted + /** @var \Kumwe\CMS\User\User $userBeingDeleted */ + $userBeingDeleted = Factory::getContainer()->get(UserFactoryInterface::class)->getUser($id); + // get the current active user ID + $user_id = $this->user->get('id', -1); + // is this the same user account as the active user + if ($user_id == $id) + { + $this->getApplication()->enqueueMessage('You can not delete your own account!', 'warning'); + } + elseif ($userBeingDeleted->get('is_admin', false) && !$this->user->get('is_admin', false)) + { + $this->getApplication()->enqueueMessage('You dont have the permission to delete an administrator account!', 'error'); + } + elseif ($this->model->delete($id)) + { + $this->getApplication()->enqueueMessage('User was deleted!', 'success'); + } + else + { + $this->getApplication()->enqueueMessage('User could not be deleted!', 'error'); + } + } + else + { + $this->getApplication()->enqueueMessage('You do not have permission to delete this user!', 'error'); + } + // go to set page + $this->_redirect('users'); + + return true; + } + + // set the current user ID + $user_id = $this->user->get('id', -1); + + if ('POST' === $method) + { + // always check the post token + $this->checkToken(); + // get the post + $post = $this->getInput()->getInputForRequestMethod(); + + // we get all the needed items + $tempItem = $post->getArray(['groups' => 'INT']); + $tempItem['id'] = $post->getInt('user_id', 0); + $tempItem['name'] = $post->getString('name', ''); + $tempItem['username'] = $post->getUsername('username', ''); + $tempItem['password'] = $post->getString('password', ''); + $tempItem['password2'] = $post->getString('password2', ''); + $tempItem['email'] = $post->getString('email', ''); + $tempItem['block'] = $post->getInt('block', 1); + $tempItem['sendEmail'] = $post->getInt('sendEmail', 1); + $tempItem['activation'] = $post->getInt('activation', 0); + + // check permissions + $update = ($tempItem['id'] > 0 && $this->user->get('access.user.update', false)); + $create = ($tempItem['id'] == 0 && $this->user->get('access.user.create', false)); + $selfUpdate = ($tempItem['id'] > 0 && $tempItem['id'] == $user_id); + + if ($create || $update || $selfUpdate) + { + $id = $this->setItem($tempItem); + } + else + { + // not allowed creating user + if ($id == 0) + { + $this->getApplication()->enqueueMessage('You do not have permission to create users!', 'error'); + } + // not allowed updating user + if ($id > 0) + { + $this->getApplication()->enqueueMessage('You do not have permission to update the user details!', 'error'); + } + } + } + + // check permissions + $read = ($id > 0 && $this->user->get('access.user.read', false)); + $create = ($id == 0 && $this->user->get('access.user.create', false)); + $selfUpdate = ($id > 0 && $id == $user_id); + + // check if user is allowed to access + if ($this->allow('user') && ($read || $create || $selfUpdate)) + { + // set values for view + $this->view->setActiveId($id); + $this->view->setActiveView('user'); + + $this->getApplication()->setResponse(new HtmlResponse($this->view->render())); + } + else + { + // not allowed creating user + if ($id == 0 && !$create) + { + $this->getApplication()->enqueueMessage('You do not have permission to create users!', 'error'); + } + // not allowed updating user + if ($id > 0 && !$read) + { + $this->getApplication()->enqueueMessage('You do not have permission to read the user details!', 'error'); + } + + // go to set page + $this->_redirect('users'); + } + + return true; + } + + /** + * Set an item + * + * @param array $tempItem + * + * @return int + * @throws Exception + */ + protected function setItem(array $tempItem): int + { + $can_save = true; + // check that we have a name + if (empty($tempItem['name'])) + { + // we show an error message + $tempItem['name'] = ''; + $this->getApplication()->enqueueMessage('Name field is required.', 'error'); + $can_save = false; + } + // check that we have a username + if (empty($tempItem['username'])) + { + // we show an error message + $tempItem['username'] = ''; + $this->getApplication()->enqueueMessage('Username field is required.', 'error'); + $can_save = false; + } + // check that we have an email TODO: check that we have a valid email + if (empty($tempItem['email'])) + { + // we show an error message + $tempItem['email'] = ''; + $this->getApplication()->enqueueMessage('Email field is required.', 'error'); + $can_save = false; + } + // check passwords + if (isset($tempItem['password2']) && $tempItem['password'] != $tempItem['password2']) + { + // we show an error message + $tempItem['password'] = 'xxxxxxxxxx'; + $tempItem['password2'] = 'xxxxxxxxxx'; + $this->getApplication()->enqueueMessage('Passwords do not match.', 'error'); + $can_save = false; + } + unset ($tempItem['password2']); + // do not set password that has not changed + if ($tempItem['password'] === 'xxxxxxxxxx') + { + if ($tempItem['id'] == 0) + { + // we show an error message + $tempItem['password'] = 'xxxxxxxxxx'; + $tempItem['password2'] = 'xxxxxxxxxx'; + $this->getApplication()->enqueueMessage('Passwords not set.', 'error'); + $can_save = false; + } + else + { + $tempItem['password'] = ''; + } + } + elseif (strlen($tempItem['password']) < 7) + { + // we show an error message + $tempItem['password'] = 'xxxxxxxxxx'; + $tempItem['password2'] = 'xxxxxxxxxx'; + $this->getApplication()->enqueueMessage('Passwords must be longer than 6 characters.', 'error'); + $can_save = false; + } + else + { + // hash the password + $tempItem['password'] = $this->secure->hashPassword($tempItem['password']); + } + + // can we save the item + if ($can_save) + { + // check that the user does not block him/her self + $user_id = $this->user->get('id', -1); + $block_status = $tempItem['block']; + // this user is the current user + if ($user_id == $tempItem['id']) + { + // don't allow user to block self + if ($tempItem['block'] != 0) + { + // we show a warning message + $this->getApplication()->enqueueMessage('You can not block yourself!', 'warning'); + $tempItem['block'] = 0; + } + // don't allow user remove self from admin groups + if ($this->user->get('is_admin', false)) + { + $admin_groups = $this->user->get('is_admin_groups', []); + if (is_array($admin_groups) && count($admin_groups) > 0) + { + $notice_set_groups = true; + foreach ($admin_groups as $admin_group) + { + if (!is_array($tempItem['groups']) || !in_array($admin_group, $tempItem['groups'])) + { + if ($notice_set_groups) + { + // we show a warning message + $this->getApplication()->enqueueMessage('You can not remove yourself from the administrator group!', 'warning'); + $notice_set_groups = false; + } + $tempItem['groups'][] = $admin_group; + } + } + } + else + { + // we show an error message + $this->getApplication()->enqueueMessage('There is a problem with the admin user groups, we can not save the user details.', 'error'); + $can_save = false; + } + } + } + + // can we save the item + if ($can_save) + { + // check that the user will have some groups left + if (!is_array($tempItem['groups']) || count($tempItem['groups']) == 0) + { + // we show a warning message + $this->getApplication()->enqueueMessage('You must select at least one group.', 'warning'); + // this user is the current user + if ($user_id == $tempItem['id']) + { + $tempItem['groups'] = $this->user->get('groups_ids', []); + // check if we still have no groups + if (count($tempItem['groups']) == 0) + { + $can_save = false; + } + } + else + { + $can_save = false; + } + } + } + + // can we save the item + if ($can_save) + { + // none admin restrictions TODO would like to move this to the database and not hard code it + if (!$this->user->get('is_admin', false)) + { + // with existing users + if ($tempItem['id'] > 0) + { + // get the current user being saved + /** @var \Kumwe\CMS\User\User $userBeingSaved */ + $userBeingSaved = Factory::getContainer()->get(UserFactoryInterface::class)->getUser($tempItem['id']); + // don't allow block status change by none admin users + $block = $userBeingSaved->get('block', 1); + $current_posted_block = $tempItem['block']; + // if the status changed we revert and give message + // we allow block but not un-block + if ($block != $current_posted_block && $current_posted_block == 0) + { + // we show a warning message + $this->getApplication()->enqueueMessage('Only the administrator can update user access to system.', 'warning'); + $tempItem['block'] = 1; + } + // get current group to see if we must give a notice + $groups = $userBeingSaved->get('groups_ids', []); + $current_posted_groups = $tempItem['groups']; + sort($groups); + sort($current_posted_groups); + // if the groups changes we give a message + if ($groups !== $current_posted_groups) + { + // we show a warning message + $this->getApplication()->enqueueMessage('Only the administrator can update user group selection.', 'warning'); + } + // if the current user being saved is an admin account + // we don't allow the following changes + if ($userBeingSaved->get('is_admin', false)) + { + // we do not allow password changes of admin accounts + $tempItem['password'] = ''; + // we don't allow username changes + $tempItem['username'] = $userBeingSaved->get('username', $tempItem['username']); + // we don't allow change of status + if ($block != $current_posted_block) + { + // we show an error message + $this->getApplication()->enqueueMessage('Only the administrator can update another administrator account access to system.', 'error'); + $tempItem['block'] = $block; + } + } + } + else + { + // new users created by none admin must be blocked by default + // since only admin can unblock any users + $tempItem['block'] = 1; + } + // only admin can change groups + // empty groups will not get updated + $tempItem['groups'] = []; + } + + $today = (new Date())->toSql(); + + return $this->model->setItem( + $tempItem['id'], + $tempItem['name'], + $tempItem['username'], + $tempItem['groups'], + $tempItem['email'], + $tempItem['password'], + $tempItem['block'], + $tempItem['sendEmail'], + $today, + $tempItem['activation']); + } + } + + // add to model the post values + $this->model->tempItem = $tempItem; + + return $tempItem['id']; + } +} diff --git a/libraries/src/Controller/UserGroupController.php b/libraries/src/Controller/UserGroupController.php new file mode 100644 index 0000000..a167c73 --- /dev/null +++ b/libraries/src/Controller/UserGroupController.php @@ -0,0 +1,262 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Controller; + +use Joomla\Application\AbstractApplication; +use Joomla\Controller\AbstractController; +use Joomla\Input\Input; +use Kumwe\CMS\Controller\Util\AccessInterface; +use Kumwe\CMS\Controller\Util\AccessTrait; +use Kumwe\CMS\Controller\Util\CheckTokenInterface; +use Kumwe\CMS\Controller\Util\CheckTokenTrait; +use Kumwe\CMS\Factory; +use Kumwe\CMS\Model\UsergroupModel; +use Kumwe\CMS\User\User; +use Kumwe\CMS\User\UserFactoryInterface; +use Kumwe\CMS\View\Admin\UsergroupHtmlView; +use Laminas\Diactoros\Response\HtmlResponse; + +/** + * Controller handling the requests + * + * @method \Kumwe\CMS\Application\AdminApplication getApplication() Get the application object. + * @property-read \Kumwe\CMS\Application\AdminApplication $app Application object + */ +class UserGroupController extends AbstractController implements AccessInterface, CheckTokenInterface +{ + use AccessTrait, CheckTokenTrait; + + /** + * The view object. + * + * @var UsergroupHtmlView + */ + private $view; + + /** + * The model object. + * + * @var UsergroupModel + */ + private $model; + + /** + * @var User + */ + private $user; + + /** + * Constructor. + * + * @param UsergroupModel $model The model object. + * @param UsergroupHtmlView $view The view object. + * @param Input|null $input The input object. + * @param AbstractApplication|null $app The application object. + */ + public function __construct( + UsergroupModel $model, + UsergroupHtmlView $view, + Input $input = null, + AbstractApplication $app = null, + User $user = null) + { + parent::__construct($input, $app); + + $this->model = $model; + $this->view = $view; + $this->user = ($user) ?: Factory::getContainer()->get(UserFactoryInterface::class)->getUser(); + } + + /** + * Execute the controller. + * + * @return boolean + * @throws \Exception + */ + public function execute(): bool + { + // Do not Enable browser caching + $this->getApplication()->allowCache(false); + + $method = $this->getInput()->getMethod(); + $task = $this->getInput()->getString('task', ''); + $id = $this->getInput()->getInt('id', 0); + + // if task is delete + if ('delete' === $task) + { + if ($this->allow('usergroup') && $this->user->get('access.usergroup.delete', false)) + { + // TODO not ideal to hard code any ID + if ($id == 1) + { + $this->getApplication()->enqueueMessage('This is the administrator user group that can not be deleted.', 'error'); + } + elseif ($id > 0 && $this->model->linked($id)) + { + $this->getApplication()->enqueueMessage('This user group is still in use and can therefore no be deleted.', 'error'); + } + elseif ($this->model->delete($id)) + { + $this->getApplication()->enqueueMessage('User group was deleted!', 'success'); + } + else + { + $this->getApplication()->enqueueMessage('User group could not be deleted!', 'error'); + } + } + else + { + $this->getApplication()->enqueueMessage('You do not have permission to delete this user group!', 'error'); + } + // go to set page + $this->_redirect('usergroups'); + + return true; + } + + if ('POST' === $method) + { + // check permissions + $update = ($id > 0 && $this->user->get('access.usergroup.update', false)); + $create = ($id == 0 && $this->user->get('access.usergroup.create', false)); + + // TODO not ideal to hard code any ID + if ($id == 1 && $update) + { + $this->getApplication()->enqueueMessage('This is the administrator user group that can not change.', 'error'); + } + elseif ( $create || $update ) + { + $id = $this->setItem(); + } + else + { + // not allowed creating user group + if ($id == 0) + { + $this->getApplication()->enqueueMessage('You do not have permission to create user groups!', 'error'); + } + // not allowed updating user group + if ($id > 0) + { + $this->getApplication()->enqueueMessage('You do not have permission to update the user group details!', 'error'); + } + } + } + + // check permissions + $read = ($id > 0 && $this->user->get('access.usergroup.read', false)); + $create = ($id == 0 && $this->user->get('access.usergroup.create', false)); + + // check if user is allowed to access + if ($this->allow('usergroup') && ( $read || $create )) + { + // set values for view + $this->view->setActiveId($id); + $this->view->setActiveView('usergroup'); + + $this->getApplication()->setResponse(new HtmlResponse($this->view->render())); + } + else + { + // not allowed creating user group + if ($id == 0 && !$create) + { + $this->getApplication()->enqueueMessage('You do not have permission to create user groups!', 'error'); + } + // not allowed read user group + if ($id > 0 && !$read) + { + $this->getApplication()->enqueueMessage('You do not have permission to read the user group details!', 'error'); + } + + // go to set page + $this->_redirect('items'); + } + + return true; + } + + /** + * Set an item + * + * @return int + * @throws \Exception + */ + protected function setItem(): int + { + // always check the post token + $this->checkToken(); + // get the post + $post = $this->getInput()->getInputForRequestMethod(); + + // we get all the needed items + $tempItem = $post->getArray(['params' => 'STRING']);; + $tempItem['id'] = $post->getInt('usergroup_id', 0); + $tempItem['title'] = $post->getString('title', ''); + + $can_save = true; + // check that we have a name + if (empty($tempItem['title'])) + { + // we show a warning message + $tempItem['name'] = ''; + $this->getApplication()->enqueueMessage('User group name field is required.', 'error'); + $can_save = false; + } + // set the params + $build_params = $this->model->getGroupDefaultsAccess(); + if (isset($tempItem['params']) && is_array($tempItem['params']) && count($tempItem['params'])) + { + $only = 'CRUD'; + foreach ($build_params as $n => &$item) + { + if (isset($tempItem['params'][$item->area]) && strlen($tempItem['params'][$item->area])) + { + $array_of_access = str_split(strtoupper($tempItem['params'][$item->area])); + $access_keeper = []; + if ($array_of_access) + { + foreach ($array_of_access as $char) + { + if (strpos($only, $char) === false) + { + $this->getApplication()->enqueueMessage("User group access in ({$item->area} area) had a wrong key ({$char}) so we removed it. Please only use the keys prescribed.", 'warning'); + } + else + { + $access_keeper[] = $char; + } + } + } + $item->access = implode($access_keeper); + } + } + } + // update the params + $tempItem['params'] = $build_params; + + // can we save the item + if ($can_save) + { + return $this->model->setItem( + $tempItem['id'], + $tempItem['title'], + $tempItem['params']); + } + + // add to model the post values + $this->model->tempItem = $tempItem; + + return $tempItem['id']; + } +} diff --git a/libraries/src/Controller/UsergroupsController.php b/libraries/src/Controller/UsergroupsController.php new file mode 100644 index 0000000..d38ddb3 --- /dev/null +++ b/libraries/src/Controller/UsergroupsController.php @@ -0,0 +1,94 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Controller; + +use Joomla\Application\AbstractApplication; +use Joomla\Controller\AbstractController; +use Joomla\Input\Input; +use Kumwe\CMS\Controller\Util\AccessInterface; +use Kumwe\CMS\Controller\Util\AccessTrait; +use Kumwe\CMS\Controller\Util\CheckTokenInterface; +use Kumwe\CMS\Controller\Util\CheckTokenTrait; +use Kumwe\CMS\Factory; +use Kumwe\CMS\User\User; +use Kumwe\CMS\User\UserFactoryInterface; +use Kumwe\CMS\View\Admin\UsergroupsHtmlView; +use Laminas\Diactoros\Response\HtmlResponse; + +/** + * Controller handling the requests + * + * @method \Kumwe\CMS\Application\AdminApplication getApplication() Get the application object. + * @property-read \Kumwe\CMS\Application\AdminApplication $app Application object + */ +class UsergroupsController extends AbstractController implements AccessInterface, CheckTokenInterface +{ + use AccessTrait, CheckTokenTrait; + + /** + * The view object. + * + * @var UsergroupsHtmlView + */ + private $view; + + /** + * @var User + */ + private $user; + + /** + * Constructor. + * + * @param UsergroupsHtmlView $view The view object. + * @param Input|null $input The input object. + * @param AbstractApplication|null $app The application object. + * @param User|null $user + */ + public function __construct( + UsergroupsHtmlView $view, + Input $input = null, + AbstractApplication $app = null, + User $user = null) + { + parent::__construct($input, $app); + + $this->view = $view; + $this->user = ($user) ?: Factory::getContainer()->get(UserFactoryInterface::class)->getUser(); + } + + /** + * Execute the controller. + * + * @return boolean + * @throws \Exception + */ + public function execute(): bool + { + // Do not Enable browser caching + $this->getApplication()->allowCache(false); + + $this->view->setActiveView('usergroups'); + + // check if user is allowed to access + if ($this->allow('usergroups') && $this->user->get('access.usergroup.read', false)) + { + $this->getApplication()->setResponse(new HtmlResponse($this->view->render())); + } + else + { + // go to set page + $this->_redirect(); + } + + return true; + } +} diff --git a/libraries/src/Controller/UsersController.php b/libraries/src/Controller/UsersController.php new file mode 100644 index 0000000..aef6fd2 --- /dev/null +++ b/libraries/src/Controller/UsersController.php @@ -0,0 +1,93 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Controller; + +use Joomla\Application\AbstractApplication; +use Joomla\Controller\AbstractController; +use Joomla\Input\Input; +use Kumwe\CMS\Controller\Util\AccessInterface; +use Kumwe\CMS\Controller\Util\AccessTrait; +use Kumwe\CMS\Controller\Util\CheckTokenInterface; +use Kumwe\CMS\Controller\Util\CheckTokenTrait; +use Kumwe\CMS\Factory; +use Kumwe\CMS\User\User; +use Kumwe\CMS\User\UserFactoryInterface; +use Kumwe\CMS\View\Admin\UsersHtmlView; +use Laminas\Diactoros\Response\HtmlResponse; + +/** + * Controller handling the requests + * + * @method \Kumwe\CMS\Application\AdminApplication getApplication() Get the application object. + * @property-read \Kumwe\CMS\Application\AdminApplication $app Application object + */ +class UsersController extends AbstractController implements AccessInterface, CheckTokenInterface +{ + use AccessTrait, CheckTokenTrait; + + /** + * The view object. + * + * @var UsersHtmlView + */ + private $view; + + /** + * @var User + */ + private $user; + + /** + * Constructor. + * + * @param UsersHtmlView $view The view object. + * @param Input $input The input object. + * @param AbstractApplication $app The application object. + */ + public function __construct( + UsersHtmlView $view, + Input $input = null, + AbstractApplication $app = null, + User $user = null) + { + parent::__construct($input, $app); + + $this->view = $view; + $this->user = ($user) ?: Factory::getContainer()->get(UserFactoryInterface::class)->getUser(); + } + + /** + * Execute the controller. + * + * @return boolean + * @throws \Exception + */ + public function execute(): bool + { + // Do not Enable browser caching + $this->getApplication()->allowCache(false); + + $this->view->setActiveView('users'); + + // check if user is allowed to access + if ($this->allow('users') && $this->user->get('access.user.read', false)) + { + $this->getApplication()->setResponse(new HtmlResponse($this->view->render())); + } + else + { + // go to set page + $this->_redirect(); + } + + return true; + } +} diff --git a/libraries/src/Controller/Util/AccessInterface.php b/libraries/src/Controller/Util/AccessInterface.php new file mode 100644 index 0000000..ae3ba59 --- /dev/null +++ b/libraries/src/Controller/Util/AccessInterface.php @@ -0,0 +1,28 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Controller\Util; + +/** + * Class for checking the user access + * + * @since 1.0.0 + */ +interface AccessInterface +{ + /** + * @param string $task + * @param string $default + * + * @return bool + * @throws \Exception + */ + public function allow(string $task, string $default = ''): bool; +} diff --git a/libraries/src/Controller/Util/AccessTrait.php b/libraries/src/Controller/Util/AccessTrait.php new file mode 100644 index 0000000..f5a8304 --- /dev/null +++ b/libraries/src/Controller/Util/AccessTrait.php @@ -0,0 +1,111 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Controller\Util; + +use Joomla\Uri\Uri; + +/** + * Trait for checking the user access + * + * @since 1.0.0 + */ +trait AccessTrait +{ + /** + * When access not allowed this is the path to redirect to + * + * @var string + */ + private $noAccessRedirect = ''; + + /** + * Check if user is allowed to access this area + * + * @param string $task + * @param string $default + * + * @return bool + * @throws \Exception + */ + public function allow(string $task = 'post', string $default = ''): bool + { + // our little access controller TODO: we can do better + $has_access = false; + + /** @var \Kumwe\CMS\Application\AdminApplication $app */ + $app = $this->getApplication(); + + /** @var \Kumwe\CMS\User\UserFactory $userFactory */ + $userFactory = $app->getUserFactory(); + + // user actions [logout] + if ('logout' === $task) + { + if ($userFactory->logout()) + { + $this->noAccessRedirect = '/'; + // clear the message queue + $app->getMessageQueue(true); + } + } + // check if this is a user valid + elseif ($userFactory->active()) + { + $has_access = true; + } + // user actions [access, signup] + elseif ('access' === $task || 'signup' === $task) + { + if ('access' === $task) + { + if ($userFactory->login()) + { + $has_access = true; + } + } + else + { + if ($userFactory->create()) + { + $has_access = true; + } + else + { + $this->noAccessRedirect = '/?account=signup'; + } + } + + // we by default always load the dashboard + $this->view->setActiveDashboard($default); + } + + return $has_access; + } + + /** + * @param string|null $target + * + * @return void + */ + private function _redirect(string $target = null) + { + // get uri request to get host + $uri = new Uri($this->getApplication()->get('uri.request')); + + // get redirect path + $redirect = (!empty($target)) ? $target : $this->noAccessRedirect; + // fix the path + $path = $uri->getPath(); + $path = substr($path, 0, strripos($path, '/')) . '/' . $redirect; + // redirect to the set area + $this->getApplication()->redirect($uri->getScheme() . '://' . $uri->getHost() . $path ); + } +} diff --git a/libraries/src/Controller/Util/CheckTokenInterface.php b/libraries/src/Controller/Util/CheckTokenInterface.php new file mode 100644 index 0000000..db784b8 --- /dev/null +++ b/libraries/src/Controller/Util/CheckTokenInterface.php @@ -0,0 +1,26 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Controller\Util; + +/** + * Class for checking the form had a token + * + * @since 1.0.0 + */ +interface CheckTokenInterface +{ + /** + * Check the token of the form + * + * @return bool + */ + public function checkToken(): bool; +} diff --git a/libraries/src/Controller/Util/CheckTokenTrait.php b/libraries/src/Controller/Util/CheckTokenTrait.php new file mode 100644 index 0000000..d4865a6 --- /dev/null +++ b/libraries/src/Controller/Util/CheckTokenTrait.php @@ -0,0 +1,36 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Controller\Util; + +/** + * Class for checking the form had a token + * + * @since 1.0.0 + */ +trait CheckTokenTrait +{ + /** + * Check the token of the form + * + * @return bool + */ + public function checkToken(): bool + { + $token = $this->getApplication()->getSession()->getToken(); + $form_token = $this->getInput()->getString($token, 0); + + if ($form_token == 0) + { + exit('Invalid form token'); + } + return true; + } +} diff --git a/libraries/src/Controller/WrongCmsController.php b/libraries/src/Controller/WrongCmsController.php new file mode 100644 index 0000000..a8e90a0 --- /dev/null +++ b/libraries/src/Controller/WrongCmsController.php @@ -0,0 +1,40 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Controller; + +use Joomla\Controller\AbstractController; +use Laminas\Diactoros\Response\TextResponse; + +/** + * Controller class to display a message to individuals looking for the wrong CMS + * + * @method \Kumwe\CMS\Application\SiteApplication getApplication() Get the application object. + * @property-read \Kumwe\CMS\Application\SiteApplication $app Application object + */ +class WrongCmsController extends AbstractController +{ + /** + * Execute the controller. + * + * @return boolean + */ + public function execute(): bool + { + // Enable browser caching + $this->getApplication()->allowCache(true); + + $response = new TextResponse("This isn't the what you're looking for.", 404); + + $this->getApplication()->setResponse($response); + + return true; + } +} diff --git a/libraries/src/Date/Date.php b/libraries/src/Date/Date.php new file mode 100644 index 0000000..3a6e31a --- /dev/null +++ b/libraries/src/Date/Date.php @@ -0,0 +1,484 @@ + + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Date; + +use Joomla\Database\DatabaseInterface; +use Kumwe\CMS\Factory; + +/** + * Date is a class that stores a date and provides logic to manipulate + * and render that date in a variety of formats. + * + * @method Date|bool add(\DateInterval $interval) Adds an amount of days, months, years, hours, minutes and seconds to a Date object. + * @method Date|bool sub(\DateInterval $interval) Subtracts an amount of days, months, years, hours, minutes and seconds from a Date object. + * @method Date|bool modify(string $modify) Alter the timestamp of this object by incre/decre-menting in a format accepted by strtotime(). + * + * @property-read string $daysinmonth t - Number of days in the given month. + * @property-read string $dayofweek N - ISO-8601 numeric representation of the day of the week. + * @property-read string $dayofyear z - The day of the year (starting from 0). + * @property-read boolean $isleapyear L - Whether it's a leap year. + * @property-read string $day d - Day of the month, 2 digits with leading zeros. + * @property-read string $hour H - 24-hour format of an hour with leading zeros. + * @property-read string $minute i - Minutes with leading zeros. + * @property-read string $second s - Seconds with leading zeros. + * @property-read string $microsecond u - Microseconds with leading zeros. + * @property-read string $month m - Numeric representation of a month, with leading zeros. + * @property-read string $ordinal S - English ordinal suffix for the day of the month, 2 characters. + * @property-read string $week W - ISO-8601 week number of year, weeks starting on Monday. + * @property-read string $year Y - A full numeric representation of a year, 4 digits. + * + * @since 1.7.0 + */ +class Date extends \DateTime +{ + const DAY_ABBR = "\x021\x03"; + const DAY_NAME = "\x022\x03"; + const MONTH_ABBR = "\x023\x03"; + const MONTH_NAME = "\x024\x03"; + + /** + * The format string to be applied when using the __toString() magic method. + * + * @var string + * @since 1.7.0 + */ + public static $format = 'Y-m-d H:i:s'; + + /** + * Placeholder for a \DateTimeZone object with GMT as the time zone. + * + * @var object + * @since 1.7.0 + * + * @deprecated 5.0 Without replacement + */ + protected static $gmt; + + /** + * Placeholder for a \DateTimeZone object with the default server + * time zone as the time zone. + * + * @var object + * @since 1.7.0 + * + * @deprecated 5.0 Without replacement + */ + protected static $stz; + + /** + * The \DateTimeZone object for usage in rending dates as strings. + * + * @var \DateTimeZone + * @since 3.0.0 + */ + protected $tz; + + /** + * Constructor. + * + * @param string $date String in a format accepted by strtotime(), defaults to "now". + * @param mixed $tz Time zone to be used for the date. Might be a string or a DateTimeZone object. + * + * @since 1.7.0 + */ + public function __construct($date = 'now', $tz = null) + { + // Create the base GMT and server time zone objects. + if (empty(self::$gmt) || empty(self::$stz)) + { + // @TODO: This code block stays here only for B/C, can be removed in 5.0 + self::$gmt = new \DateTimeZone('GMT'); + self::$stz = new \DateTimeZone(@date_default_timezone_get()); + } + + // If the time zone object is not set, attempt to build it. + if (!($tz instanceof \DateTimeZone)) + { + if (\is_string($tz)) + { + $tz = new \DateTimeZone($tz); + } + else + { + $tz = new \DateTimeZone('UTC'); + } + } + + // Backup active time zone + $activeTZ = date_default_timezone_get(); + + // Force UTC timezone for correct time handling + date_default_timezone_set('UTC'); + + // If the date is numeric assume a unix timestamp and convert it. + $date = is_numeric($date) ? date('c', $date) : $date; + + // Call the DateTime constructor. + parent::__construct($date, $tz); + + // Restore previously active timezone + date_default_timezone_set($activeTZ); + + // Set the timezone object for access later. + $this->tz = $tz; + } + + /** + * Magic method to access properties of the date given by class to the format method. + * + * @param string $name The name of the property. + * + * @return mixed A value if the property name is valid, null otherwise. + * + * @since 1.7.0 + */ + public function __get($name) + { + $value = null; + + switch ($name) + { + case 'daysinmonth': + $value = $this->format('t', true); + break; + + case 'dayofweek': + $value = $this->format('N', true); + break; + + case 'dayofyear': + $value = $this->format('z', true); + break; + + case 'isleapyear': + $value = (boolean) $this->format('L', true); + break; + + case 'day': + $value = $this->format('d', true); + break; + + case 'hour': + $value = $this->format('H', true); + break; + + case 'minute': + $value = $this->format('i', true); + break; + + case 'second': + $value = $this->format('s', true); + break; + + case 'month': + $value = $this->format('m', true); + break; + + case 'ordinal': + $value = $this->format('S', true); + break; + + case 'week': + $value = $this->format('W', true); + break; + + case 'year': + $value = $this->format('Y', true); + break; + + default: + $trace = debug_backtrace(); + trigger_error( + 'Undefined property via __get(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], + E_USER_NOTICE + ); + } + + return $value; + } + + /** + * Magic method to render the date object in the format specified in the public + * static member Date::$format. + * + * @return string The date as a formatted string. + * + * @since 1.7.0 + */ + public function __toString() + { + return (string) parent::format(self::$format); + } + + /** + * Proxy for new Date(). + * + * @param string $date String in a format accepted by strtotime(), defaults to "now". + * @param mixed $tz Time zone to be used for the date. + * + * @return Date + * + * @since 1.7.3 + */ + public static function getInstance($date = 'now', $tz = null) + { + return new static($date, $tz); + } + + /** + * Translates day of week number to a string. + * + * @param integer $day The numeric day of the week. + * @param boolean $abbr Return the abbreviated day string? + * + * @return string The day of the week. + * + * @since 1.7.0 + */ + public function dayToString($day, $abbr = false) + { + switch ($day) + { + case 0: + return $abbr ? 'Sun' : 'Sunday'; + case 1: + return $abbr ? 'Mon' : 'Monday'; + case 2: + return $abbr ? 'Tue' : 'Tuesday'; + case 3: + return $abbr ? 'Wed' : 'Wednesday'; + case 4: + return $abbr ? 'Thu' : 'Thursday'; + case 5: + return $abbr ? 'Fri' : 'Friday'; + case 6: + return $abbr ? 'Sat' : 'Saturday'; + } + } + + /** + * Gets the date as a formatted string in a local calendar. + * + * @param string $format The date format specification string (see {@link PHP_MANUAL#date}) + * @param boolean $local True to return the date string in the local time zone, false to return it in GMT. + * @param boolean $translate True to translate localised strings + * + * @return string The date string in the specified format format. + * + * @since 1.7.0 + */ + public function calendar($format, $local = false, $translate = true) + { + return $this->format($format, $local, $translate); + } + + /** + * Gets the date as a formatted string. + * + * @param string $format The date format specification string (see {@link PHP_MANUAL#date}) + * @param boolean $local True to return the date string in the local time zone, false to return it in GMT. + * @param boolean $translate True to translate localised strings + * + * @return string The date string in the specified format format. + * + * @since 1.7.0 + */ + #[\ReturnTypeWillChange] + public function format($format, $local = false, $translate = true) + { + if ($translate) + { + // Do string replacements for date format options that can be translated. + $format = preg_replace('/(^|[^\\\])D/', "\\1" . self::DAY_ABBR, $format); + $format = preg_replace('/(^|[^\\\])l/', "\\1" . self::DAY_NAME, $format); + $format = preg_replace('/(^|[^\\\])M/', "\\1" . self::MONTH_ABBR, $format); + $format = preg_replace('/(^|[^\\\])F/', "\\1" . self::MONTH_NAME, $format); + } + + // If the returned time should not be local use UTC. + if ($local == false) + { + parent::setTimezone(new \DateTimeZone('UTC')); + } + + // Format the date. + $return = parent::format($format); + + if ($translate) + { + // Manually modify the month and day strings in the formatted time. + if (strpos($return, self::DAY_ABBR) !== false) + { + $return = str_replace(self::DAY_ABBR, $this->dayToString(parent::format('w'), true), $return); + } + + if (strpos($return, self::DAY_NAME) !== false) + { + $return = str_replace(self::DAY_NAME, $this->dayToString(parent::format('w')), $return); + } + + if (strpos($return, self::MONTH_ABBR) !== false) + { + $return = str_replace(self::MONTH_ABBR, $this->monthToString(parent::format('n'), true), $return); + } + + if (strpos($return, self::MONTH_NAME) !== false) + { + $return = str_replace(self::MONTH_NAME, $this->monthToString(parent::format('n')), $return); + } + } + + if ($local == false && $this->tz !== null) + { + parent::setTimezone($this->tz); + } + + return $return; + } + + /** + * Get the time offset from GMT in hours or seconds. + * + * @param boolean $hours True to return the value in hours. + * + * @return float The time offset from GMT either in hours or in seconds. + * + * @since 1.7.0 + */ + public function getOffsetFromGmt($hours = false) + { + return (float) $hours ? ($this->tz->getOffset($this) / 3600) : $this->tz->getOffset($this); + } + + /** + * Translates month number to a string. + * + * @param integer $month The numeric month of the year. + * @param boolean $abbr If true, return the abbreviated month string + * + * @return string The month of the year. + * + * @since 1.7.0 + */ + public function monthToString($month, $abbr = false) + { + switch ($month) + { + case 1: + return $abbr ? 'Jan' : 'January'; // + case 2: + return $abbr ? 'Feb' : 'February'; + case 3: + return $abbr ? 'Mar' : 'March'; + case 4: + return $abbr ? 'Apr' : 'April'; + case 5: + return 'May'; + case 6: + return $abbr ? 'Jun' : 'June'; + case 7: + return $abbr ? 'Jul' : 'July'; + case 8: + return $abbr ? 'Aug' : 'August'; + case 9: + return $abbr ? 'Sep' : 'September'; + case 10: + return $abbr ? 'Oct' : 'October'; + case 11: + return $abbr ? 'Nov' : 'November'; + case 12: + return $abbr ? 'Dec' : 'December'; + } + } + + /** + * Method to wrap the setTimezone() function and set the internal time zone object. + * + * @param \DateTimeZone $tz The new \DateTimeZone object. + * + * @return Date + * + * @since 1.7.0 + * @note This method can't be type hinted due to a PHP bug: https://bugs.php.net/bug.php?id=61483 + */ + #[\ReturnTypeWillChange] + public function setTimezone($tz) + { + $this->tz = $tz; + + return parent::setTimezone($tz); + } + + /** + * Gets the date as an ISO 8601 string. IETF RFC 3339 defines the ISO 8601 format + * and it can be found at the IETF Web site. + * + * @param boolean $local True to return the date string in the local time zone, false to return it in GMT. + * + * @return string The date string in ISO 8601 format. + * + * @link http://www.ietf.org/rfc/rfc3339.txt + * @since 1.7.0 + */ + public function toISO8601($local = false) + { + return $this->format(\DateTimeInterface::RFC3339, $local, false); + } + + /** + * Gets the date as an SQL datetime string. + * + * @param boolean $local True to return the date string in the local time zone, false to return it in GMT. + * @param DatabaseInterface $db The database driver or null to use Factory::getDbo() + * + * @return string The date string in SQL datetime format. + * + * @throws \Exception + * @since 2.5.0 + * @link http://dev.mysql.com/doc/refman/5.0/en/datetime.html + */ + public function toSql($local = false, DatabaseInterface $db = null) + { + if ($db === null) + { + /** @var \Joomla\Database\DatabaseInterface $db */ + $db = Factory::getContainer()->get(DatabaseInterface::class); + } + + return $this->format($db->getDateFormat(), $local, false); + } + + /** + * Gets the date as an RFC 822 string. IETF RFC 2822 supercedes RFC 822 and its definition + * can be found at the IETF Web site. + * + * @param boolean $local True to return the date string in the local time zone, false to return it in GMT. + * + * @return string The date string in RFC 822 format. + * + * @link http://www.ietf.org/rfc/rfc2822.txt + * @since 1.7.0 + */ + public function toRFC822($local = false) + { + return $this->format(\DateTimeInterface::RFC2822, $local, false); + } + + /** + * Gets the date as UNIX time stamp. + * + * @return integer The date as a UNIX timestamp. + * + * @since 1.7.0 + */ + public function toUnix() + { + return (int) parent::format('U'); + } +} diff --git a/libraries/src/EventListener/ErrorSubscriber.php b/libraries/src/EventListener/ErrorSubscriber.php new file mode 100644 index 0000000..3afabaf --- /dev/null +++ b/libraries/src/EventListener/ErrorSubscriber.php @@ -0,0 +1,195 @@ +renderer = $renderer; + } + + /** + * Returns an array of events this subscriber will listen to. + * + * @return array + */ + public static function getSubscribedEvents(): array + { + return [ + ApplicationEvents::ERROR => 'handleWebError', + ConsoleEvents::APPLICATION_ERROR => 'handleConsoleError', + ]; + } + + /** + * Handle console application errors. + * + * @param ConsoleApplicationErrorEvent $event Event object + * + * @return void + */ + public function handleConsoleError(ConsoleApplicationErrorEvent $event): void + { + $this->logError($event->getError()); + } + + /** + * Handle web application errors. + * + * @param ApplicationErrorEvent $event Event object + * + * @return void + */ + public function handleWebError(ApplicationErrorEvent $event): void + { + /** @var SiteApplication $app */ + $app = $event->getApplication(); + + switch (true) + { + case $event->getError() instanceof MethodNotAllowedException : + // Log the error for reference + $this->logger->error( + sprintf('Route `%s` not supported by method `%s`', $app->get('uri.route'), $app->input->getMethod()), + ['exception' => $event->getError()] + ); + + $this->prepareResponse($event); + + $app->setHeader('Allow', implode(', ', $event->getError()->getAllowedMethods())); + + break; + + case $event->getError() instanceof RouteNotFoundException : + // Log the error for reference + $this->logger->error( + sprintf('Route `%s` not found', $app->get('uri.route')), + ['exception' => $event->getError()] + ); + + $this->prepareResponse($event); + + break; + + default: + $this->logError($event->getError()); + + $this->prepareResponse($event); + + break; + } + } + + /** + * Log the error. + * + * @param \Throwable $throwable The error being processed + * + * @return void + */ + private function logError(\Throwable $throwable): void + { + $this->logger->error( + sprintf('Uncaught Throwable of type %s caught.', \get_class($throwable)), + ['exception' => $throwable] + ); + } + + /** + * Prepare the response for the event + * + * @param ApplicationErrorEvent $event Event object + * + * @return void + */ + private function prepareResponse(ApplicationErrorEvent $event): void + { + /** @var SiteApplication $app */ + $app = $event->getApplication(); + + $app->allowCache(false); + + switch (true) + { + case $app->input->getString('_format', 'html') === 'json' : + case $app->mimeType === 'application/json' : + case $app->getResponse() instanceof JsonResponse : + $data = [ + 'code' => $event->getError()->getCode(), + 'message' => $event->getError()->getMessage(), + 'error' => true, + ]; + + $response = new JsonResponse($data); + + break; + + default : + $response = new HtmlResponse( + $this->renderer->render('exception.twig', ['exception' => $event->getError()]) + ); + + break; + } + + switch ($event->getError()->getCode()) + { + case 404 : + $response = $response->withStatus(404); + + break; + + case 405 : + $response = $response->withStatus(405); + + break; + + case 500 : + default : + $response = $response->withStatus(500); + + break; + } + + $app->setResponse($response); + } +} diff --git a/libraries/src/Factory.php b/libraries/src/Factory.php new file mode 100644 index 0000000..4266501 --- /dev/null +++ b/libraries/src/Factory.php @@ -0,0 +1,301 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS; + +\defined('LPATH_PLATFORM') or die; + +use Joomla\Application\WebApplicationInterface; +use Joomla\Database\Service\DatabaseProvider; +use Joomla\DI\Container; +use Joomla\Registry\Registry; +use PHPMailer\PHPMailer\Exception as phpmailerException; + +/** + * Kumwe Platform Factory class. + * + * @since 1.0.0 + * + * SOURCE: https://github.com/joomla/joomla-cms/blob/4.1-dev/libraries/src/Factory.php#L39 + */ +abstract class Factory +{ + /** + * Global application object + * + * @var WebApplicationInterface + * @since 1.0.0 + */ + public static $application = null; + + /** + * Global configuration object + * + * @var \LConfig + * @since 1.0.0 + */ + public static $config = null; + + /** + * Global container object + * + * @var Container + * @since 1.0.0 + */ + public static $container = null; + + /** + * Global mailer object + * + * @var Mail + * @since 1.0.0 + */ + public static $mailer = null; + + /** + * Get the global application object. When the global application doesn't exist, an exception is thrown. + * + * @return WebApplicationInterface object + * + * @since 1.0.0 + * @throws \Exception + */ + public static function getApplication() : WebApplicationInterface + { + if (!self::$application) + { + throw new \Exception('Failed to start application', 500); + } + + return self::$application; + } + + /** + * Get a container object + * + * Returns the global service container object, only creating it if it doesn't already exist. + * + * This method is only suggested for use in code whose responsibility is to create new services + * and needs to be able to resolve the dependencies, and should therefore only be used when the + * container is not accessible by other means. Valid uses of this method include: + * + * - A static `getInstance()` method calling a factory service from the container, + * see `Joomla\CMS\Toolbar\Toolbar::getInstance()` as an example + * - An application front controller loading and executing the Joomla application class, + * see the `cli/joomla.php` file as an example + * - Retrieving optional constructor dependencies when not injected into a class during a transitional + * period to retain backward compatibility, in this case a deprecation notice should also be emitted to + * notify developers of changes needed in their code + * + * This method is not suggested for use as a one-for-one replacement of static calls, such as + * replacing calls to `Factory::getDbo()` with calls to `Factory::getContainer()->get('db')`, code + * should be refactored to support dependency injection instead of making this change. + * + * @return Container + * + * @since 4.0.0 + */ + public static function getContainer(): Container + { + if (!self::$container) + { + self::$container = self::createContainer(); + } + + return self::$container; + } + + /** + * Get a mailer object. + * + * Returns the global {@link Mail} object, only creating it if it doesn't already exist. + * + * @return Mail object + * + * @see Mail + * @since 1.7.0 + */ + public static function getMailer() + { + if (!self::$mailer) + { + self::$mailer = self::createMailer(); + } + + $copy = clone self::$mailer; + + return $copy; + } + + /** + * Create a container object + * + * @return Container + * + * @since 4.0.0 + */ + protected static function createContainer(): Container + { + return (new Container) + ->registerServiceProvider(new Service\ConfigurationProvider(LPATH_CONFIGURATION . '/config.php')) + ->registerServiceProvider(new Service\SessionProvider) + ->registerServiceProvider(new Service\UserProvider) + ->registerServiceProvider(new Service\InputProvider) + ->registerServiceProvider(new DatabaseProvider) + ->registerServiceProvider(new Service\EventProvider) + ->registerServiceProvider(new Service\HttpProvider) + ->registerServiceProvider(new Service\LoggingProvider); + } + + /** + * Get a configuration object + * + * Returns the global {@link \JConfig} object, only creating it if it doesn't already exist. + * + * @param string $file The path to the configuration file + * @param string $type The type of the configuration file + * @param string $namespace The namespace of the configuration file + * + * @return Registry + * + * @see Registry + * @since 1.1.1 + */ + public static function getConfig($file = null, $type = 'PHP', $namespace = '') + { + /** + * If there is an application object, fetch the configuration from there. + * Check it's not null because LanguagesModel can make it null and if it's null + * we would want to re-init it from configuration.php. + */ + if (self::$application && self::$application->getConfig() !== null) + { + return self::$application->getConfig(); + } + + if (!self::$config) + { + if ($file === null) + { + $file = JPATH_CONFIGURATION . '/config.php'; + } + + self::$config = self::createConfig($file, $type, $namespace); + } + + return self::$config; + } + + /** + * Create a configuration object + * + * @param string $file The path to the configuration file. + * @param string $type The type of the configuration file. + * @param string $namespace The namespace of the configuration file. + * + * @return Registry + * + * @see Registry + * @since 1.0.0 + */ + protected static function createConfig($file, $type = 'PHP', $namespace = '') + { + if (is_file($file)) + { + include_once $file; + } + + // Create the registry with a default namespace of config + $registry = new Registry; + + // Sanitize the namespace. + $namespace = ucfirst((string) preg_replace('/[^A-Z_]/i', '', $namespace)); + + // Build the config name. + $name = 'LConfig' . $namespace; + + // Handle the PHP configuration type. + if ($type === 'PHP' && class_exists($name)) + { + // Create the LConfig object + $config = new $name; + + // Load the configuration values into the registry + $registry->loadObject($config); + } + + return $registry; + } + + /** + * Create a mailer object + * + * @return Mail object + * + * @see Mail + * @since 1.0.0 + */ + protected static function createMailer() + { +// $conf = self::getConfig(); +// +// $smtpauth = ($conf->get('smtpauth') == 0) ? null : 1; +// $smtpuser = $conf->get('smtpuser'); +// $smtppass = $conf->get('smtppass'); +// $smtphost = $conf->get('smtphost'); +// $smtpsecure = $conf->get('smtpsecure'); +// $smtpport = $conf->get('smtpport'); +// $mailfrom = $conf->get('mailfrom'); +// $fromname = $conf->get('fromname'); +// $mailer = $conf->get('mailer'); +// +// // Create a Mail object +// $mail = Mail::getInstance(); +// +// // Clean the email address +// $mailfrom = MailHelper::cleanLine($mailfrom); +// +// // Set default sender without Reply-to if the mailfrom is a valid address +// if (MailHelper::isEmailAddress($mailfrom)) +// { +// // Wrap in try/catch to catch phpmailerExceptions if it is throwing them +// try +// { +// // Check for a false return value if exception throwing is disabled +// if ($mail->setFrom($mailfrom, MailHelper::cleanLine($fromname), false) === false) +// { +// Log::add(__METHOD__ . '() could not set the sender data.', Log::WARNING, 'mail'); +// } +// } +// catch (phpmailerException $e) +// { +// Log::add(__METHOD__ . '() could not set the sender data.', Log::WARNING, 'mail'); +// } +// } +// +// // Default mailer is to use PHP's mail function +// switch ($mailer) +// { +// case 'smtp': +// $mail->useSmtp($smtpauth, $smtphost, $smtpuser, $smtppass, $smtpsecure, $smtpport); +// break; +// +// case 'sendmail': +// $mail->isSendmail(); +// break; +// +// default: +// $mail->isMail(); +// break; +// } +// +// return $mail; + } +} diff --git a/libraries/src/Filter/InputFilter.php b/libraries/src/Filter/InputFilter.php new file mode 100644 index 0000000..d57236e --- /dev/null +++ b/libraries/src/Filter/InputFilter.php @@ -0,0 +1,527 @@ + + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Filter; + +use Kumwe\CMS\String\PunycodeHelper; +use Joomla\Filter\InputFilter as BaseInputFilter; + +/** + * InputFilter is a class for filtering input from any data source + * + * Forked from the php input filter library by: Daniel Morris + * Original Contributors: Gianpaolo Racca, Ghislain Picard, Marco Wandschneider, Chris Tobin and Andrew Eddie. + * + * @since 1.7.0 + */ +class InputFilter extends BaseInputFilter +{ + /** + * An array containing a list of extensions for files that are typically + * executable directly in the webserver context, potentially resulting in code executions + * + * @since 4.0.0 + */ + public const FORBIDDEN_FILE_EXTENSIONS = [ + 'php', 'phps', 'pht', 'phtml', 'php3', 'php4', 'php5', 'php6', 'php7', 'asp', + 'php8', 'phar', 'inc', 'pl', 'cgi', 'fcgi', 'java', 'jar', 'py', 'aspx' + ]; + + /** + * A flag for Unicode Supplementary Characters (4-byte Unicode character) stripping. + * + * @var integer + * @since 3.5 + */ + private $stripUSC = 0; + + /** + * A container for InputFilter instances. + * + * @var InputFilter[] + * @since 4.0.0 + */ + protected static $instances = array(); + /** + * Constructor for inputFilter class. Only first parameter is required. + * + * @param array $tagsArray List of user-defined tags + * @param array $attrArray List of user-defined attributes + * @param integer $tagsMethod The constant static::ONLY_ALLOW_DEFINED_TAGS or static::BLOCK_DEFINED_TAGS + * @param integer $attrMethod The constant static::ONLY_ALLOW_DEFINED_ATTRIBUTES or static::BLOCK_DEFINED_ATTRIBUTES + * @param integer $xssAuto Only auto clean essentials = 0, Allow clean blocked tags/attributes = 1 + * @param integer $stripUSC Strip 4-byte unicode characters = 1, no strip = 0 + * + * @since 1.7.0 + */ + public function __construct($tagsArray = array(), $attrArray = array(), $tagsMethod = 0, $attrMethod = 0, $xssAuto = 1, $stripUSC = 0) + { + parent::__construct($tagsArray, $attrArray, $tagsMethod, $attrMethod, $xssAuto); + + // Assign member variables + $this->stripUSC = $stripUSC; + } + + /** + * Returns an input filter object, only creating it if it doesn't already exist. + * + * @param array $tagsArray List of user-defined tags + * @param array $attrArray List of user-defined attributes + * @param integer $tagsMethod The constant static::ONLY_ALLOW_DEFINED_TAGS or static::BLOCK_DEFINED_TAGS + * @param integer $attrMethod The constant static::ONLY_ALLOW_DEFINED_ATTRIBUTES or static::BLOCK_DEFINED_ATTRIBUTES + * @param integer $xssAuto Only auto clean essentials = 0, Allow clean blocked tags/attributes = 1 + * @param integer $stripUSC Strip 4-byte unicode characters = 1, no strip = 0 + * + * @return InputFilter The InputFilter object. + * + * @since 1.7.0 + */ + public static function getInstance($tagsArray = array(), $attrArray = array(), $tagsMethod = 0, $attrMethod = 0, $xssAuto = 1, $stripUSC = 0) + { + $sig = md5(serialize(array($tagsArray, $attrArray, $tagsMethod, $attrMethod, $xssAuto))); + + if (empty(self::$instances[$sig])) + { + self::$instances[$sig] = new InputFilter($tagsArray, $attrArray, $tagsMethod, $attrMethod, $xssAuto, $stripUSC); + } + + return self::$instances[$sig]; + } + + /** + * Method to be called by another php script. Processes for XSS and + * specified bad code. + * + * @param mixed $source Input string/array-of-string to be 'cleaned' + * @param string $type The return type for the variable: + * INT: An integer, or an array of integers, + * UINT: An unsigned integer, or an array of unsigned integers, + * FLOAT: A floating point number, or an array of floating point numbers, + * BOOLEAN: A boolean value, + * WORD: A string containing A-Z or underscores only (not case sensitive), + * ALNUM: A string containing A-Z or 0-9 only (not case sensitive), + * CMD: A string containing A-Z, 0-9, underscores, periods or hyphens (not case sensitive), + * BASE64: A string containing A-Z, 0-9, forward slashes, plus or equals (not case sensitive), + * STRING: A fully decoded and sanitised string (default), + * HTML: A sanitised string, + * ARRAY: An array, + * PATH: A sanitised file path, or an array of sanitised file paths, + * TRIM: A string trimmed from normal, non-breaking and multibyte spaces + * USERNAME: Do not use (use an application specific filter), + * RAW: The raw string is returned with no filtering, + * unknown: An unknown filter will act like STRING. If the input is an array it will return an + * array of fully decoded and sanitised strings. + * + * @return mixed 'Cleaned' version of input parameter + * + * @since 1.7.0 + */ + public function clean($source, $type = 'string') + { + // Strip Unicode Supplementary Characters when requested to do so + if ($this->stripUSC) + { + // Alternatively: preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xE2\xAF\x91", $source) but it'd be slower. + $source = $this->stripUSC($source); + } + + return parent::clean($source, $type); + } + + /** + * Function to punyencode utf8 mail when saving content + * + * @param string $text The strings to encode + * + * @return string The punyencoded mail + * + * @since 3.5 + */ + public function emailToPunycode($text) + { + $pattern = '/(("mailto:)+[\w\.\-\+]+\@[^"?]+\.+[^."?]+("|\?))/'; + + if (preg_match_all($pattern, $text, $matches)) + { + foreach ($matches[0] as $match) + { + $match = (string) str_replace(array('?', '"'), '', $match); + $text = (string) str_replace($match, PunycodeHelper::emailToPunycode($match), $text); + } + } + + return $text; + } + + /** + * Checks an uploaded for suspicious naming and potential PHP contents which could indicate a hacking attempt. + * + * The options you can define are: + * null_byte Prevent files with a null byte in their name (buffer overflow attack) + * forbidden_extensions Do not allow these strings anywhere in the file's extension + * php_tag_in_content Do not allow ` true, + + // Forbidden string in extension (e.g. php matched .php, .xxx.php, .php.xxx and so on) + 'forbidden_extensions' => self::FORBIDDEN_FILE_EXTENSIONS, + + // true, + + // true, + + // __HALT_COMPILER() + 'phar_stub_in_content' => true, + + // Which file extensions to scan for short tags + 'shorttag_extensions' => array( + 'inc', 'phps', 'class', 'php3', 'php4', 'php5', 'php6', 'php7', 'php8', 'txt', 'dat', 'tpl', 'tmpl', + ), + + // Forbidden extensions anywhere in the content + 'fobidden_ext_in_content' => true, + + // Which file extensions to scan for .php in the content + 'php_ext_content_extensions' => array('zip', 'rar', 'tar', 'gz', 'tgz', 'bz2', 'tbz', 'jpa'), + ); + + $options = array_merge($defaultOptions, $options); + + // Make sure we can scan nested file descriptors + $descriptors = $file; + + if (isset($file['name']) && isset($file['tmp_name'])) + { + $descriptors = static::decodeFileData( + array( + $file['name'], + $file['type'], + $file['tmp_name'], + $file['error'], + $file['size'], + ) + ); + } + + // Handle non-nested descriptors (single files) + if (isset($descriptors['name'])) + { + $descriptors = array($descriptors); + } + + // Scan all descriptors detected + foreach ($descriptors as $fileDescriptor) + { + if (!isset($fileDescriptor['name'])) + { + // This is a nested descriptor. We have to recurse. + if (!static::isSafeFile($fileDescriptor, $options)) + { + return false; + } + + continue; + } + + $tempNames = $fileDescriptor['tmp_name']; + $intendedNames = $fileDescriptor['name']; + + if (!\is_array($tempNames)) + { + $tempNames = array($tempNames); + } + + if (!\is_array($intendedNames)) + { + $intendedNames = array($intendedNames); + } + + $len = \count($tempNames); + + for ($i = 0; $i < $len; $i++) + { + $tempName = array_shift($tempNames); + $intendedName = array_shift($intendedNames); + + // 1. Null byte check + if ($options['null_byte']) + { + if (strstr($intendedName, "\x00")) + { + return false; + } + } + + // 2. PHP-in-extension check (.php, .php.xxx[.yyy[.zzz[...]]], .xxx[.yyy[.zzz[...]]].php) + if (!empty($options['forbidden_extensions'])) + { + $explodedName = explode('.', $intendedName); + $explodedName = array_reverse($explodedName); + array_pop($explodedName); + $explodedName = array_map('strtolower', $explodedName); + + /* + * DO NOT USE array_intersect HERE! array_intersect expects the two arrays to + * be set, i.e. they should have unique values. + */ + foreach ($options['forbidden_extensions'] as $ext) + { + if (\in_array($ext, $explodedName)) + { + return false; + } + } + } + + // 3. File contents scanner (PHP tag in file contents) + if ($options['php_tag_in_content'] + || $options['shorttag_in_content'] || $options['phar_stub_in_content'] + || ($options['fobidden_ext_in_content'] && !empty($options['forbidden_extensions']))) + { + $fp = strlen($tempName) ? @fopen($tempName, 'r') : false; + + if ($fp !== false) + { + $data = ''; + + while (!feof($fp)) + { + $data .= @fread($fp, 131072); + + if ($options['php_tag_in_content'] && stripos($data, ' $v) + { + $result[$k] = static::decodeFileData(array($data[0][$k], $data[1][$k], $data[2][$k], $data[3][$k], $data[4][$k])); + } + + return $result; + } + + return array('name' => $data[0], 'type' => $data[1], 'tmp_name' => $data[2], 'error' => $data[3], 'size' => $data[4]); + } + + /** + * Try to convert to plaintext + * + * @param string $source The source string. + * + * @return string Plaintext string + * + * @since 3.5 + */ + protected function decode($source) + { + static $ttr; + + if (!\is_array($ttr)) + { + // Entity decode + $trans_tbl = get_html_translation_table(HTML_ENTITIES, ENT_COMPAT, 'ISO-8859-1'); + + foreach ($trans_tbl as $k => $v) + { + $ttr[$v] = utf8_encode($k); + } + } + + $source = strtr($source, $ttr); + + // Convert decimal + $source = preg_replace_callback( + '/&#(\d+);/m', + function ($m) { + return utf8_encode(\chr($m[1])); + }, + $source + ); + + // Convert hex + $source = preg_replace_callback( + '/&#x([a-f0-9]+);/mi', + function ($m) { + return utf8_encode(\chr('0x' . $m[1])); + }, + $source + ); + + return $source; + } + + /** + * Recursively strip Unicode Supplementary Characters from the source. Not: objects cannot be filtered. + * + * @param mixed $source The data to filter + * + * @return mixed The filtered result + * + * @since 3.5 + */ + protected function stripUSC($source) + { + if (\is_object($source)) + { + return $source; + } + + if (\is_array($source)) + { + $filteredArray = array(); + + foreach ($source as $k => $v) + { + $filteredArray[$k] = $this->stripUSC($v); + } + + return $filteredArray; + } + + return preg_replace('/[\xF0-\xF7].../s', "\xE2\xAF\x91", $source); + } +} diff --git a/libraries/src/Model/DashboardModel.php b/libraries/src/Model/DashboardModel.php new file mode 100644 index 0000000..74c4a4b --- /dev/null +++ b/libraries/src/Model/DashboardModel.php @@ -0,0 +1,47 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Model; + +use Joomla\Database\DatabaseDriver; +use Joomla\Model\DatabaseModelInterface; +use Joomla\Model\DatabaseModelTrait; + +/** + * Model class + * source: https://github.com/joomla/framework.joomla.org/blob/master/src/Model/PackageModel.php + */ +class DashboardModel implements DatabaseModelInterface +{ + use DatabaseModelTrait; + + /** + * Instantiate the model. + * + * @param DatabaseDriver $db The database adapter. + */ + public function __construct(DatabaseDriver $db) + { + $this->setDb($db); + } + + /** + * Get an active dashboard template name + * + * @param string $dashboardName The dashboard to lookup + * + * @return string + * + */ + public function getDashboard(string $dashboardName): string + { + return 'dashboard.twig'; // only one at this time + } +} diff --git a/libraries/src/Model/ItemModel.php b/libraries/src/Model/ItemModel.php new file mode 100644 index 0000000..abb9bf3 --- /dev/null +++ b/libraries/src/Model/ItemModel.php @@ -0,0 +1,287 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Model; + +use Joomla\Database\DatabaseDriver; +use Joomla\Database\ParameterType; +use Joomla\Model\DatabaseModelInterface; +use Joomla\Model\DatabaseModelTrait; +use Kumwe\CMS\Date\Date; + +/** + * Model class + */ +class ItemModel implements DatabaseModelInterface +{ + use DatabaseModelTrait; + + /** + * @var array + */ + public $tempItem; + + /** + * Instantiate the model. + * + * @param DatabaseDriver $db The database adapter. + */ + public function __construct(DatabaseDriver $db) + { + $this->setDb($db); + } + + /** + * Add an item + * + * @param int $id + * @param string $title + * @param string $introtext + * @param string $fulltext + * @param int $state + * @param string $created + * @param int $createdBy + * @param string $createdByAlias + * @param string $modified + * @param int $modifiedBy + * @param string $publishUp + * @param string $publishDown + * @param string $metakey + * @param string $metadesc + * @param string $metadata + * @param int $featured + * + * @return int + * @throws \Exception + */ + public function setItem( + int $id, + string $title, + string $fulltext, + int $state, + string $created, + int $createdBy, + string $createdByAlias, + string $modified, + int $modifiedBy, + string $publishUp, + string $publishDown, + string $metakey, + string $metadesc, + string $metadata, + int $featured): int + { + $db = $this->getDb(); + + // extract the intro text + $introtext = ''; + if (strpos($fulltext, '

intro-text

')) + { + $bucket = explode('

intro-text

', $fulltext); + $introtext = array_shift($bucket); + $fulltext = implode('', $bucket); + } + + $data = [ + 'title' => (string) $title, + 'introtext' => (string) $introtext, + 'fulltext' => (string) $fulltext, + 'state' => (int) $state, + 'created' => (string) $created, + 'created_by' => (int) $createdBy, + 'created_by_alias' => (string) $createdByAlias, + 'modified' => (string) $modified, + 'modified_by' => (int) $modifiedBy, + 'publish_up' => (string) (empty($publishUp)) ? '0000-00-00 00:00:00' : (new Date($publishUp))->toSql(), + 'publish_down' => (string) (empty($publishDown)) ? '0000-00-00 00:00:00' : (new Date($publishDown))->toSql(), + 'metakey' => (string) $metakey, + 'metadesc' => (string) $metadesc, + 'metadata' => (string) $metadata, + 'featured' => (int) $featured + ]; + + // if we have ID update + if ($id > 0) + { + $data['id'] = (int) $id; + // remove what can not now be set + unset($data['created']); + unset($data['created_by']); + // change to object + $data = (object) $data; + + try + { + $db->updateObject('#__item', $data, 'id'); + } + catch (\RuntimeException $exception) + { + throw new \RuntimeException($exception->getMessage(), 404); + } + + return $id; + + } + else + { + // remove what can not now be set + $data['modified'] = '0000-00-00 00:00:00'; + $data['modified_by'] = 0; + // we don't have any params for now + $data['params'] = ''; + // change to object + $data = (object) $data; + + try + { + $db->insertObject('#__item', $data); + } + catch (\RuntimeException $exception) + { + throw new \RuntimeException($exception->getMessage(), 404); + } + + return $db->insertid(); + } + } + + /** + * Get an item + * + * @param int|null $id + * + * @return \stdClass + * @throws \Exception + */ + public function getItem(?int $id): \stdClass + { + $db = $this->getDb(); + // default object (use posted values if set) + if (is_array($this->tempItem)) + { + $default = (object) $this->tempItem; + } + else + { + $default = new \stdClass(); + } + // to be sure ;) + $default->today_date = (new Date())->toSql(); + $default->post_key = "?task=create"; + $default->state = 1; + + // we return the default if id not correct + if (!is_numeric($id)) + { + return $default; + } + + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__item')) + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER) + ->setLimit(1); + + try + { + $result = $db->setQuery($query)->loadObject(); + } + catch (\RuntimeException $e) + { + // we ignore this and just return an empty object + } + + if (isset($result) && $result instanceof \stdClass) + { + $result->post_key = "?id=$id&task=edit"; + $result->today_date = $default->today_date; + // check if we have intro text we add it to full text + if (!empty($result->introtext)) + { + $result->fulltext = $result->introtext . '

intro-text

' . $result->fulltext; + } + + return $result; + } + + return $default; + } + + /** + * @param string $name + * + * @return string + */ + public function setLayout(string $name): string + { + return $name . '.twig'; + } + + /** + * @param int $id + * + * @return bool + */ + public function linked(int $id): bool + { + $db = $this->getDb(); + // first check if this item is linked to menu + $query = $db->getQuery(true) + ->select($db->quoteName('title')) + ->from($db->quoteName('#__menu')) + ->where($db->quoteName('item_id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + + try + { + $menu = $db->setQuery($query)->loadResult(); + } + catch (\RuntimeException $e) + { + // not linked... or something + return false; + } + + if ($menu) + { + return true; + } + + return false; + } + + /** + * @param int $id + * + * @return bool + */ + public function delete(int $id): bool + { + $db = $this->getDb(); + + // Purge the session + $query = $db->getQuery(true) + ->delete($db->quoteName('#__item')) + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + try + { + $db->setQuery($query)->execute(); + } + catch (\RuntimeException $e) + { + // delete failed + return false; + } + + return true; + } +} diff --git a/libraries/src/Model/ItemsModel.php b/libraries/src/Model/ItemsModel.php new file mode 100644 index 0000000..77e2d0e --- /dev/null +++ b/libraries/src/Model/ItemsModel.php @@ -0,0 +1,54 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Model; + +use Joomla\Database\DatabaseDriver; +use Joomla\Model\DatabaseModelInterface; +use Joomla\Model\DatabaseModelTrait; + +/** + * Model class + */ +class ItemsModel implements DatabaseModelInterface +{ + use DatabaseModelTrait; + + /** + * Instantiate the model. + * + * @param DatabaseDriver $db The database adapter. + */ + public function __construct(DatabaseDriver $db) + { + $this->setDb($db); + } + + /** + * Get all items + * + * @return array + */ + public function getItems(): array + { + $db = $this->getDb(); + + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__item')); + + return $db->setQuery($query)->loadObjectList('id'); + } + + public function setLayout(string $name): string + { + return $name . '.twig'; + } +} diff --git a/libraries/src/Model/MenuModel.php b/libraries/src/Model/MenuModel.php new file mode 100644 index 0000000..02c12e2 --- /dev/null +++ b/libraries/src/Model/MenuModel.php @@ -0,0 +1,353 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Model; + +use Joomla\Database\DatabaseDriver; +use Joomla\Database\ParameterType; +use Joomla\Model\DatabaseModelInterface; +use Joomla\Model\DatabaseModelTrait; +use Kumwe\CMS\Date\Date; +use Kumwe\CMS\Model\Util\MenuInterface; +use Kumwe\CMS\Model\Util\SelectMenuTrait; +use Kumwe\CMS\Model\Util\UniqueInterface; +use Kumwe\CMS\Model\Util\UniqueMenuAliasTrait; + +/** + * Model class + */ +class MenuModel implements DatabaseModelInterface, UniqueInterface, MenuInterface +{ + use DatabaseModelTrait, UniqueMenuAliasTrait, SelectMenuTrait; + + /** + * Active id + * + * @var int + */ + public $id = 0; + + /** + * @var array + */ + public $tempItem; + + /** + * Instantiate the model. + * + * @param DatabaseDriver $db The database adapter. + */ + public function __construct(DatabaseDriver $db) + { + $this->setDb($db); + } + + /** + * Add an item + * + * @param int $id + * @param string $title + * @param string $alias + * @param int $itemId + * @param string $path + * @param int $published + * @param string $publishUp + * @param string $publishDown + * @param string $position + * @param int $home + * @param int $parent + * + * @return int + * @throws \Exception + */ + public function setItem( + int $id, + string $title, + string $alias, + int $itemId, + string $path, + int $published, + string $publishUp, + string $publishDown, + string $position, + int $home, + int $parent): int + { + $db = $this->getDb(); + + // set the alias if not set + $alias = (empty($alias)) ? $title : $alias; + $alias = $this->unique($id, $alias, $parent); + // set the path + $path = $this->getPath($alias, $parent); + + $data = [ + 'title' => (string) $title, + 'alias' => (string) $alias, + 'path' => (string) $path, + 'item_id' => (int) $itemId, + 'published' => (int) $published, + 'publish_up' => (string) (empty($publishUp)) ? '0000-00-00 00:00:00' : (new Date($publishUp))->toSql(), + 'publish_down' => (string) (empty($publishDown)) ? '0000-00-00 00:00:00' : (new Date($publishDown))->toSql(), + 'home' => (int) $home, + 'parent_id' => (int) $parent + ]; + + // we set position in params + $data['params'] = json_encode(['position' => $position]); + + // if we have ID update + if ($id > 0) + { + // set active ID + $data['id'] = (int) $id; + $this->id = (int) $id; + // change to object + $data = (object) $data; + + try + { + $db->updateObject('#__menu', $data, 'id'); + } + catch (\RuntimeException $exception) + { + throw new \RuntimeException($exception->getMessage(), 404); + } + } + else + { + // change to object + $data = (object) $data; + + try + { + $db->insertObject('#__menu', $data); + } + catch (\RuntimeException $exception) + { + throw new \RuntimeException($exception->getMessage(), 404); + } + + $id = $db->insertid(); + } + + // check if we have another home set + if ($data->home == 1) + { + $this->setHome($id); + } + + return $id; + } + + /** + * Get all published items + * + * @return array + */ + public function getItems(): array + { + $db = $this->getDb(); + + $query = $db->getQuery(true) + ->select($db->quoteName(array('id', 'title'))) + ->from($db->quoteName('#__item')) + ->where('state = 1'); + + return $db->setQuery($query)->loadObjectList('id'); + } + + /** + * Get an item + * + * @param int|null $id + * + * @return \stdClass + * @throws \Exception + */ + public function getItem(?int $id): \stdClass + { + $db = $this->getDb(); + // default object (use posted values if set) + if (is_array($this->tempItem)) + { + $default = (object) $this->tempItem; + } + else + { + $default = new \stdClass(); + } + // to be sure ;) + $default->today_date = (new Date())->toSql(); + $default->post_key = "?task=create"; + $default->published = 1; + $default->home = 0; + + // we return the default if id not correct + if (!is_numeric($id)) + { + return $default; + } + + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__menu')) + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER) + ->setLimit(1); + + try + { + $result = $db->setQuery($query)->loadObject(); + } + catch (\RuntimeException $e) + { + // we ignore this and just return an empty object + } + + if (isset($result) && $result instanceof \stdClass) + { + $result->post_key = "?id=$id&task=edit"; + $result->today_date = $default->today_date; + // set the position + $result->params = json_decode($result->params); + + return $result; + } + + return $default; + } + + /** + * @param string $name + * + * @return string + */ + public function setLayout(string $name): string + { + return $name . '.twig'; + } + + /** + * @param int $id + * + * @return bool + */ + public function delete(int $id): bool + { + $db = $this->getDb(); + // Purge the session + $query = $db->getQuery(true) + ->delete($db->quoteName('#__menu')) + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + try + { + $db->setQuery($query)->execute(); + } + catch (\RuntimeException $e) + { + // delete failed + return false; + } + return true; + } + + /** + * Make sure that we have only one home + * + * @param $id + * + * @return bool + */ + private function setHome($id): bool + { + $db = $this->getDb(); + // Purge the session + $query = $db->getQuery(true) + ->update($db->quoteName('#__menu')) + ->set($db->quoteName('home') . ' = 0') + ->where($db->quoteName('id') . ' != :id') + ->bind(':id', $id, ParameterType::INTEGER); + try + { + $db->setQuery($query)->execute(); + } + catch (\RuntimeException $e) + { + // delete failed + return false; + } + return true; + } + + /** + * get path + * + * @param string $alias + * @param int $parent + * + * @return string + */ + private function getPath(string $alias, int $parent): string + { + // alias bucket + $bucket = []; + $bucket[] = $alias; + $parent = $this->getParent($parent); + // make sure to get all path aliases TODO: we should limit the menu depth to 6 or something + while (isset($parent->alias)) + { + // load the alias + $bucket[] = $parent->alias; + // get the next parent + $parent = $this->getParent($parent->parent_id); + } + // now return the path + return implode('/', array_reverse($bucket)); + } + + /** + * get parent + * + * @param int $id + * + * @return \stdClass + */ + private function getParent(int $id): \stdClass + { + if ($id > 0) + { + $db = $this->getDb(); + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__menu')) + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $id) + ->setLimit(1); + + try + { + $parent = $db->setQuery($query)->loadObject(); + } + catch (\RuntimeException $e) + { + // we ignore this and just return an empty object + } + + // return only if found + if (isset($parent) && $parent instanceof \stdClass) + { + return $parent; + } + } + return new \stdClass(); + } +} diff --git a/libraries/src/Model/MenusModel.php b/libraries/src/Model/MenusModel.php new file mode 100644 index 0000000..5779857 --- /dev/null +++ b/libraries/src/Model/MenusModel.php @@ -0,0 +1,62 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Model; + +use Joomla\Database\DatabaseDriver; +use Joomla\Database\ParameterType; +use Joomla\Model\DatabaseModelInterface; +use Joomla\Model\DatabaseModelTrait; + +/** + * Model class + */ +class MenusModel implements DatabaseModelInterface +{ + use DatabaseModelTrait; + + /** + * Instantiate the model. + * + * @param DatabaseDriver $db The database adapter. + */ + public function __construct(DatabaseDriver $db) + { + $this->setDb($db); + } + + /** + * Get all items + * + * @return array + */ + public function getItems(): array + { + $db = $this->getDb(); + + $query = $db->getQuery(true) + ->select('a.*') + ->select($db->quoteName(array('t.title'), array('item_title'))) + ->from($db->quoteName('#__menu', 'a')) + ->join('INNER', $db->quoteName('#__item', 't'), 'a.item_id = t.id'); + + return $db->setQuery($query)->loadObjectList('id'); + } + + /** + * @param string $name + * + * @return string + */ + public function setLayout(string $name): string + { + return $name . '.twig'; + } +} diff --git a/libraries/src/Model/PageModel.php b/libraries/src/Model/PageModel.php new file mode 100644 index 0000000..d0e5cd6 --- /dev/null +++ b/libraries/src/Model/PageModel.php @@ -0,0 +1,98 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Model; + +use Joomla\Database\DatabaseDriver; +use Joomla\Model\DatabaseModelInterface; +use Joomla\Model\DatabaseModelTrait; +use Kumwe\CMS\Model\Util\MenuInterface; +use Kumwe\CMS\Model\Util\PageInterface; +use Kumwe\CMS\Model\Util\HomeMenuInterface; +use Kumwe\CMS\Model\Util\HomeMenuTrait; +use Kumwe\CMS\Model\Util\SiteMenuTrait; +use Kumwe\CMS\Model\Util\SitePageTrait; + +/** + * Model class + */ +class PageModel implements DatabaseModelInterface, MenuInterface, PageInterface, HomeMenuInterface +{ + use DatabaseModelTrait, HomeMenuTrait, SiteMenuTrait, SitePageTrait; + + /** + * Instantiate the model. + * + * @param DatabaseDriver $db The database adapter. + */ + public function __construct(DatabaseDriver $db) + { + $this->setDb($db); + } + + /** + * Method to get all date needed for the view + * + * @param string|null $page + * + * @return array The data needed + */ + public function getData(?string $page): array + { + // set the defaults + $data = (object) [ + // main title + 'title' => 'Error', + // menus + 'menus' => [], + // menu ID + 'menu_id' => 0, + // is this the home page + 'menu_home' => false, + // home page title + 'home_menu_title' => 'Home' + ]; + // we check if we have a home page + $home_page = $this->getHomePage(); + // get the page data + if (empty($page) && isset($home_page->item_id) && $home_page->item_id > 0) + { + // this is the home menu + $data = $this->getPageItemById($home_page->item_id); + $data->menu_home = true; + } + elseif (!empty($page)) + { + $data = $this->getPageItemByPath($page); + } + // load the home menu title + if (isset($home_page->title)) + { + $data->home_menu_title = $home_page->title; + } + // check if we found any data + if (isset($data->id)) + { + // check if we have intro text we add it to full text + if (!empty($data->introtext)) + { + $data->fulltext = $data->introtext . $data->fulltext; + } + } + + // set the menus if possible + if (isset($data->menu_id) && $data->menu_id > 0) + { + $data->menus = $this->getMenus($data->menu_id); + } + + return (array) $data; + } +} diff --git a/libraries/src/Model/UserModel.php b/libraries/src/Model/UserModel.php new file mode 100644 index 0000000..2258df2 --- /dev/null +++ b/libraries/src/Model/UserModel.php @@ -0,0 +1,337 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Model; + +use Joomla\Database\DatabaseDriver; +use Joomla\Database\ParameterType; +use Joomla\Model\DatabaseModelInterface; +use Joomla\Model\DatabaseModelTrait; +use Kumwe\CMS\Date\Date; +use Kumwe\CMS\Model\Util\GetUsergroupsInterface; +use Kumwe\CMS\Model\Util\GetUsergroupsTrait; +use Exception; +use RuntimeException; +use stdClass; + +/** + * Model class + */ +class UserModel implements DatabaseModelInterface, GetUsergroupsInterface +{ + use DatabaseModelTrait, GetUsergroupsTrait; + + /** + * @var array + */ + public $tempItem; + + /** + * Instantiate the model. + * + * @param DatabaseDriver|null $db The database adapter. + */ + public function __construct(DatabaseDriver $db = null) + { + $this->setDb($db); + } + + /** + * Add an item + * + * @param int $id + * @param string $name + * @param string $username + * @param array $groups + * @param string $email + * @param string $password + * @param int $block + * @param int $sendEmail + * @param string $registerDate + * @param int $activation + * + * @return int + * @throws Exception + */ + public function setItem( + int $id, + string $name, + string $username, + array $groups, + string $email, + string $password, + int $block, + int $sendEmail, + string $registerDate, + int $activation): int + { + $db = $this->getDb(); + + $data = [ + 'name' => (string) $name, + 'username' => (string) $username, + 'email' => (string) $email, + 'block' => (int) $block, + 'sendEmail' => (int) $sendEmail, + 'registerDate' => (string) (empty($registerDate)) ? (new Date())->toSql() : (new Date($registerDate))->toSql(), + 'activation' => (int) $activation + ]; + + // only update password if set + if (!empty($password) && strlen($password) > 6) + { + $data['password'] = (string) $password; + } + + // if we have ID update + if ($id > 0) + { + $data['id'] = (int) $id; + // we remove registration date when we update the user + unset($data['registerDate']); + // change to object + $data = (object) $data; + + try + { + $db->updateObject('#__users', $data, 'id'); + } + catch (RuntimeException $exception) + { + throw new RuntimeException($exception->getMessage(), 404); + } + } + else + { + // we don't have any params for now + $data['params'] = ''; + // change to object + $data = (object) $data; + + try + { + $db->insertObject('#__users', $data); + } + catch (RuntimeException $exception) + { + throw new RuntimeException($exception->getMessage(), 404); + } + + $id = $db->insertid(); + } + + // update the group linked to this user + // only if there are groups + if (count($groups) > 0) + { + try + { + $this->setGroups($id, $groups); + } + catch (RuntimeException $exception) + { + throw new RuntimeException($exception->getMessage(), 404); + } + } + + return $id; + } + + /** + * Add groups for this user + * + * @param int $id + * @param array $groups + * + * @return bool + * @throws Exception + */ + private function setGroups(int $id, array $groups): bool + { + $db = $this->getDb(); + // add the new groups + $query = $db->getQuery(true) + ->insert($db->quoteName('#__user_usergroup_map')) + ->columns($db->quoteName(['user_id', 'group_id'])); + // Insert values. + foreach ($groups as $group) + { + $query->values(implode(',', [(int) $id, (int) $group])); + } + // execute the update/change + try + { + // delete link to groups + if ($this->deleteGroups($id)) + { + // add the new groups + $db->setQuery($query)->execute(); + } + } + catch (RuntimeException $e) + { + throw new RuntimeException($e->getMessage(), 404); + } + + return true; + } + + /** + * Get an item + * + * @param int|null $id + * + * @return stdClass + * @throws Exception + */ + public function getItem(?int $id): stdClass + { + $db = $this->getDb(); + // default object (use posted values if set) + if (is_array($this->tempItem)) + { + $default = (object) $this->tempItem; + } + else + { + $default = new stdClass(); + } + // to be sure ;) + $default->today_date = (new Date())->toSql(); + $default->post_key = "?task=create"; + $default->block = 0; + $default->activation = 1; + $default->sendEmail = 1; + // always remove password + $default->password = 'xxxxxxxxxx'; + $default->password2 = 'xxxxxxxxxx'; + + // we return the default if id not correct + if (!is_numeric($id)) + { + return $default; + } + + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__users')) + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER) + ->setLimit(1); + + try + { + $result = $db->setQuery($query)->loadObject(); + } + catch (RuntimeException $e) + { + // we ignore this and just return an empty object + } + + if (isset($result) && $result instanceof stdClass && isset($result->id)) + { + $result->post_key = "?id=$id&task=edit"; + $result->today_date = $default->today_date; + // always remove password + $result->password = $default->password; + $result->password2 = $default->password2; + + // Initialise some variables + $query = $db->getQuery(true) + ->select('m.group_id') + ->from($db->quoteName('#__user_usergroup_map', 'm')) + ->where($db->quoteName('m.user_id') . ' = :user_id') + ->bind(':user_id', $result->id, ParameterType::INTEGER); + + try + { + // we just load the ID's + $result->groups = $db->setQuery($query)->loadColumn(); + } + catch (RuntimeException $e) + { + // we ignore this and just return result + } + + return $result; + } + + return $default; + } + + /** + * @param string $name + * + * @return string + */ + public function setLayout(string $name): string + { + return $name . '.twig'; + } + + /** + * @param int $id + * + * @return bool + * @throws Exception + */ + public function delete(int $id): bool + { + $db = $this->getDb(); + // Delete the user from the database + $query = $db->getQuery(true) + ->delete($db->quoteName('#__users')) + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + try + { + // delete link to groups + if ($this->deleteGroups($id)) + { + // delete user + $db->setQuery($query)->execute(); + } + } + catch (RuntimeException $e) + { + throw new RuntimeException($e->getMessage(), 404); + } + + return true; + } + + /** + * delete all groups form this user + * + * @param int $id + * + * @return bool + * @throws Exception + */ + private function deleteGroups(int $id): bool + { + $db = $this->getDb(); + // Delete the user from the database + $query = $db->getQuery(true) + ->delete($db->quoteName('#__user_usergroup_map')) + ->where($db->quoteName('user_id') . ' = :user_id') + ->bind(':user_id', $id, ParameterType::INTEGER); + try + { + $db->setQuery($query)->execute(); + } + catch (RuntimeException $e) + { + throw new RuntimeException($e->getMessage(), 404); + } + + return true; + } +} diff --git a/libraries/src/Model/UsergroupModel.php b/libraries/src/Model/UsergroupModel.php new file mode 100644 index 0000000..eb9a039 --- /dev/null +++ b/libraries/src/Model/UsergroupModel.php @@ -0,0 +1,239 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Model; + +use Joomla\Database\DatabaseDriver; +use Joomla\Database\ParameterType; +use Joomla\Model\DatabaseModelInterface; +use Joomla\Model\DatabaseModelTrait; +use Kumwe\CMS\Model\Util\GetUsergroupsInterface; +use Kumwe\CMS\Model\Util\GetUsergroupsTrait; + +/** + * Model class + */ +class UsergroupModel implements DatabaseModelInterface, GetUsergroupsInterface +{ + use DatabaseModelTrait, GetUsergroupsTrait; + + /** + * @var array + */ + public $tempItem; + + /** + * Instantiate the model. + * + * @param DatabaseDriver $db The database adapter. + */ + public function __construct(DatabaseDriver $db) + { + $this->setDb($db); + } + + /** + * Add an item + * + * @param int $id + * @param string $title + * @param array $params + * + * @return int + */ + public function setItem( + int $id, + string $title, + array $params): int + { + $db = $this->getDb(); + + if (count($params) > 0) + { + $params = json_encode($params); + } + else + { + $params = ''; + } + + $data = [ + 'title' => (string) $title, + 'params' => (string) $params + ]; + + // if we have ID update + if ($id > 0) + { + $data['id'] = (int) $id; + // change to object + $data = (object) $data; + + try + { + $db->updateObject('#__usergroups', $data, 'id'); + } + catch (\RuntimeException $exception) + { + throw new \RuntimeException($exception->getMessage(), 404); + } + + return $id; + + } + else + { + // change to object + $data = (object) $data; + + try + { + $db->insertObject('#__usergroups', $data); + } + catch (\RuntimeException $exception) + { + throw new \RuntimeException($exception->getMessage(), 404); + } + + return $db->insertid(); + } + } + + /** + * Get an item + * + * @param int|null $id + * + * @return \stdClass + * @throws \Exception + */ + public function getItem(?int $id): \stdClass + { + $db = $this->getDb(); + // default object (use posted values if set) + if (is_array($this->tempItem)) + { + $default = (object) $this->tempItem; + } + else + { + $default = new \stdClass(); + $default->params = $this->getGroupDefaultsAccess(); + } + + // we return the default if id not correct + if (!is_numeric($id)) + { + return $default; + } + + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__usergroups')) + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER) + ->setLimit(1); + + try + { + $result = $db->setQuery($query)->loadObject(); + } + catch (\RuntimeException $e) + { + // we ignore this and just return an empty object + } + + if (isset($result) && $result instanceof \stdClass) + { + $result->post_key = "?id=$id&task=edit"; + + // make sure to set the params + $result->params = json_decode($result->params); + // We set an empty default + if (!is_array($result->params)) + { + $result->params = $this->getGroupDefaultsAccess(); + } + + return $result; + } + + return $default; + } + + /** + * @param string $name + * + * @return string + */ + public function setLayout(string $name): string + { + return $name . '.twig'; + } + + /** + * @param int $id + * + * @return bool + */ + public function delete(int $id): bool + { + $db = $this->getDb(); + // Purge the session + $query = $db->getQuery(true) + ->delete($db->quoteName('#__usergroups')) + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + try + { + $db->setQuery($query)->execute(); + } + catch (\RuntimeException $e) + { + // delete failed + return false; + } + + return true; + } + + /** + * @param int $id + * + * @return bool + */ + public function linked(int $id): bool + { + $db = $this->getDb(); + // first check if this item is linked to menu + $query = $db->getQuery(true) + ->select($db->quoteName('user_id')) + ->from($db->quoteName('#__user_usergroup_map')) + ->where($db->quoteName('group_id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + + try + { + $users = $db->setQuery($query)->loadColumn(); + } + catch (\RuntimeException $e) + { + // not linked... or something + return false; + } + + if ($users) + { + return true; + } + + return false; + } +} diff --git a/libraries/src/Model/UsergroupsModel.php b/libraries/src/Model/UsergroupsModel.php new file mode 100644 index 0000000..4a715d8 --- /dev/null +++ b/libraries/src/Model/UsergroupsModel.php @@ -0,0 +1,50 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Model; + +use Joomla\Database\DatabaseDriver; +use Joomla\Model\DatabaseModelInterface; +use Joomla\Model\DatabaseModelTrait; +use Kumwe\CMS\Model\Util\GetUsergroupsInterface; +use Kumwe\CMS\Model\Util\GetUsergroupsTrait; + +/** + * Model class + */ +class UsergroupsModel implements DatabaseModelInterface, GetUsergroupsInterface +{ + use DatabaseModelTrait, GetUsergroupsTrait; + + /** + * Instantiate the model. + * + * @param DatabaseDriver $db The database adapter. + */ + public function __construct(DatabaseDriver $db) + { + $this->setDb($db); + } + + /** + * Get all items + * + * @return array + */ + public function getItems(): array + { + return $this->getUsergroups(); + } + + public function setLayout(string $name): string + { + return $name . '.twig'; + } +} diff --git a/libraries/src/Model/UsersModel.php b/libraries/src/Model/UsersModel.php new file mode 100644 index 0000000..5cbf2a2 --- /dev/null +++ b/libraries/src/Model/UsersModel.php @@ -0,0 +1,67 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Model; + +use Joomla\Database\DatabaseDriver; +use Joomla\Model\DatabaseModelInterface; +use Joomla\Model\DatabaseModelTrait; +use Kumwe\CMS\Model\Util\GetUsergroupsInterface; +use Kumwe\CMS\Model\Util\GetUsergroupsTrait; + +/** + * Model class + */ +class UsersModel implements DatabaseModelInterface, GetUsergroupsInterface +{ + use DatabaseModelTrait, GetUsergroupsTrait; + + /** + * Instantiate the model. + * + * @param DatabaseDriver $db The database adapter. + */ + public function __construct(DatabaseDriver $db) + { + $this->setDb($db); + } + + /** + * Get all items + * + * @return array + */ + public function getItems(): array + { + $db = $this->getDb(); + + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__users')); + + $users = $db->setQuery($query)->loadObjectList('id'); + + // add groups + if ($users) + { + foreach ($users as $id => &$user) + { + $user->groups = $this->getUsergroups($id); + } + } + + return $users; + } + + public function setLayout(string $name): string + { + return $name . '.twig'; + } +} diff --git a/libraries/src/Model/Util/GetUsergroupsInterface.php b/libraries/src/Model/Util/GetUsergroupsInterface.php new file mode 100644 index 0000000..1f871c2 --- /dev/null +++ b/libraries/src/Model/Util/GetUsergroupsInterface.php @@ -0,0 +1,37 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Model\Util; + +/** + * Class for all user groups + * + * @since 1.0.0 + */ +interface GetUsergroupsInterface +{ + /** + * Get all user groups + * + * @param int|null $id + * + * @return array + */ + public function getUsergroups(?int $id = null): array; + + /** + * Get the group default full access values + * + * @param string $access + * + * @return array + */ + public function getGroupDefaultsAccess(string $access = 'CRUD'): array; +} diff --git a/libraries/src/Model/Util/GetUsergroupsTrait.php b/libraries/src/Model/Util/GetUsergroupsTrait.php new file mode 100644 index 0000000..bac964d --- /dev/null +++ b/libraries/src/Model/Util/GetUsergroupsTrait.php @@ -0,0 +1,103 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Model\Util; + +use Joomla\Database\ParameterType; +use Kumwe\CMS\Utilities\StringHelper; +use RuntimeException; +use stdClass; + +/** + * Class for all user groups + * + * @since 1.0.0 + * @method getDb() + */ +trait GetUsergroupsTrait +{ + /** + * Get all user groups + * + * @param int|null $id user ID + * + * @return array + */ + public function getUsergroups(?int $id = null): array + { + $db = $this->getDb(); + + $query = $db->getQuery(true) + ->select('g.*') + ->from($db->quoteName('#__usergroups', 'g')); + + if ($id > 0) + { + $query + ->join('INNER', $db->quoteName('#__user_usergroup_map', 'm'), 'g.id = m.group_id') + ->where($db->quoteName('m.user_id') . ' = :user_id') + ->bind(':user_id', $id, ParameterType::INTEGER); + } + + try + { + $groups = $db->setQuery($query)->loadObjectList('id'); + } + catch (RuntimeException $e) + { + throw new RuntimeException($e->getMessage(), 404); + } + + if (is_array($groups) && count($groups) > 0) + { + foreach ($groups as $n => &$group) + { + $group->params = json_decode($group->params); + // We set an empty default + if (!is_array($group->params)) + { + $group->params = $this->getGroupDefaultsAccess(); + } + } + return $groups; + } + + return []; + } + + /** + * Get the group default full access values + * + * @param string $access + * + * @return array + */ + public function getGroupDefaultsAccess(string $access = ''): array + { + return [ + (object) [ + 'area' => 'user', + 'access' => $access + ], + (object) [ + 'area' => 'usergroup', + 'access' => $access + ], + (object) [ + 'area' => 'menu', + 'access' => $access + ], + (object) [ + 'area' => 'item', + 'access' => $access + ], + ]; + } +} diff --git a/libraries/src/Model/Util/HomeMenuInterface.php b/libraries/src/Model/Util/HomeMenuInterface.php new file mode 100644 index 0000000..76e0dd7 --- /dev/null +++ b/libraries/src/Model/Util/HomeMenuInterface.php @@ -0,0 +1,24 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Model\Util; + +/** + * Class for getting the home page + * + * @since 1.0.0 + */ +interface HomeMenuInterface +{ + /** + * @return \stdClass + */ + public function getHomePage(): \stdClass; +} diff --git a/libraries/src/Model/Util/HomeMenuTrait.php b/libraries/src/Model/Util/HomeMenuTrait.php new file mode 100644 index 0000000..5b6e0d8 --- /dev/null +++ b/libraries/src/Model/Util/HomeMenuTrait.php @@ -0,0 +1,51 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Model\Util; + +/** + * Trait for getting home menu + * + * @since 1.0.0 + */ +trait HomeMenuTrait +{ + /** + * @return \stdClass + */ + public function getHomePage(): \stdClass + { + $db = $this->getDb(); + + $query = $db->getQuery(true) + ->select('a.*') + ->from($db->quoteName('#__menu', 'a')) + ->where($db->quoteName('a.parent_id') . ' = 0') + ->where($db->quoteName('a.published') . ' = 1') + ->where($db->quoteName('a.home') . ' = 1') + ->setLimit(1); + + try + { + $home = $db->setQuery($query)->loadObject(); + } + catch (\RuntimeException $e) + { + return new \stdClass(); + } + + if ($home) + { + return $home; + } + + return new \stdClass(); + } +} diff --git a/libraries/src/Model/Util/MenuInterface.php b/libraries/src/Model/Util/MenuInterface.php new file mode 100644 index 0000000..ffba24e --- /dev/null +++ b/libraries/src/Model/Util/MenuInterface.php @@ -0,0 +1,28 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Model\Util; + +/** + * Class for getting menu items + * + * @since 1.0.0 + */ +interface MenuInterface +{ + /** + * Get all menu items + * + * @param int $active + * + * @return array + */ + public function getMenus(int $active = 0): array; +} diff --git a/libraries/src/Model/Util/PageInterface.php b/libraries/src/Model/Util/PageInterface.php new file mode 100644 index 0000000..efc0f86 --- /dev/null +++ b/libraries/src/Model/Util/PageInterface.php @@ -0,0 +1,41 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Model\Util; + +/** + * Class for getting page data + * + * @since 1.0.0 + */ +interface PageInterface +{ + /** + * Get page data + * + * @param string $path The page path + * + * @return \stdClass + * + * @throws \RuntimeException + */ + public function getPageItemByPath(string $path): \stdClass; + + /** + * Get page data + * + * @param int $item The item id + * + * @return \stdClass + * + * @throws \RuntimeException + */ + public function getPageItemById(int $item): \stdClass; +} diff --git a/libraries/src/Model/Util/SelectMenuTrait.php b/libraries/src/Model/Util/SelectMenuTrait.php new file mode 100644 index 0000000..11470ae --- /dev/null +++ b/libraries/src/Model/Util/SelectMenuTrait.php @@ -0,0 +1,54 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Model\Util; + +use Joomla\Database\ParameterType; + +/** + * Trait for getting menu items + * + * @since 1.0.0 + */ +trait SelectMenuTrait +{ + /** + * Get all menu items + * + * @param int $active + * + * @return array + */ + public function getMenus(int $active = 0): array + { + $db = $this->getDb(); + + if (empty($active) && !empty($this->id)) + { + $active = $this->id; + } + + $query = $db->getQuery(true) + ->select($db->quoteName(array('id', 'title'))) + ->from($db->quoteName('#__menu')) + ->where('published = 1') + ->where('home = 0'); + + // we need to remove the active menu + if ($active > 0) + { + $query + ->where($db->quoteName('id') . ' != :id') + ->bind(':id', $active, ParameterType::INTEGER); + } + + return $db->setQuery($query)->loadObjectList('id'); + } +} diff --git a/libraries/src/Model/Util/SiteMenuTrait.php b/libraries/src/Model/Util/SiteMenuTrait.php new file mode 100644 index 0000000..60d5120 --- /dev/null +++ b/libraries/src/Model/Util/SiteMenuTrait.php @@ -0,0 +1,70 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Model\Util; + +/** + * Trait for getting menu items + * + * @since 1.0.0 + */ +trait SiteMenuTrait +{ + /** + * Get all menu items that are root and published and not home page + * + * @param int $active + * + * @return array + */ + public function getMenus(int $active = 0): array + { + $db = $this->getDb(); + + $query = $db->getQuery(true) + ->select('a.*') + ->from($db->quoteName('#__menu', 'a')) + ->where($db->quoteName('a.published') . ' = 1') + ->where($db->quoteName('a.home') . ' = 0'); + + try + { + $menus = $db->setQuery($query)->loadObjectList(); + } + catch (\RuntimeException $e) + { + return []; + } + + if ($menus) + { + $bucket = []; + foreach ($menus as $menu) + { + $row = []; + // set the details + $row['id'] = $menu->id; + $row['title'] = $menu->title; + $row['path'] = $menu->path; + $row['parent'] = $menu->parent_id; + // set position + $params = (isset($menu->params) && strpos($menu->params, 'position') !== false) ? json_decode($menu->params) : null; + // default is center + $row['position'] = (is_object($params) && isset($params->position)) ? $params->position : 'center'; + + // add to our bucket + $bucket[] = $row; + } + return $bucket; + } + + return []; + } +} diff --git a/libraries/src/Model/Util/SitePageTrait.php b/libraries/src/Model/Util/SitePageTrait.php new file mode 100644 index 0000000..9276039 --- /dev/null +++ b/libraries/src/Model/Util/SitePageTrait.php @@ -0,0 +1,103 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Model\Util; + +use Joomla\Database\ParameterType; + +/** + * Trait for getting page data + * + * @since 1.0.0 + */ +trait SitePageTrait +{ + /** + * Get page data + * + * @param string $path The page path + * + * @return \stdClass + * + * @throws \RuntimeException + */ + public function getPageItemByPath(string $path): \stdClass + { + $db = $this->getDb(); + + $query = $db->getQuery(true) + ->select('i.*') + ->select($db->quoteName(array('m.id'), array('menu_id'))) + ->from($db->quoteName('#__menu', 'm')) + ->join('INNER', $db->quoteName('#__item', 'i'), 'm.item_id = i.id') + ->where($db->quoteName('i.state') . ' >= 1') + ->where($db->quoteName('m.published') . ' = 1') + ->where($db->quoteName('m.path') . ' = :path') + ->bind(':path', $path) + ->setLimit(1); + + try + { + $page = $db->setQuery($query)->loadObject(); + } + catch (\RuntimeException $e) + { + return new \stdClass(); + } + + if ($page) + { + return $page; + } + + return new \stdClass(); + } + + /** + * Get page data + * + * @param int $item The item id + * + * @return \stdClass + * + * @throws \RuntimeException + */ + public function getPageItemById(int $item): \stdClass + { + $db = $this->getDb(); + + $query = $db->getQuery(true) + ->select('i.*') + ->select($db->quoteName(array('m.id'), array('menu_id'))) + ->from($db->quoteName('#__item', 'i')) + ->join('INNER', $db->quoteName('#__menu', 'm'), 'i.id = m.item_id') + ->where($db->quoteName('m.published') . ' = 1') + ->where($db->quoteName('i.state') . ' >= 1') + ->where($db->quoteName('i.id') . ' = :id') + ->bind(':id', $item, ParameterType::INTEGER) + ->setLimit(1); + + try + { + $page = $db->setQuery($query)->loadObject(); + } + catch (\RuntimeException $e) + { + return new \stdClass(); + } + + if ($page) + { + return $page; + } + + return new \stdClass(); + } +} diff --git a/libraries/src/Model/Util/UniqueInterface.php b/libraries/src/Model/Util/UniqueInterface.php new file mode 100644 index 0000000..4d6a6f0 --- /dev/null +++ b/libraries/src/Model/Util/UniqueInterface.php @@ -0,0 +1,44 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Model\Util; + +/** + * Class for getting unique string + * + * @since 1.0.0 + */ +interface UniqueInterface +{ + /** + * Get a unique string + * + * @param int $id + * @param string $value + * @param int $parent + * @param string $key + * @param string $spacer + * + * @return string + */ + public function unique(int $id, string $value, int $parent = -1, string $key = 'alias', string $spacer = '-'): string; + + /** + * Check if an any key exist with same parent + * + * @param int $id + * @param string $value + * @param string $key + * @param int $parent + * + * @return bool + */ + public function exist(int $id, string $value, string $key = 'alias', int $parent = -1): bool; +} diff --git a/libraries/src/Model/Util/UniqueMenuAliasTrait.php b/libraries/src/Model/Util/UniqueMenuAliasTrait.php new file mode 100644 index 0000000..20344b6 --- /dev/null +++ b/libraries/src/Model/Util/UniqueMenuAliasTrait.php @@ -0,0 +1,103 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Model\Util; + +use Joomla\Database\ParameterType; + +/** + * Trait for getting unique string + * + * @since 1.0.0 + */ +trait UniqueMenuAliasTrait +{ + /** + * Get a unique string + * + * @param int $id + * @param string $value + * @param int $parent + * @param string $key + * @param string $spacer + * + * @return string + */ + public function unique(int $id, string $value, int $parent = -1, string $key = 'alias', string $spacer = '-'): string + { + // start building the value + $value = str_replace($spacer, ' ', $value); + $value = preg_replace('/\s+/', $spacer, strtolower(preg_replace("/[^A-Za-z0-9\- ]/", '', $value))); + // set a counter + $counter = 2; + // set original tracker + $original = $value; + // check if we found any with the same alias + while ($this->exist($id, $value, $key, $parent)) + { + $value = $original . '-' . $counter; + $counter++; + } + // return the unique value (on this parent layer) + return $value; + } + + /** + * Check if an any key exist with same parent + * + * @param int $id + * @param string $value + * @param string $key + * @param int $parent + * + * @return bool + */ + public function exist(int $id, string $value, string $key = 'alias', int $parent = -1): bool + { + $db = $this->getDb(); + $query = $db->getQuery(true) + ->select('id') + ->from($db->quoteName('#__menu')) + ->where($db->quoteName($key) . " = :$key") + ->bind(":$key", $value) + ->setLimit(1); + + // only add the id item exist + if ($parent >= 0) + { + $query + ->where($db->quoteName('parent_id') . ' = :parent_id') + ->bind(':parent_id', $parent, ParameterType::INTEGER); + } + + // only add the id item exist + if ($id > 0) + { + $query + ->where($db->quoteName('id') . ' != :id') + ->bind(':id', $id, ParameterType::INTEGER); + } + + try + { + $result = $db->setQuery($query)->loadResult(); + } + catch (\RuntimeException $e) + { + // we ignore this and just return an empty object + } + + if (isset($result) && $result > 0) + { + return true; + } + return false; + } +} diff --git a/libraries/src/Renderer/ApplicationContext.php b/libraries/src/Renderer/ApplicationContext.php new file mode 100644 index 0000000..db9421f --- /dev/null +++ b/libraries/src/Renderer/ApplicationContext.php @@ -0,0 +1,62 @@ +app = $app; + } + + /** + * Gets the base path. + * + * @return string The base path + */ + public function getBasePath() + { + return rtrim($this->app->get('uri.base.path'), '/'); + } + + /** + * Checks whether the request is secure or not. + * + * @return boolean + */ + public function isSecure() + { + if ($this->app instanceof AbstractWebApplication) + { + return $this->app->isSslConnection(); + } + + return false; + } +} \ No newline at end of file diff --git a/libraries/src/Renderer/FrameworkExtension.php b/libraries/src/Renderer/FrameworkExtension.php new file mode 100644 index 0000000..5d2b11e --- /dev/null +++ b/libraries/src/Renderer/FrameworkExtension.php @@ -0,0 +1,67 @@ + ['html']]), + new TwigFunction('message_queue', [FrameworkTwigRuntime::class, 'getMessageQueue']), + new TwigFunction('user_array', [FrameworkTwigRuntime::class, 'getUserArray']), + new TwigFunction('user', [FrameworkTwigRuntime::class, 'getUser']), + new TwigFunction('token', [FrameworkTwigRuntime::class, 'getToken']), + new TwigFunction('shorten_string', [FrameworkTwigRuntime::class, 'shortenString']), + ]; + } + + /** + * Removes the application root path defined by the constant "JPATH_ROOT" + * + * @param string $string The string to process + * + * @return string + */ + public function stripRootPath(string $string): string + { + return str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR, '', $string); + } +} diff --git a/libraries/src/Renderer/FrameworkTwigRuntime.php b/libraries/src/Renderer/FrameworkTwigRuntime.php new file mode 100644 index 0000000..4567785 --- /dev/null +++ b/libraries/src/Renderer/FrameworkTwigRuntime.php @@ -0,0 +1,278 @@ +app = $app; + $this->preloadManager = $preloadManager; + $this->sriManifestPath = $sriManifestPath; + $this->session = $session; + } + + /** + * Retrieves the current URI + * + * @return string + */ + public function getRequestUri(): string + { + return $this->app->get('uri.request'); + } + + /** + * Get the URI for a route + * + * @param string $route Route to get the path for + * + * @return string + */ + public function getRouteUri(string $route = ''): string + { + return $this->app->get('uri.base.path') . $route; + } + + /** + * Get the full URL for a route + * + * @param string $route Route to get the URL for + * + * @return string + */ + public function getRouteUrl(string $route = ''): string + { + return $this->app->get('uri.base.host') . $this->getRouteUri($route); + } + + /** + * Get form Token + * + * @return string + */ + public function getToken(): string + { + if ($this->session instanceof SessionInterface) + { + return $this->session->getToken(); + } + return ''; + } + + /** + * Shorten a string + * + * @input string The you would like to shorten + * + * @returns string on success + * + * @since 1.0.0 + */ + public function shortenString($string, $length = 100) + { + if (is_string($string) && strlen($string) > $length) + { + $initial = strlen($string); + $words = preg_split('/([\s\n\r]+)/', $string, null, PREG_SPLIT_DELIM_CAPTURE); + $words_count = count((array)$words); + + $word_length = 0; + $last_word = 0; + for (; $last_word < $words_count; ++$last_word) + { + $word_length += strlen($words[$last_word]); + if ($word_length > $length) + { + break; + } + } + + $newString = implode(array_slice($words, 0, $last_word)); + return trim($newString) . '...'; + } + return $string; + } + + /** + * Get current user as array + * + * @return array + */ + public function getUserArray(): array + { + if (!$this->user instanceof User) + { + /** @var \Kumwe\CMS\User\User $user */ + $this->user = Factory::getContainer()->get(UserFactoryInterface::class)->getUser(); + } + // check again + if ($this->user instanceof User && method_exists($this->user, 'toArray')) + { + return $this->user->toArray(); + } + return []; + } + + public function getUser(string $key = 'name', $default = '') + { + if (!$this->user instanceof User) + { + /** @var \Kumwe\CMS\User\User $user */ + $this->user = Factory::getContainer()->get(UserFactoryInterface::class)->getUser(); + } + // check again + if ($this->user instanceof User) + { + return $this->user->get($key, $default); + } + return ''; + } + + /** + * Get any messages in the queue + * + * @return array + */ + public function getMessageQueue(): array + { + if (method_exists($this->app, 'getMessageQueue')) + { + return $this->app->getMessageQueue(true); + } + return []; + } + + /** + * Get the SRI attributes for an asset + * + * @param string $path A public path + * + * @return string + */ + public function getSriAttributes(string $path): string + { + if ($this->sriManifestData === null) + { + if (!file_exists($this->sriManifestPath)) + { + throw new \RuntimeException(sprintf('SRI manifest file "%s" does not exist.', $this->sriManifestPath)); + } + + $sriManifestContents = file_get_contents($this->sriManifestPath); + + if ($sriManifestContents === false) + { + throw new \RuntimeException(sprintf('Could not read SRI manifest file "%s".', $this->sriManifestPath)); + } + + $this->sriManifestData = json_decode($sriManifestContents, true); + + if (0 < json_last_error()) + { + throw new \RuntimeException(sprintf('Error parsing JSON from SRI manifest file "%s" - %s', $this->sriManifestPath, json_last_error_msg())); + } + } + + $assetKey = "/$path"; + + if (!isset($this->sriManifestData[$assetKey])) + { + return ''; + } + + $attributes = ''; + + foreach ($this->sriManifestData[$assetKey] as $key => $value) + { + $attributes .= ' ' . $key . '="' . $value . '"'; + } + + return $attributes; + } + + /** + * Preload a resource + * + * @param string $uri The URI for the resource to preload + * @param string $linkType The preload method to apply + * @param array $attributes The attributes of this link (e.g. "array('as' => true)", "array('pr' => 0.5)") + * + * @return string + * + * @throws \InvalidArgumentException + */ + public function preloadAsset(string $uri, string $linkType = 'preload', array $attributes = []): string + { + $this->preloadManager->link($uri, $linkType, $attributes); + + return $uri; + } +} \ No newline at end of file diff --git a/libraries/src/Service/AdminApplicationProvider.php b/libraries/src/Service/AdminApplicationProvider.php new file mode 100644 index 0000000..b1a0116 --- /dev/null +++ b/libraries/src/Service/AdminApplicationProvider.php @@ -0,0 +1,103 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Service; + +use Joomla\Application\AbstractWebApplication; +use Joomla\Application\Controller\ControllerResolverInterface; +use Joomla\Application\Web\WebClient; +use Joomla\DI\Container; +use Joomla\DI\ServiceProviderInterface; +use Joomla\Event\DispatcherInterface; + +use Joomla\Session\SessionInterface; +use Kumwe\CMS\User\UserFactoryInterface; +use Kumwe\CMS\Application\AdminApplication; + +use Joomla\Input\Input; +use Joomla\Router\RouterInterface; +use Psr\Log\LoggerInterface; + +/** + * Admin Application service provider + * source: https://github.com/joomla/framework.joomla.org/blob/master/src/Service/ApplicationProvider.php + */ +class AdminApplicationProvider implements ServiceProviderInterface +{ + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + */ + public function register(Container $container): void + { + /* + * Application Classes + */ + + // This service cannot be protected as it is decorated when the debug bar is available + $container->alias(AdminApplication::class, AbstractWebApplication::class) + ->share(AbstractWebApplication::class, [$this, 'getAdminApplicationClassService']); + + /* + * Application Helpers and Dependencies + */ + $container->share(WebClient::class, [$this, 'getWebClientService'], true); + } + + /** + * Get the WebApplication class service + * + * @param Container $container The DI container. + * + * @return AdminApplication + */ + public function getAdminApplicationClassService(Container $container): AdminApplication + { + /** @var \Kumwe\CMS\Application\AdminApplication $application */ + $application = new AdminApplication( + $container->get(ControllerResolverInterface::class), + $container->get(RouterInterface::class), + $container->get(Input::class), + $container->get('config'), + $container->get(WebClient::class) + ); + + $application->httpVersion = '2'; + + // Inject extra services + $application->setDispatcher($container->get(DispatcherInterface::class)); + $application->setLogger($container->get(LoggerInterface::class)); + $application->setSession($container->get(SessionInterface::class)); + $application->setUserFactory($container->get(UserFactoryInterface::class)); + + return $application; + } + + /** + * Get the web client service + * + * @param Container $container The DI container. + * + * @return WebClient + */ + public function getWebClientService(Container $container): WebClient + { + /** @var Input $input */ + $input = $container->get(Input::class); + $userAgent = $input->server->getString('HTTP_USER_AGENT', ''); + $acceptEncoding = $input->server->getString('HTTP_ACCEPT_ENCODING', ''); + $acceptLanguage = $input->server->getString('HTTP_ACCEPT_LANGUAGE', ''); + + return new WebClient($userAgent, $acceptEncoding, $acceptLanguage); + } +} diff --git a/libraries/src/Service/AdminMVCProvider.php b/libraries/src/Service/AdminMVCProvider.php new file mode 100644 index 0000000..b11b40e --- /dev/null +++ b/libraries/src/Service/AdminMVCProvider.php @@ -0,0 +1,605 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Service; + +use Joomla\Application\Controller\ContainerControllerResolver; +use Joomla\Application\Controller\ControllerResolverInterface; +use Joomla\Database\DatabaseInterface; +use Joomla\DI\Container; +use Joomla\DI\ServiceProviderInterface; + +use Kumwe\CMS\Controller\DashboardController; +use Kumwe\CMS\Controller\ItemsController; +use Kumwe\CMS\Controller\ItemController; +use Kumwe\CMS\Controller\LoginController; +use Kumwe\CMS\Controller\MenuController; +use Kumwe\CMS\Controller\MenusController; +use Kumwe\CMS\Controller\UserController; +use Kumwe\CMS\Controller\UsersController; +use Kumwe\CMS\Controller\UserGroupController; +use Kumwe\CMS\Controller\UsergroupsController; +use Kumwe\CMS\Controller\WrongCmsController; +use Kumwe\CMS\Model\DashboardModel; +use Kumwe\CMS\Model\ItemsModel; +use Kumwe\CMS\Model\ItemModel; +use Kumwe\CMS\Model\MenusModel; +use Kumwe\CMS\Model\MenuModel; +use Kumwe\CMS\Model\UserModel; +use Kumwe\CMS\Model\UsersModel; +use Kumwe\CMS\Model\UsergroupModel; +use Kumwe\CMS\Model\UsergroupsModel; +use Kumwe\CMS\User\UserFactoryInterface; +use Kumwe\CMS\View\Admin\DashboardHtmlView; +use Kumwe\CMS\View\Admin\ItemsHtmlView; +use Kumwe\CMS\View\Admin\ItemHtmlView; +use Kumwe\CMS\View\Admin\MenuHtmlView; +use Kumwe\CMS\View\Admin\MenusHtmlView; +use Kumwe\CMS\View\Admin\UserHtmlView; +use Kumwe\CMS\View\Admin\UsersHtmlView; +use Kumwe\CMS\View\Admin\UsergroupHtmlView; +use Kumwe\CMS\View\Admin\UsergroupsHtmlView; +use Kumwe\CMS\Application\AdminApplication; + +use Joomla\Input\Input; +use Joomla\Renderer\RendererInterface; + +/** + * Model View Controller service provider + */ +class AdminMVCProvider implements ServiceProviderInterface +{ + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + */ + public function register(Container $container): void + { + // This service cannot be protected as it is decorated when the debug bar is available + $container->alias(ContainerControllerResolver::class, ControllerResolverInterface::class) + ->share(ControllerResolverInterface::class, [$this, 'getControllerResolverService']); + + // Controllers + $container->alias(DashboardController::class, 'controller.dashboard') + ->share('controller.dashboard', [$this, 'getControllerDashboardService'], true); + + $container->alias(UsersController::class, 'controller.users') + ->share('controller.users', [$this, 'getControllerUsersService'], true); + + $container->alias(UserController::class, 'controller.user') + ->share('controller.user', [$this, 'getControllerUserService'], true); + + $container->alias(UsergroupsController::class, 'controller.usergroups') + ->share('controller.usergroups', [$this, 'getControllerUsergroupsService'], true); + + $container->alias(UsergroupController::class, 'controller.usergroup') + ->share('controller.usergroup', [$this, 'getControllerUsergroupService'], true); + + $container->alias(MenusController::class, 'controller.menus') + ->share('controller.menus', [$this, 'getControllerMenusService'], true); + + $container->alias(MenuController::class, 'controller.menu') + ->share('controller.menu', [$this, 'getControllerMenuService'], true); + + $container->alias(ItemsController::class, 'controller.items') + ->share('controller.items', [$this, 'getControllerItemsService'], true); + + $container->alias(ItemController::class, 'controller.item') + ->share('controller.item', [$this, 'getControllerItemService'], true); + + $container->alias(LoginController::class, 'controller.login') + ->share('controller.login', [$this, 'getControllerLoginService'], true); + + $container->alias(WrongCmsController::class, 'controller.wrong.cms') + ->share('controller.wrong.cms', [$this, 'getControllerWrongCmsService'], true); + + // Models + $container->alias(DashboardModel::class, 'model.dashboard') + ->share('model.dashboard', [$this, 'getModelDashboardService'], true); + + $container->alias(UsersModel::class, 'model.users') + ->share('model.users', [$this, 'getModelUsersService'], true); + + $container->alias(UserModel::class, 'model.user') + ->share('model.user', [$this, 'getModelUserService'], true); + + $container->alias(UsergroupsModel::class, 'model.usergroups') + ->share('model.usergroups', [$this, 'getModelUsergroupsService'], true); + + $container->alias(UsergroupModel::class, 'model.usergroup') + ->share('model.usergroup', [$this, 'getModelUsergroupService'], true); + + $container->alias(MenusModel::class, 'model.menus') + ->share('model.menus', [$this, 'getModelMenusService'], true); + + $container->alias(MenuModel::class, 'model.menu') + ->share('model.menu', [$this, 'getModelMenuService'], true); + + $container->alias(ItemsModel::class, 'model.items') + ->share('model.items', [$this, 'getModelItemsService'], true); + + $container->alias(ItemModel::class, 'model.item') + ->share('model.item', [$this, 'getModelItemService'], true); + + // Views + $container->alias(DashboardHtmlView::class, 'view.dashboard.html') + ->share('view.dashboard.html', [$this, 'getViewDashboardHtmlService'], true); + + $container->alias(UsersHtmlView::class, 'view.users.html') + ->share('view.users.html', [$this, 'getViewUsersHtmlService'], true); + + $container->alias(UserHtmlView::class, 'view.user.html') + ->share('view.user.html', [$this, 'getViewUserHtmlService'], true); + + $container->alias(UsergroupsHtmlView::class, 'view.usergroups.html') + ->share('view.usergroups.html', [$this, 'getViewUsergroupsHtmlService'], true); + + $container->alias(UsergroupHtmlView::class, 'view.usergroup.html') + ->share('view.usergroup.html', [$this, 'getViewUsergroupHtmlService'], true); + + $container->alias(MenusHtmlView::class, 'view.menus.html') + ->share('view.menus.html', [$this, 'getViewMenusHtmlService'], true); + + $container->alias(MenuHtmlView::class, 'view.menu.html') + ->share('view.menu.html', [$this, 'getViewMenuHtmlService'], true); + + $container->alias(ItemsHtmlView::class, 'view.items.html') + ->share('view.items.html', [$this, 'getViewItemsHtmlService'], true); + + $container->alias(ItemHtmlView::class, 'view.item.html') + ->share('view.item.html', [$this, 'getViewItemHtmlService'], true); + } + + /** + * Get the controller resolver service + * + * @param Container $container The DI container. + * + * @return ControllerResolverInterface + */ + public function getControllerResolverService(Container $container): ControllerResolverInterface + { + return new ContainerControllerResolver($container); + } + + /** + * Get the `controller.login` service + * + * @param Container $container The DI container. + * + * @return LoginController + */ + public function getControllerLoginService(Container $container): LoginController + { + return new LoginController( + $container->get(DashboardHtmlView::class), + $container->get(RendererInterface::class), + $container->get(Input::class), + $container->get(AdminApplication::class) + ); + } + + /** + * Get the `controller.wrong.cms` service + * + * @param Container $container The DI container. + * + * @return WrongCmsController + */ + public function getControllerWrongCmsService(Container $container): WrongCmsController + { + return new WrongCmsController( + $container->get(Input::class), + $container->get(AdminApplication::class) + ); + } + + /** + * Get the `controller.dashboard` service + * + * @param Container $container The DI container. + * + * @return DashboardController + */ + public function getControllerDashboardService(Container $container): DashboardController + { + return new DashboardController( + $container->get(DashboardHtmlView::class), + $container->get(Input::class), + $container->get(AdminApplication::class) + ); + } + + /** + * Get the `model.dashboard` service + * + * @param Container $container The DI container. + * + * @return DashboardModel + */ + public function getModelDashboardService(Container $container): DashboardModel + { + return new DashboardModel($container->get(DatabaseInterface::class)); + } + + /** + * Get the `view.dashboard.html` service + * + * @param Container $container The DI container. + * + * @return DashboardHtmlView + */ + public function getViewDashboardHtmlService(Container $container): DashboardHtmlView + { + return new DashboardHtmlView( + $container->get('model.dashboard'), + $container->get('renderer') + ); + } + + /** + * Get the `controller.users` service + * + * @param Container $container The DI container. + * + * @return UsersController + */ + public function getControllerUsersService(Container $container): UsersController + { + return new UsersController( + $container->get(UsersHtmlView::class), + $container->get(Input::class), + $container->get(AdminApplication::class), + $container->get(UserFactoryInterface::class)->getUser() + ); + } + + /** + * Get the `model.users` service + * + * @param Container $container The DI container. + * + * @return UsersModel + */ + public function getModelUsersService(Container $container): UsersModel + { + return new UsersModel($container->get(DatabaseInterface::class)); + } + + /** + * Get the `view.users.html` service + * + * @param Container $container The DI container. + * + * @return UsersHtmlView + */ + public function getViewUsersHtmlService(Container $container): UsersHtmlView + { + return new UsersHtmlView( + $container->get('model.users'), + $container->get('renderer') + ); + } + + /** + * Get the `controller.user` service + * + * @param Container $container The DI container. + * + * @return UserController + */ + public function getControllerUserService(Container $container): UserController + { + return new UserController( + $container->get(UserModel::class), + $container->get(UserHtmlView::class), + $container->get(Input::class), + $container->get(AdminApplication::class), + $container->get(UserFactoryInterface::class)->getUser() + ); + } + + /** + * Get the `model.user` service + * + * @param Container $container The DI container. + * + * @return UserModel + */ + public function getModelUserService(Container $container): UserModel + { + return new UserModel($container->get(DatabaseInterface::class)); + } + + /** + * Get the `view.user.html` service + * + * @param Container $container The DI container. + * + * @return UserHtmlView + */ + public function getViewUserHtmlService(Container $container): UserHtmlView + { + return new UserHtmlView( + $container->get('model.user'), + $container->get('renderer') + ); + } + + /** + * Get the `controller.usergroups` service + * + * @param Container $container The DI container. + * + * @return UsergroupsController + */ + public function getControllerUsergroupsService(Container $container): UsergroupsController + { + return new UsergroupsController( + $container->get(UsergroupsHtmlView::class), + $container->get(Input::class), + $container->get(AdminApplication::class), + $container->get(UserFactoryInterface::class)->getUser() + ); + } + + /** + * Get the `model.usergroups` service + * + * @param Container $container The DI container. + * + * @return UsergroupsModel + */ + public function getModelUsergroupsService(Container $container): UsergroupsModel + { + return new UsergroupsModel($container->get(DatabaseInterface::class)); + } + + /** + * Get the `view.usergroups.html` service + * + * @param Container $container The DI container. + * + * @return UsergroupsHtmlView + */ + public function getViewUsergroupsHtmlService(Container $container): UsergroupsHtmlView + { + return new UsergroupsHtmlView( + $container->get('model.usergroups'), + $container->get('renderer') + ); + } + + /** + * Get the `controller.usergroup` service + * + * @param Container $container The DI container. + * + * @return UsergroupController + */ + public function getControllerUsergroupService(Container $container): UsergroupController + { + return new UsergroupController( + $container->get(UsergroupModel::class), + $container->get(UsergroupHtmlView::class), + $container->get(Input::class), + $container->get(AdminApplication::class), + $container->get(UserFactoryInterface::class)->getUser() + ); + } + + /** + * Get the `model.usergroup` service + * + * @param Container $container The DI container. + * + * @return UsergroupModel + */ + public function getModelUsergroupService(Container $container): UsergroupModel + { + return new UsergroupModel($container->get(DatabaseInterface::class)); + } + + /** + * Get the `view.usergroup.html` service + * + * @param Container $container The DI container. + * + * @return UsergroupHtmlView + */ + public function getViewUsergroupHtmlService(Container $container): UsergroupHtmlView + { + return new UsergroupHtmlView( + $container->get('model.usergroup'), + $container->get('renderer') + ); + } + + /** + * Get the `controller.menus` service + * + * @param Container $container The DI container. + * + * @return MenusController + */ + public function getControllerMenusService(Container $container): MenusController + { + return new MenusController( + $container->get(MenusHtmlView::class), + $container->get(Input::class), + $container->get(AdminApplication::class), + $container->get(UserFactoryInterface::class)->getUser() + ); + } + + /** + * Get the `model.menus` service + * + * @param Container $container The DI container. + * + * @return MenusModel + */ + public function getModelMenusService(Container $container): MenusModel + { + return new MenusModel($container->get(DatabaseInterface::class)); + } + + /** + * Get the `view.menus.html` service + * + * @param Container $container The DI container. + * + * @return MenusHtmlView + */ + public function getViewMenusHtmlService(Container $container): MenusHtmlView + { + return new MenusHtmlView( + $container->get('model.menus'), + $container->get('renderer') + ); + } + + /** + * Get the `controller.menu` service + * + * @param Container $container The DI container. + * + * @return MenuController + */ + public function getControllerMenuService(Container $container): MenuController + { + return new MenuController( + $container->get(MenuModel::class), + $container->get(MenuHtmlView::class), + $container->get(Input::class), + $container->get(AdminApplication::class), + $container->get(UserFactoryInterface::class)->getUser() + ); + } + + /** + * Get the `model.menu` service + * + * @param Container $container The DI container. + * + * @return MenuModel + */ + public function getModelMenuService(Container $container): MenuModel + { + return new MenuModel($container->get(DatabaseInterface::class)); + } + + /** + * Get the `view.menu.html` service + * + * @param Container $container The DI container. + * + * @return MenuHtmlView + */ + public function getViewMenuHtmlService(Container $container): MenuHtmlView + { + return new MenuHtmlView( + $container->get('model.menu'), + $container->get('renderer') + ); + } + + /** + * Get the `controller.items` service + * + * @param Container $container The DI container. + * + * @return ItemsController + */ + public function getControllerItemsService(Container $container): ItemsController + { + return new ItemsController( + $container->get(ItemsHtmlView::class), + $container->get(Input::class), + $container->get(AdminApplication::class), + $container->get(UserFactoryInterface::class)->getUser() + ); + } + + /** + * Get the `model.items` service + * + * @param Container $container The DI container. + * + * @return ItemsModel + */ + public function getModelItemsService(Container $container): ItemsModel + { + return new ItemsModel($container->get(DatabaseInterface::class)); + } + + /** + * Get the `view.items.html` service + * + * @param Container $container The DI container. + * + * @return ItemsHtmlView + */ + public function getViewItemsHtmlService(Container $container): ItemsHtmlView + { + return new ItemsHtmlView( + $container->get('model.items'), + $container->get('renderer') + ); + } + + /** + * Get the `controller.item` service + * + * @param Container $container The DI container. + * + * @return ItemController + */ + public function getControllerItemService(Container $container): ItemController + { + return new ItemController( + $container->get(ItemModel::class), + $container->get(ItemHtmlView::class), + $container->get(Input::class), + $container->get(AdminApplication::class), + $container->get(UserFactoryInterface::class)->getUser() + ); + } + + /** + * Get the `model.item` service + * + * @param Container $container The DI container. + * + * @return ItemModel + */ + public function getModelItemService(Container $container): ItemModel + { + return new ItemModel($container->get(DatabaseInterface::class)); + } + + /** + * Get the `view.item.html` service + * + * @param Container $container The DI container. + * + * @return ItemHtmlView + */ + public function getViewItemHtmlService(Container $container): ItemHtmlView + { + return new ItemHtmlView( + $container->get('model.item'), + $container->get('renderer') + ); + } +} diff --git a/libraries/src/Service/AdminRouterProvider.php b/libraries/src/Service/AdminRouterProvider.php new file mode 100644 index 0000000..d39f0f2 --- /dev/null +++ b/libraries/src/Service/AdminRouterProvider.php @@ -0,0 +1,108 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Service; + +use Joomla\DI\Container; +use Joomla\DI\ServiceProviderInterface; + +use Kumwe\CMS\Controller\DashboardController; +use Kumwe\CMS\Controller\LoginController; +use Kumwe\CMS\Controller\ItemsController; +use Kumwe\CMS\Controller\ItemController; +use Kumwe\CMS\Controller\MenuController; +use Kumwe\CMS\Controller\MenusController; +use Kumwe\CMS\Controller\UserController; +use Kumwe\CMS\Controller\UsersController; +use Kumwe\CMS\Controller\UserGroupController; +use Kumwe\CMS\Controller\UsergroupsController; + +use Joomla\Router\Router; +use Joomla\Router\RouterInterface; + +/** + * Application service provider + * source: https://github.com/joomla/framework.joomla.org/blob/master/src/Service/ApplicationProvider.php + */ +class AdminRouterProvider implements ServiceProviderInterface +{ + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + */ + public function register(Container $container): void + { + // This service cannot be protected as it is decorated when the debug bar is available + $container->alias(RouterInterface::class, 'application.router') + ->alias(Router::class, 'application.router') + ->share('application.router', [$this, 'getApplicationRouterService']); + } + + /** + * Get the `application.router` service + * + * @param Container $container The DI container. + * + * @return RouterInterface + */ + public function getApplicationRouterService(Container $container): RouterInterface + { + $router = new Router; + + /** + * CMS Admin Panels + **/ + $router->all( + '/index.php/dashboard', + DashboardController::class + ); + $router->get( + '/index.php/users', + UsersController::class + ); + $router->all( + '/index.php/user', + UserController::class + ); + $router->get( + '/index.php/usergroups', + UsergroupsController::class + ); + $router->all( + '/index.php/usergroup', + UsergroupController::class + ); + $router->get( + '/index.php/menus', + MenusController::class + ); + $router->all( + '/index.php/menu', + MenuController::class + ); + $router->get( + '/index.php/items', + ItemsController::class + ); + $router->all( + '/index.php/item', + ItemController::class + ); + $router->get( + '/*', + LoginController::class + ); + + return $router; + } +} diff --git a/libraries/src/Service/AdminTemplatingProvider.php b/libraries/src/Service/AdminTemplatingProvider.php new file mode 100644 index 0000000..f8c8ca0 --- /dev/null +++ b/libraries/src/Service/AdminTemplatingProvider.php @@ -0,0 +1,314 @@ +alias(Packages::class, 'asset.packages') + ->share('asset.packages', [$this, 'getAssetPackagesService'], true); + + $container->alias(RendererInterface::class, 'renderer') + ->alias(TwigRenderer::class, 'renderer') + ->share('renderer', [$this, 'getRendererService'], true); + + $container->alias(CacheInterface::class, 'twig.cache') + ->alias(\Twig_CacheInterface::class, 'twig.cache') + ->share('twig.cache', [$this, 'getTwigCacheService'], true); + + $container->alias(Environment::class, 'twig.environment') + ->alias(\Twig_Environment::class, 'twig.environment') + ->share('twig.environment', [$this, 'getTwigEnvironmentService'], true); + + $container->alias(DebugExtension::class, 'twig.extension.debug') + ->alias(\Twig_Extension_Debug::class, 'twig.extension.debug') + ->share('twig.extension.debug', [$this, 'getTwigExtensionDebugService'], true); + + $container->alias(FrameworkExtension::class, 'twig.extension.framework') + ->share('twig.extension.framework', [$this, 'getTwigExtensionFrameworkService'], true); + + // This service cannot be protected as it is decorated when the debug bar is available + $container->alias(ProfilerExtension::class, 'twig.extension.profiler') + ->alias(\Twig_Extension_Profiler::class, 'twig.extension.profiler') + ->share('twig.extension.profiler', [$this, 'getTwigExtensionProfilerService']); + + $container->alias(LoaderInterface::class, 'twig.loader') + ->alias(\Twig_LoaderInterface::class, 'twig.loader') + ->share('twig.loader', [$this, 'getTwigLoaderService'], true); + + $container->alias(Profile::class, 'twig.profiler.profile') + ->alias(\Twig_Profiler_Profile::class, 'twig.profiler.profile') + ->share('twig.profiler.profile', [$this, 'getTwigProfilerProfileService'], true); + + $container->alias(FrameworkTwigRuntime::class, 'twig.runtime.framework') + ->share('twig.runtime.framework', [$this, 'getTwigRuntimeFrameworkService'], true); + + $container->alias(ContainerRuntimeLoader::class, 'twig.runtime.loader') + ->alias(\Twig_ContainerRuntimeLoader::class, 'twig.runtime.loader') + ->share('twig.runtime.loader', [$this, 'getTwigRuntimeLoaderService'], true); + + $this->tagTwigExtensions($container); + } + + /** + * Get the `asset.packages` service + * + * @param Container $container The DI container. + * + * @return Packages + */ + public function getAssetPackagesService(Container $container): Packages + { + /** @var AbstractApplication $app */ + $app = $container->get(AbstractApplication::class); + + $context = new ApplicationContext($app); + + $mediaPath = $app->get('uri.media.path', '/media/'); + + $defaultPackage = new PathPackage($mediaPath, new EmptyVersionStrategy, $context); + + $mixStrategy = new MixPathPackage( + $defaultPackage, + $mediaPath, + new JsonManifestVersionStrategy(LPATH_ROOT . '/media/mix-manifest.json'), + $context + ); + + return new Packages( + $defaultPackage, + [ + 'mix' => $mixStrategy, + ] + ); + } + + /** + * Get the `renderer` service + * + * @param Container $container The DI container. + * + * @return RendererInterface + */ + public function getRendererService(Container $container): RendererInterface + { + return new TwigRenderer($container->get('twig.environment')); + } + + /** + * Get the `twig.cache` service + * + * @param Container $container The DI container. + * + * @return \Twig_CacheInterface + */ + public function getTwigCacheService(Container $container): \Twig_CacheInterface + { + /** @var \Joomla\Registry\Registry $config */ + $config = $container->get('config'); + + // Pull down the renderer config + $cacheEnabled = $config->get('template.cache.enabled', false); + $cachePath = $config->get('template.cache.path', 'cache/twig'); + $debug = $config->get('template.debug', false); + + if ($debug === false && $cacheEnabled !== false) + { + return new FilesystemCache(LPATH_ROOT . '/' . $cachePath); + } + + return new NullCache; + } + + /** + * Get the `twig.environment` service + * + * @param Container $container The DI container. + * + * @return Environment + */ + public function getTwigEnvironmentService(Container $container): Environment + { + /** @var \Joomla\Registry\Registry $config */ + $config = $container->get('config'); + + $debug = $config->get('template.debug', false); + + $environment = new Environment( + $container->get('twig.loader'), + ['debug' => $debug] + ); + + // Add the runtime loader + $environment->addRuntimeLoader($container->get('twig.runtime.loader')); + + // Set up the environment's caching service + $environment->setCache($container->get('twig.cache')); + + // Add the Twig extensions + $environment->setExtensions($container->getTagged('twig.extension')); + + // Add a global tracking the debug states + $environment->addGlobal('appDebug', $config->get('debug', false)); + $environment->addGlobal('fwDebug', $debug); + + return $environment; + } + + /** + * Get the `twig.extension.debug` service + * + * @param Container $container The DI container. + * + * @return DebugExtension + */ + public function getTwigExtensionDebugService(Container $container): DebugExtension + { + return new DebugExtension; + } + + /** + * Get the `twig.extension.framework` service + * + * @param Container $container The DI container. + * + * @return FrameworkExtension + */ + public function getTwigExtensionFrameworkService(Container $container): FrameworkExtension + { + return new FrameworkExtension; + } + + /** + * Get the `twig.extension.profiler` service + * + * @param Container $container The DI container. + * + * @return ProfilerExtension + */ + public function getTwigExtensionProfilerService(Container $container): ProfilerExtension + { + return new ProfilerExtension($container->get('twig.profiler.profile')); + } + + /** + * Get the `twig.loader` service + * + * @param Container $container The DI container. + * + * @return \Twig_LoaderInterface + */ + public function getTwigLoaderService(Container $container): \Twig_LoaderInterface + { + return new FilesystemLoader([LPATH_TEMPLATES]); + } + + /** + * Get the `twig.profiler.profile` service + * + * @param Container $container The DI container. + * + * @return Profile + */ + public function getTwigProfilerProfileService(Container $container): Profile + { + return new Profile; + } + + /** + * Get the `twig.runtime.framework` service + * + * @param Container $container The DI container. + * + * @return FrameworkTwigRuntime + */ + public function getTwigRuntimeFrameworkService(Container $container): FrameworkTwigRuntime + { + return new FrameworkTwigRuntime( + $container->get(AbstractApplication::class), + $container->get(PreloadManager::class), + LPATH_ROOT . '/media/sri-manifest.json', + $container->get(SessionInterface::class) + ); + } + + /** + * Get the `twig.runtime.loader` service + * + * @param Container $container The DI container. + * + * @return ContainerRuntimeLoader + */ + public function getTwigRuntimeLoaderService(Container $container): ContainerRuntimeLoader + { + return new ContainerRuntimeLoader($container); + } + + /** + * Tag services which are Twig extensions + * + * @param Container $container The DI container. + * + * @return void + */ + private function tagTwigExtensions(Container $container): void + { + /** @var \Joomla\Registry\Registry $config */ + $config = $container->get('config'); + + $debug = $config->get('template.debug', false); + + $twigExtensions = ['twig.extension.framework']; + + if ($debug) + { + $twigExtensions[] = 'twig.extension.debug'; + } + + $container->tag('twig.extension', $twigExtensions); + } +} \ No newline at end of file diff --git a/libraries/src/Service/ConfigurationProvider.php b/libraries/src/Service/ConfigurationProvider.php new file mode 100644 index 0000000..ad4db41 --- /dev/null +++ b/libraries/src/Service/ConfigurationProvider.php @@ -0,0 +1,80 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Service; + +use Joomla\DI\Container; +use Joomla\DI\ServiceProviderInterface; +use Joomla\Registry\Registry; + +/** + * Configuration service provider + */ +class ConfigurationProvider implements ServiceProviderInterface +{ + /** + * Configuration instance + * + * @var Registry + */ + private $config; + + /** + * Constructor. + * + * @param string $file Path to the config file. + * + * @throws \RuntimeException + */ + public function __construct(string $file) + { + // Verify the configuration exists and is readable. + if (!is_readable($file)) + { + throw new \RuntimeException('Configuration file does not exist or is unreadable.'); + } + + // load the class + include_once $file; + $this->config = new Registry(new \LConfig()); + + // Set database values based on config values + $this->config->loadObject( (object) [ + 'database' => [ + 'driver' => $this->config->get('dbtype'), + 'host' => $this->config->get('host'), + 'port' => $this->config->get('port', ''), + 'user' => $this->config->get('user'), + 'password' => $this->config->get('password'), + 'database' => $this->config->get('db'), + 'prefix' => $this->config->get('dbprefix') + ] + ]); + } + + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + */ + public function register(Container $container): void + { + $container->share( + 'config', + function (): Registry + { + return $this->config; + }, + true + ); + } +} diff --git a/libraries/src/Service/EventProvider.php b/libraries/src/Service/EventProvider.php new file mode 100644 index 0000000..70c3955 --- /dev/null +++ b/libraries/src/Service/EventProvider.php @@ -0,0 +1,75 @@ +alias(Dispatcher::class, DispatcherInterface::class) + ->share(DispatcherInterface::class, [$this, 'getDispatcherService']); + + $container->share(ErrorSubscriber::class, [$this, 'getErrorSubscriber'], true) + ->tag('event.subscriber', [ErrorSubscriber::class]); + } + + /** + * Get the DispatcherInterface service + * + * @param Container $container The DI container. + * + * @return DispatcherInterface + */ + public function getDispatcherService(Container $container): DispatcherInterface + { + $dispatcher = new Dispatcher; + + foreach ($container->getTagged('event.subscriber') as $subscriber) + { + $dispatcher->addSubscriber($subscriber); + } + + return $dispatcher; + } + + /** + * Get the ErrorSubscriber service + * + * @param Container $container The DI container. + * + * @return ErrorSubscriber + */ + public function getErrorSubscriber(Container $container): ErrorSubscriber + { + $subscriber = new ErrorSubscriber($container->get(RendererInterface::class)); + $subscriber->setLogger($container->get(LoggerInterface::class)); + + return $subscriber; + } +} diff --git a/libraries/src/Service/HttpProvider.php b/libraries/src/Service/HttpProvider.php new file mode 100644 index 0000000..3fc53bc --- /dev/null +++ b/libraries/src/Service/HttpProvider.php @@ -0,0 +1,64 @@ +alias(Http::class, 'http') + ->share('http', [$this, 'getHttpService'], true); + + $container->alias(HttpFactory::class, 'http.factory') + ->share('http.factory', [$this, 'getHttpFactoryService'], true); + } + + /** + * Get the `http` service + * + * @param Container $container The DI container. + * + * @return Http + */ + public function getHttpService(Container $container): Http + { + /** @var HttpFactory $factory */ + $factory = $container->get('http.factory'); + + return $factory->getHttp(); + } + + /** + * Get the `http.factory` service + * + * @param Container $container The DI container. + * + * @return HttpFactory + */ + public function getHttpFactoryService(Container $container): HttpFactory + { + return new HttpFactory; + } +} \ No newline at end of file diff --git a/libraries/src/Service/InputProvider.php b/libraries/src/Service/InputProvider.php new file mode 100644 index 0000000..f26fa37 --- /dev/null +++ b/libraries/src/Service/InputProvider.php @@ -0,0 +1,46 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Service; + +use Joomla\DI\Container; +use Joomla\DI\ServiceProviderInterface; + +use Joomla\Input\Input; + +/** + * Input service provider + */ +class InputProvider implements ServiceProviderInterface +{ + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + */ + public function register(Container $container): void + { + $container->share(Input::class, [$this, 'getInputClassService'], true); + } + + /** + * Get the Input class service + * + * @param Container $container The DI container. + * + * @return Input + */ + public function getInputClassService(Container $container): Input + { + return new Input($_REQUEST); + } +} diff --git a/libraries/src/Service/LoggingProvider.php b/libraries/src/Service/LoggingProvider.php new file mode 100644 index 0000000..d07915d --- /dev/null +++ b/libraries/src/Service/LoggingProvider.php @@ -0,0 +1,132 @@ +share('monolog.handler.application', [$this, 'getMonologHandlerApplicationService'], true); + + /* + * Monolog Processors + */ + $container->share('monolog.processor.psr3', [$this, 'getMonologProcessorPsr3Service'], true); + $container->share('monolog.processor.web', [$this, 'getMonologProcessorWebService'], true); + + /* + * Application Loggers + */ + $container->share('monolog.logger.application.cli', [$this, 'getMonologLoggerApplicationCliService'], true); + $container->share('monolog.logger.application.web', [$this, 'getMonologLoggerApplicationWebService'], true); + } + + /** + * Get the `monolog.handler.application` service + * + * @param Container $container The DI container. + * + * @return StreamHandler + */ + public function getMonologHandlerApplicationService(Container $container): StreamHandler + { + /** @var \Joomla\Registry\Registry $config */ + $config = $container->get('config'); + + $level = strtoupper($config->get('log.application', $config->get('log.level', 'error'))); + + return new StreamHandler(LPATH_ROOT . '/logs/framework.log', \constant('\\Monolog\\Logger::' . $level)); + } + + /** + * Get the `monolog.logger.application.cli` service + * + * @param Container $container The DI container. + * + * @return Logger + */ + public function getMonologLoggerApplicationCliService(Container $container): Logger + { + return new Logger( + 'Framework', + [ + $container->get('monolog.handler.application'), + ], + [ + $container->get('monolog.processor.psr3'), + ] + ); + } + + /** + * Get the `monolog.logger.application.web` service + * + * @param Container $container The DI container. + * + * @return Logger + */ + public function getMonologLoggerApplicationWebService(Container $container): Logger + { + return new Logger( + 'Framework', + [ + $container->get('monolog.handler.application'), + ], + [ + $container->get('monolog.processor.psr3'), + $container->get('monolog.processor.web'), + ] + ); + } + + /** + * Get the `monolog.processor.psr3` service + * + * @param Container $container The DI container. + * + * @return PsrLogMessageProcessor + */ + public function getMonologProcessorPsr3Service(Container $container): PsrLogMessageProcessor + { + return new PsrLogMessageProcessor; + } + + /** + * Get the `monolog.processor.web` service + * + * @param Container $container The DI container. + * + * @return WebProcessor + */ + public function getMonologProcessorWebService(Container $container): WebProcessor + { + return new WebProcessor; + } +} \ No newline at end of file diff --git a/libraries/src/Service/SessionProvider.php b/libraries/src/Service/SessionProvider.php new file mode 100644 index 0000000..dc5dd20 --- /dev/null +++ b/libraries/src/Service/SessionProvider.php @@ -0,0 +1,126 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Service; + +use Joomla\Database\DatabaseInterface; +use Joomla\DI\Container; +use Joomla\DI\ServiceProviderInterface; +use Joomla\Event\Dispatcher; +use Joomla\Session\Session; +use Joomla\Session\SessionInterface; +use Joomla\Session\Storage\NativeStorage as SessionNativeStorage; +use Joomla\Session\StorageInterface; +use Joomla\Session\Handler\DatabaseHandler as SessionDatabaseHandler; +use Joomla\Session\HandlerInterface; +use Kumwe\CMS\Session\MetadataManager; + +/** + * Session service provider + */ +class SessionProvider implements ServiceProviderInterface +{ + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + */ + public function register(Container $container): void + { + $container->alias(SessionDatabaseHandler::class, HandlerInterface::class) + ->share(HandlerInterface::class, [$this, 'getSessionDatabaseHandlerClassService'], true); + + $container->alias(SessionNativeStorage::class, StorageInterface::class) + ->share(StorageInterface::class, [$this, 'getSessionNativeStorageClassService'], true); + + $container->alias(Session::class, SessionInterface::class) + ->share(SessionInterface::class, [$this, 'getSessionClassService'], true); + + $container->alias(MetadataManager::class, MetadataManager::class) + ->share(MetadataManager::class, [$this, 'getMetadataManagerClassService'], true); + } + + /** + * Get the session metadata manager service + * + * @param Container $container The DI container. + * + * @return MetadataManager + */ + public function getMetadataManagerClassService(Container $container): MetadataManager + { + return new MetadataManager( + $container->get(DatabaseInterface::class) + ); + } + + /** + * Get the `admin.session` service + * + * @param Container $container The DI container. + * + * @return SessionInterface + * @throws \Exception + */ + public function getSessionClassService(Container $container): SessionInterface + { + /** @var \Joomla\Session\Session; $session */ + $session = new Session($container->get(SessionNativeStorage::class), $container->get(Dispatcher::class)); + + // Start session if not already started + if (empty($session->getId())) + { + $session->start(); + } + + return $session; + } + + /** + * Get the Session Database Handler service + * + * @param Container $container The DI container. + * + * @return HandlerInterface + */ + public function getSessionDatabaseHandlerClassService(Container $container): HandlerInterface + { + return new SessionDatabaseHandler($container->get(DatabaseInterface::class)); + } + + /** + * Get the `admin.session` service + * + * @param Container $container The DI container. + * + * @return StorageInterface + */ + public function getSessionNativeStorageClassService(Container $container): StorageInterface + { + /** @var \Joomla\Registry\Registry $config */ + $config = $container->get('config'); + + // Generate a session name. (not secure enough) + $name = md5('kumweAdmin'); + + // Calculate the session lifetime. + $lifetime = $config->get('lifetime') ? $config->get('lifetime') * 60 : 900; + + // Initialize the options for the Session object. + $options = [ + 'name' => $name, + 'expire' => $lifetime + ]; + + return new SessionNativeStorage($container->get(SessionDatabaseHandler::class), $options); + } +} diff --git a/libraries/src/Service/SiteApplicationProvider.php b/libraries/src/Service/SiteApplicationProvider.php new file mode 100644 index 0000000..84099b7 --- /dev/null +++ b/libraries/src/Service/SiteApplicationProvider.php @@ -0,0 +1,265 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Service; + +use Joomla\Application\AbstractWebApplication; +use Joomla\Application\Controller\ContainerControllerResolver; +use Joomla\Application\Controller\ControllerResolverInterface; +use Joomla\Application\Web\WebClient; +use Joomla\Database\DatabaseInterface; +use Joomla\DI\Container; +use Joomla\DI\ServiceProviderInterface; +use Joomla\Event\DispatcherInterface; + +use Kumwe\CMS\Controller\WrongCmsController; +use Kumwe\CMS\Controller\PageController; +use Kumwe\CMS\Model\PageModel; +use Kumwe\CMS\Utilities\StringHelper; +use Kumwe\CMS\View\Site\PageHtmlView; +use Kumwe\CMS\Application\SiteApplication; + +use Joomla\Input\Input; +use Joomla\Router\Route; +use Joomla\Router\Router; +use Joomla\Router\RouterInterface; +use Psr\Log\LoggerInterface; + +/** + * Site Application service provider + * source: https://github.com/joomla/framework.joomla.org/blob/master/src/Service/ApplicationProvider.php + */ +class SiteApplicationProvider implements ServiceProviderInterface +{ + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + */ + public function register(Container $container): void + { + /* + * Application Classes + */ + + // This service cannot be protected as it is decorated when the debug bar is available + $container->alias(SiteApplication::class, AbstractWebApplication::class) + ->share(AbstractWebApplication::class, [$this, 'getSiteApplicationClassService']); + + /* + * Application Helpers and Dependencies + */ + + // This service cannot be protected as it is decorated when the debug bar is available + $container->alias(ContainerControllerResolver::class, ControllerResolverInterface::class) + ->share(ControllerResolverInterface::class, [$this, 'getControllerResolverService']); + + $container->share(WebClient::class, [$this, 'getWebClientService'], true); + + // This service cannot be protected as it is decorated when the debug bar is available + $container->alias(RouterInterface::class, 'application.router') + ->alias(Router::class, 'application.router') + ->share('application.router', [$this, 'getApplicationRouterService']); + + /* + * MVC Layer + */ + + // Controllers + $container->alias(PageController::class, 'controller.page') + ->share('controller.page', [$this, 'getControllerPageService'], true); + + $container->alias(WrongCmsController::class, 'controller.wrong.cms') + ->share('controller.wrong.cms', [$this, 'getControllerWrongCmsService'], true); + + // Models + $container->alias(PageModel::class, 'model.page') + ->share('model.page', [$this, 'getModelPageService'], true); + + // Views + $container->alias(PageHtmlView::class, 'view.page.html') + ->share('view.page.html', [$this, 'getViewPageHtmlService'], true); + } + + /** + * Get the `application.router` service + * + * @param Container $container The DI container. + * + * @return RouterInterface + */ + public function getApplicationRouterService(Container $container): RouterInterface + { + $router = new Router; + + /* + * CMS Admin Panels + */ + $router->get( + '/wp-admin', + WrongCmsController::class + ); + + $router->get( + '/wp-admin/*', + WrongCmsController::class + ); + + $router->get( + 'wp-login.php', + WrongCmsController::class + ); + + /* + * Web routes + */ + $router->addRoute(new Route(['GET', 'HEAD'], '/', PageController::class)); + + // dynamic pages + $pages = '/:root'; + $router->get( + $pages, + PageController::class + ); + // set a mad depth TODO: we should limit the menu depth to 6 or something + $depth = range(1,20); + foreach ($depth as $page) + { + $page = StringHelper::numbers($page); + $pages .= "/:$page"; + $router->get( + $pages, + PageController::class + ); + } + + return $router; + } + + /** + * Get the `controller.page` service + * + * @param Container $container The DI container. + * + * @return PageController + */ + public function getControllerPageService(Container $container): PageController + { + return new PageController( + $container->get(PageHtmlView::class), + $container->get(Input::class), + $container->get(SiteApplication::class) + ); + } + + /** + * Get the `controller.wrong.cms` service + * + * @param Container $container The DI container. + * + * @return WrongCmsController + */ + public function getControllerWrongCmsService(Container $container): WrongCmsController + { + return new WrongCmsController( + $container->get(Input::class), + $container->get(SiteApplication::class) + ); + } + + /** + * Get the `model.page` service + * + * @param Container $container The DI container. + * + * @return PageModel + */ + public function getModelPageService(Container $container): PageModel + { + return new PageModel($container->get(DatabaseInterface::class)); + } + + /** + * Get the WebApplication class service + * + * @param Container $container The DI container. + * + * @return SiteApplication + */ + public function getSiteApplicationClassService(Container $container): SiteApplication + { + $application = new SiteApplication( + $container->get(ControllerResolverInterface::class), + $container->get(RouterInterface::class), + $container->get(Input::class), + $container->get('config'), + $container->get(WebClient::class) + ); + + $application->httpVersion = '2'; + + // Inject extra services + $application->setDispatcher($container->get(DispatcherInterface::class)); + $application->setLogger($container->get(LoggerInterface::class)); + + return $application; + } + + /** + * Get the controller resolver service + * + * @param Container $container The DI container. + * + * @return ControllerResolverInterface + */ + public function getControllerResolverService(Container $container): ControllerResolverInterface + { + return new ContainerControllerResolver($container); + } + + /** + * Get the `view.page.html` service + * + * @param Container $container The DI container. + * + * @return PageHtmlView + */ + public function getViewPageHtmlService(Container $container): PageHtmlView + { + $view = new PageHtmlView( + $container->get('model.page'), + $container->get('renderer') + ); + + $view->setLayout('page.twig'); + + return $view; + } + + /** + * Get the web client service + * + * @param Container $container The DI container. + * + * @return WebClient + */ + public function getWebClientService(Container $container): WebClient + { + /** @var Input $input */ + $input = $container->get(Input::class); + $userAgent = $input->server->getString('HTTP_USER_AGENT', ''); + $acceptEncoding = $input->server->getString('HTTP_ACCEPT_ENCODING', ''); + $acceptLanguage = $input->server->getString('HTTP_ACCEPT_LANGUAGE', ''); + + return new WebClient($userAgent, $acceptEncoding, $acceptLanguage); + } +} diff --git a/libraries/src/Service/SiteTemplatingProvider.php b/libraries/src/Service/SiteTemplatingProvider.php new file mode 100644 index 0000000..3327772 --- /dev/null +++ b/libraries/src/Service/SiteTemplatingProvider.php @@ -0,0 +1,313 @@ +alias(Packages::class, 'asset.packages') + ->share('asset.packages', [$this, 'getAssetPackagesService'], true); + + $container->alias(RendererInterface::class, 'renderer') + ->alias(TwigRenderer::class, 'renderer') + ->share('renderer', [$this, 'getRendererService'], true); + + $container->alias(CacheInterface::class, 'twig.cache') + ->alias(\Twig_CacheInterface::class, 'twig.cache') + ->share('twig.cache', [$this, 'getTwigCacheService'], true); + + $container->alias(Environment::class, 'twig.environment') + ->alias(\Twig_Environment::class, 'twig.environment') + ->share('twig.environment', [$this, 'getTwigEnvironmentService'], true); + + $container->alias(DebugExtension::class, 'twig.extension.debug') + ->alias(\Twig_Extension_Debug::class, 'twig.extension.debug') + ->share('twig.extension.debug', [$this, 'getTwigExtensionDebugService'], true); + + $container->alias(FrameworkExtension::class, 'twig.extension.framework') + ->share('twig.extension.framework', [$this, 'getTwigExtensionFrameworkService'], true); + + // This service cannot be protected as it is decorated when the debug bar is available + $container->alias(ProfilerExtension::class, 'twig.extension.profiler') + ->alias(\Twig_Extension_Profiler::class, 'twig.extension.profiler') + ->share('twig.extension.profiler', [$this, 'getTwigExtensionProfilerService']); + + $container->alias(LoaderInterface::class, 'twig.loader') + ->alias(\Twig_LoaderInterface::class, 'twig.loader') + ->share('twig.loader', [$this, 'getTwigLoaderService'], true); + + $container->alias(Profile::class, 'twig.profiler.profile') + ->alias(\Twig_Profiler_Profile::class, 'twig.profiler.profile') + ->share('twig.profiler.profile', [$this, 'getTwigProfilerProfileService'], true); + + $container->alias(FrameworkTwigRuntime::class, 'twig.runtime.framework') + ->share('twig.runtime.framework', [$this, 'getTwigRuntimeFrameworkService'], true); + + $container->alias(ContainerRuntimeLoader::class, 'twig.runtime.loader') + ->alias(\Twig_ContainerRuntimeLoader::class, 'twig.runtime.loader') + ->share('twig.runtime.loader', [$this, 'getTwigRuntimeLoaderService'], true); + + $this->tagTwigExtensions($container); + } + + /** + * Get the `asset.packages` service + * + * @param Container $container The DI container. + * + * @return Packages + */ + public function getAssetPackagesService(Container $container): Packages + { + /** @var AbstractApplication $app */ + $app = $container->get(AbstractApplication::class); + + $context = new ApplicationContext($app); + + $mediaPath = $app->get('uri.media.path', '/media/'); + + $defaultPackage = new PathPackage($mediaPath, new EmptyVersionStrategy, $context); + + $mixStrategy = new MixPathPackage( + $defaultPackage, + $mediaPath, + new JsonManifestVersionStrategy(LPATH_ROOT . '/media/mix-manifest.json'), + $context + ); + + return new Packages( + $defaultPackage, + [ + 'mix' => $mixStrategy, + ] + ); + } + + /** + * Get the `renderer` service + * + * @param Container $container The DI container. + * + * @return RendererInterface + */ + public function getRendererService(Container $container): RendererInterface + { + return new TwigRenderer($container->get('twig.environment')); + } + + /** + * Get the `twig.cache` service + * + * @param Container $container The DI container. + * + * @return \Twig_CacheInterface + */ + public function getTwigCacheService(Container $container): \Twig_CacheInterface + { + /** @var \Joomla\Registry\Registry $config */ + $config = $container->get('config'); + + // Pull down the renderer config + $cacheEnabled = $config->get('template.cache.enabled', false); + $cachePath = $config->get('template.cache.path', 'cache/twig'); + $debug = $config->get('template.debug', false); + + if ($debug === false && $cacheEnabled !== false) + { + return new FilesystemCache(LPATH_ROOT . '/' . $cachePath); + } + + return new NullCache; + } + + /** + * Get the `twig.environment` service + * + * @param Container $container The DI container. + * + * @return Environment + */ + public function getTwigEnvironmentService(Container $container): Environment + { + /** @var \Joomla\Registry\Registry $config */ + $config = $container->get('config'); + + $debug = $config->get('template.debug', false); + + $environment = new Environment( + $container->get('twig.loader'), + ['debug' => $debug] + ); + + // Add the runtime loader + $environment->addRuntimeLoader($container->get('twig.runtime.loader')); + + // Set up the environment's caching service + $environment->setCache($container->get('twig.cache')); + + // Add the Twig extensions + $environment->setExtensions($container->getTagged('twig.extension')); + + // Add a global tracking the debug states + $environment->addGlobal('appDebug', $config->get('debug', false)); + $environment->addGlobal('fwDebug', $debug); + + return $environment; + } + + /** + * Get the `twig.extension.debug` service + * + * @param Container $container The DI container. + * + * @return DebugExtension + */ + public function getTwigExtensionDebugService(Container $container): DebugExtension + { + return new DebugExtension; + } + + /** + * Get the `twig.extension.framework` service + * + * @param Container $container The DI container. + * + * @return FrameworkExtension + */ + public function getTwigExtensionFrameworkService(Container $container): FrameworkExtension + { + return new FrameworkExtension; + } + + /** + * Get the `twig.extension.profiler` service + * + * @param Container $container The DI container. + * + * @return ProfilerExtension + */ + public function getTwigExtensionProfilerService(Container $container): ProfilerExtension + { + return new ProfilerExtension($container->get('twig.profiler.profile')); + } + + /** + * Get the `twig.loader` service + * + * @param Container $container The DI container. + * + * @return \Twig_LoaderInterface + */ + public function getTwigLoaderService(Container $container): \Twig_LoaderInterface + { + return new FilesystemLoader([LPATH_TEMPLATES]); + } + + /** + * Get the `twig.profiler.profile` service + * + * @param Container $container The DI container. + * + * @return Profile + */ + public function getTwigProfilerProfileService(Container $container): Profile + { + return new Profile; + } + + /** + * Get the `twig.runtime.framework` service + * + * @param Container $container The DI container. + * + * @return FrameworkTwigRuntime + */ + public function getTwigRuntimeFrameworkService(Container $container): FrameworkTwigRuntime + { + return new FrameworkTwigRuntime( + $container->get(AbstractApplication::class), + $container->get(PreloadManager::class), + LPATH_ROOT . '/media/sri-manifest.json' + ); + } + + /** + * Get the `twig.runtime.loader` service + * + * @param Container $container The DI container. + * + * @return ContainerRuntimeLoader + */ + public function getTwigRuntimeLoaderService(Container $container): ContainerRuntimeLoader + { + return new ContainerRuntimeLoader($container); + } + + /** + * Tag services which are Twig extensions + * + * @param Container $container The DI container. + * + * @return void + */ + private function tagTwigExtensions(Container $container): void + { + /** @var \Joomla\Registry\Registry $config */ + $config = $container->get('config'); + + $debug = $config->get('template.debug', false); + + $twigExtensions = ['twig.extension.framework']; + + if ($debug) + { + $twigExtensions[] = 'twig.extension.debug'; + } + + $container->tag('twig.extension', $twigExtensions); + } +} \ No newline at end of file diff --git a/libraries/src/Service/UserProvider.php b/libraries/src/Service/UserProvider.php new file mode 100644 index 0000000..70f63e7 --- /dev/null +++ b/libraries/src/Service/UserProvider.php @@ -0,0 +1,81 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Service; + +use Joomla\Authentication\AuthenticationStrategyInterface; +use Joomla\Authentication\Strategies\DatabaseStrategy; +use Joomla\Input\Input; +use Kumwe\CMS\Session\MetadataManager; +use Kumwe\CMS\User\UserFactory; +use Kumwe\CMS\User\UserFactoryInterface; +use Joomla\Database\DatabaseInterface; +use Joomla\DI\Container; +use Joomla\DI\ServiceProviderInterface; + +/** +* Service provider for the user dependency +* +* @since 1.0.0 +* source: https://github.com/joomla/joomla-cms/blob/4.2-dev/libraries/src/Service/Provider/User.php +*/ +class UserProvider implements ServiceProviderInterface +{ + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->alias('user.factory', UserFactoryInterface::class) + ->alias(UserFactory::class, UserFactoryInterface::class) + ->share(UserFactoryInterface::class, [$this, 'getUserFactoryService'], true); + + $container->alias(DatabaseStrategy::class, AuthenticationStrategyInterface::class) + ->share(AuthenticationStrategyInterface::class, [$this, 'getAuthenticationStrategyService'], true); + } + + /** + * Get the UserFactoryInterface class service + * + * @param Container $container The DI container. + * + * @return UserFactoryInterface + * @throws \Exception + */ + public function getUserFactoryService(Container $container): UserFactoryInterface + { + return new UserFactory( + $container->get(DatabaseInterface::class), + $container->get(AuthenticationStrategyInterface::class), + $container->get(MetadataManager::class) + ); + } + + /** + * Get the AuthenticationStrategyInterface class service + * + * @param Container $container The DI container. + * + * @return AuthenticationStrategyInterface + */ + public function getAuthenticationStrategyService(Container $container): AuthenticationStrategyInterface + { + return new DatabaseStrategy( + $container->get(Input::class), + $container->get(DatabaseInterface::class) + ); + } +} diff --git a/libraries/src/Session/MetadataManager.php b/libraries/src/Session/MetadataManager.php new file mode 100644 index 0000000..e193c2c --- /dev/null +++ b/libraries/src/Session/MetadataManager.php @@ -0,0 +1,326 @@ + + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Session; + +use Kumwe\CMS\User\User; +use Joomla\Database\DatabaseInterface; +use Joomla\Database\Exception\ExecutionFailureException; +use Joomla\Database\ParameterType; +use Joomla\Session\SessionInterface; + +/** + * Manager for optional session metadata. + * + * @since 3.8.6 + * @internal + */ +final class MetadataManager +{ + /** + * Internal variable indicating a session record exists. + * + * @var integer + * @since 4.0.0 + * @note Once PHP 7.1 is the minimum supported version this should become a private constant + */ + private static $sessionRecordExists = 1; + + /** + * Internal variable indicating a session record does not exist. + * + * @var integer + * @since 4.0.0 + * @note Once PHP 7.1 is the minimum supported version this should become a private constant + */ + private static $sessionRecordDoesNotExist = 0; + + /** + * Internal variable indicating an unknown session record statue. + * + * @var integer + * @since 4.0.0 + * @note Once PHP 7.1 is the minimum supported version this should become a private constant + */ + private static $sessionRecordUnknown = -1; + + /** + * Database driver. + * + * @var DatabaseInterface + * @since 3.8.6 + */ + private $db; + + /** + * MetadataManager constructor. + * + * @param DatabaseInterface $db Database driver. + * + * @since 3.8.6 + */ + public function __construct(DatabaseInterface $db) + { + $this->db = $db; + } + + /** + * Create the metadata record if it does not exist. + * + * @param SessionInterface $session The session to create the metadata record for. + * @param User $user The user to associate with the record. + * + * @return void + * + * @since 3.8.6 + * @throws \RuntimeException + */ + public function createRecordIfNonExisting(SessionInterface $session, User $user) + { + $exists = $this->checkSessionRecordExists($session->getId()); + + // Only touch the database if the record does not already exist + if ($exists !== self::$sessionRecordExists) + { + return; + } + + $this->createSessionRecord($session, $user); + } + + /** + * Create the metadata record if it does not exist. + * + * @param SessionInterface $session The session to create or update the metadata record for. + * @param User $user The user to associate with the record. + * + * @return void + * + * @since 4.0.0 + * @throws \RuntimeException + */ + public function createOrUpdateRecord(SessionInterface $session, User $user) + { + $exists = $this->checkSessionRecordExists($session->getId()); + + // Do not try to touch the database if we can't determine the record state + if ($exists === self::$sessionRecordUnknown) + { + return; + } + + if ($exists === self::$sessionRecordDoesNotExist) + { + $this->createSessionRecord($session, $user); + + return; + } + + $this->updateSessionRecord($session, $user); + } + + /** + * Delete records with a timestamp prior to the given time. + * + * @param integer $time The time records should be deleted if expired before. + * + * @return void + * + * @since 3.8.6 + */ + public function deletePriorTo($time) + { + $query = $this->db->getQuery(true) + ->delete($this->db->quoteName('#__session')) + ->where($this->db->quoteName('time') . ' < :time') + ->bind(':time', $time, ParameterType::INTEGER); + + $this->db->setQuery($query); + + try + { + $this->db->execute(); + } + catch (ExecutionFailureException $exception) + { + // Since garbage collection does not result in a fatal error when run in the session API, we don't allow it here either. + } + } + + /** + * Get session record exists + * + * @param string $sessionId The session ID to check + * + * @return mixed on success value for record presence + * + * @since 1.0.0 + */ + public function getSessionRecord(string $sessionId) + { + $query = $this->db->getQuery(true) + ->select('*') + ->from($this->db->quoteName('#__session')) + ->where($this->db->quoteName('session_id') . ' = :session_id') + ->bind(':session_id', $sessionId) + ->setLimit(1); + + $this->db->setQuery($query); + + return $this->db->loadObject(); + } + + /** + * Check if the session record exists + * + * @param string $sessionId The session ID to check + * + * @return integer Status value for record presence + * + * @since 4.0.0 + */ + private function checkSessionRecordExists(string $sessionId): int + { + $query = $this->db->getQuery(true) + ->select($this->db->quoteName('session_id')) + ->from($this->db->quoteName('#__session')) + ->where($this->db->quoteName('session_id') . ' = :session_id') + ->bind(':session_id', $sessionId) + ->setLimit(1); + + $this->db->setQuery($query); + + try + { + $exists = $this->db->loadResult(); + } + catch (ExecutionFailureException $e) + { + return self::$sessionRecordUnknown; + } + + if ($exists) + { + return self::$sessionRecordExists; + } + + return self::$sessionRecordDoesNotExist; + } + + /** + * Create the session record + * + * @param SessionInterface $session The session to create the metadata record for. + * @param User $user The user to associate with the record. + * + * @return void + * + * @since 4.0.0 + */ + private function createSessionRecord(SessionInterface $session, User $user) + { + $query = $this->db->getQuery(true); + + $time = $session->isNew() ? time() : $session->get('session.timer.start'); + + $columns = [ + $this->db->quoteName('session_id'), + $this->db->quoteName('guest'), + $this->db->quoteName('time'), + $this->db->quoteName('userid'), + $this->db->quoteName('username'), + ]; + + // Add query placeholders + $values = [ + ':session_id', + ':guest', + ':time', + ':user_id', + ':username', + ]; + + // Bind query values + $sessionId = $session->getId(); + $userIsGuest = $user->get('guest', 0); + $userId = $user->get('id', 0); + $username = $user->get('username', ''); + + $query->bind(':session_id', $sessionId) + ->bind(':guest', $userIsGuest, ParameterType::INTEGER) + ->bind(':time', $time) + ->bind(':user_id', $userId, ParameterType::INTEGER) + ->bind(':username', $username); + + $query->insert($this->db->quoteName('#__session')) + ->columns($columns) + ->values(implode(', ', $values)); + + $this->db->setQuery($query); + + try + { + $this->db->execute(); + } + catch (ExecutionFailureException $e) + { + // This failure isn't critical, we can go on without the metadata + } + } + + /** + * Update the session record + * + * @param SessionInterface $session The session to update the metadata record for. + * @param User $user The user to associate with the record. + * + * @return void + * + * @since 4.0.0 + */ + private function updateSessionRecord(SessionInterface $session, User $user) + { + $query = $this->db->getQuery(true); + + $time = time(); + + $setValues = [ + $this->db->quoteName('guest') . ' = :guest', + $this->db->quoteName('time') . ' = :time', + $this->db->quoteName('userid') . ' = :user_id', + $this->db->quoteName('username') . ' = :username', + ]; + + // Bind query values + $sessionId = $session->getId(); + $userIsGuest = $user->get('guest', 0); + $userId = $user->get('id', 0); + $username = $user->get('username', ''); + + $query->bind(':session_id', $sessionId) + ->bind(':guest', $userIsGuest, ParameterType::INTEGER) + ->bind(':time', $time) + ->bind(':user_id', $userId, ParameterType::INTEGER) + ->bind(':username', $username); + + $query->update($this->db->quoteName('#__session')) + ->set($setValues) + ->where($this->db->quoteName('session_id') . ' = :session_id'); + + $this->db->setQuery($query); + + try + { + $this->db->execute(); + } + catch (ExecutionFailureException $e) + { + // This failure isn't critical, we can go on without the metadata + } + } +} \ No newline at end of file diff --git a/libraries/src/String/PunycodeHelper.php b/libraries/src/String/PunycodeHelper.php new file mode 100644 index 0000000..7a19e77 --- /dev/null +++ b/libraries/src/String/PunycodeHelper.php @@ -0,0 +1,260 @@ + + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\String; + +use Algo26\IdnaConvert\ToIdn; +use Algo26\IdnaConvert\ToUnicode; +use Joomla\Uri\UriHelper; + +/** + * Joomla Platform String Punycode Class + * + * Class for handling UTF-8 URLs + * Wraps the Punycode library + * All functions assume the validity of utf-8 URLs. + * + * @since 3.1.2 + */ +abstract class PunycodeHelper +{ + /** + * Transforms a UTF-8 string to a Punycode string + * + * @param string $utfString The UTF-8 string to transform + * + * @return string The punycode string + * + * @since 3.1.2 + */ + public static function toPunycode($utfString) + { + return (new ToIdn)->convert($utfString); + } + + /** + * Transforms a Punycode string to a UTF-8 string + * + * @param string $punycodeString The Punycode string to transform + * + * @return string The UF-8 URL + * + * @since 3.1.2 + */ + public static function fromPunycode($punycodeString) + { + return (new ToUnicode)->convert($punycodeString); + } + + /** + * Transforms a UTF-8 URL to a Punycode URL + * + * @param string $uri The UTF-8 URL to transform + * + * @return string The punycode URL + * + * @since 3.1.2 + */ + public static function urlToPunycode($uri) + { + $parsed = UriHelper::parse_url($uri); + + if (!isset($parsed['host']) || $parsed['host'] == '') + { + // If there is no host we do not need to convert it. + return $uri; + } + + $host = $parsed['host']; + $hostExploded = explode('.', $host); + $newhost = ''; + + foreach ($hostExploded as $hostex) + { + $hostex = static::toPunycode($hostex); + $newhost .= $hostex . '.'; + } + + $newhost = substr($newhost, 0, -1); + $newuri = ''; + + if (!empty($parsed['scheme'])) + { + // Assume :// is required although it is not always. + $newuri .= $parsed['scheme'] . '://'; + } + + if (!empty($newhost)) + { + $newuri .= $newhost; + } + + if (!empty($parsed['port'])) + { + $newuri .= ':' . $parsed['port']; + } + + if (!empty($parsed['path'])) + { + $newuri .= $parsed['path']; + } + + if (!empty($parsed['query'])) + { + $newuri .= '?' . $parsed['query']; + } + + if (!empty($parsed['fragment'])) + { + $newuri .= '#' . $parsed['fragment']; + } + + return $newuri; + } + + /** + * Transforms a Punycode URL to a UTF-8 URL + * + * @param string $uri The Punycode URL to transform + * + * @return string The UTF-8 URL + * + * @since 3.1.2 + */ + public static function urlToUTF8($uri) + { + if (empty($uri)) + { + return ''; + } + + $parsed = UriHelper::parse_url($uri); + + if (!isset($parsed['host']) || $parsed['host'] == '') + { + // If there is no host we do not need to convert it. + return $uri; + } + + $host = $parsed['host']; + $hostExploded = explode('.', $host); + $newhost = ''; + + foreach ($hostExploded as $hostex) + { + $hostex = self::fromPunycode($hostex); + $newhost .= $hostex . '.'; + } + + $newhost = substr($newhost, 0, -1); + $newuri = ''; + + if (!empty($parsed['scheme'])) + { + // Assume :// is required although it is not always. + $newuri .= $parsed['scheme'] . '://'; + } + + if (!empty($newhost)) + { + $newuri .= $newhost; + } + + if (!empty($parsed['port'])) + { + $newuri .= ':' . $parsed['port']; + } + + if (!empty($parsed['path'])) + { + $newuri .= $parsed['path']; + } + + if (!empty($parsed['query'])) + { + $newuri .= '?' . $parsed['query']; + } + + if (!empty($parsed['fragment'])) + { + $newuri .= '#' . $parsed['fragment']; + } + + return $newuri; + } + + /** + * Transforms a UTF-8 email to a Punycode email + * This assumes a valid email address + * + * @param string $email The UTF-8 email to transform + * + * @return string The punycode email + * + * @since 3.1.2 + */ + public static function emailToPunycode($email) + { + $explodedAddress = explode('@', $email); + + // Not addressing UTF-8 user names + $newEmail = $explodedAddress[0]; + + if (!empty($explodedAddress[1])) + { + $domainExploded = explode('.', $explodedAddress[1]); + $newdomain = ''; + + foreach ($domainExploded as $domainex) + { + $domainex = static::toPunycode($domainex); + $newdomain .= $domainex . '.'; + } + + $newdomain = substr($newdomain, 0, -1); + $newEmail = $newEmail . '@' . $newdomain; + } + + return $newEmail; + } + + /** + * Transforms a Punycode email to a UTF-8 email + * This assumes a valid email address + * + * @param string $email The punycode email to transform + * + * @return string The punycode email + * + * @since 3.1.2 + */ + public static function emailToUTF8($email) + { + $explodedAddress = explode('@', $email); + + // Not addressing UTF-8 user names + $newEmail = $explodedAddress[0]; + + if (!empty($explodedAddress[1])) + { + $domainExploded = explode('.', $explodedAddress[1]); + $newdomain = ''; + + foreach ($domainExploded as $domainex) + { + $domainex = static::fromPunycode($domainex); + $newdomain .= $domainex . '.'; + } + + $newdomain = substr($newdomain, 0, -1); + $newEmail = $newEmail . '@' . $newdomain; + } + + return $newEmail; + } +} diff --git a/libraries/src/User/User.php b/libraries/src/User/User.php new file mode 100644 index 0000000..d725849 --- /dev/null +++ b/libraries/src/User/User.php @@ -0,0 +1,179 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\User; + +use Joomla\Registry\Registry; +use Joomla\Database\DatabaseInterface; +use Joomla\Database\ParameterType; +use Kumwe\CMS\Factory; +use Kumwe\CMS\Utilities\StringHelper; +use stdClass; +use Exception; + +/** + * User class. Handles all application interaction with a user + * + * @since 1.0.0 + */ +class User extends Registry +{ + /** + * Constructor activating the default information of the language + * + * @param integer $identifier The primary key of the user to load (optional). + * + * @throws Exception + * @since 1.1.0 + */ + public function __construct($identifier = 0) + { + // Load the user if it exists + if (!empty($identifier)) + { + $data = $this->load($identifier); + // not a guest + $data->guest = 0; + } + else + { + // Initialise guest + $data = (object) ['id' => 0, 'sendEmail' => 0, 'block' => 1, 'aid' => 0, 'guest' => 1, 'groups' => []]; + } + // set the data + parent::__construct($data); + } + + /** + * Method to load a User object by user id number + * + * @param int $id The user id of the user to load + * + * @return stdClass on success + * + * @throws Exception + * @since 1.0.0 + */ + protected function load(int $id): stdClass + { + // Get the database + $db = Factory::getContainer()->get(DatabaseInterface::class); + + // Initialise some variables + $query = $db->getQuery(true) + ->select('u.*') + ->from($db->quoteName('#__users', 'u')) + ->where($db->quoteName('u.id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER) + ->setLimit(1); + $db->setQuery($query); + + $user = $db->loadObject(); + + if ($user instanceof stdClass && isset($user->id)) + { + // start admin details + $user->is_admin = false; + $user->is_admin_groups = []; + // start access + $user->access = new stdClass(); + // Initialise some variables + $query = $db->getQuery(true) + ->select($db->quoteName(array('g.id', 'g.title', 'g.params'))) + ->from($db->quoteName('#__user_usergroup_map', 'm')) + ->join('INNER', $db->quoteName('#__usergroups', 'g'), 'g.id = m.group_id') + ->where($db->quoteName('m.user_id') . ' = :user_id') + ->bind(':user_id', $user->id, ParameterType::INTEGER); + $db->setQuery($query); + + $groups = $db->loadObjectList(); + + if (is_array($groups) && count($groups) > 0) + { + // group bucket of id's + $groups_ids = []; + foreach ($groups as $group) + { + // add group ID + $groups_ids[] = $group->id; + // convert params to object + $params = json_decode($group->params); + // set the access + if (is_array($params) && count($params) > 0) + { + $counter = 0; + $checker = 0; + foreach ($params as $param) + { + // prep the area string + $area = StringHelper::safe($param->area); + // only tart object if not already set + if(empty($user->access->{$area})) + { + // start object + $user->access->{$area} = new stdClass(); + } + // make sure we have upper case + $param->access = strtoupper($param->access); + // full access to area + if ($param->access === 'CRUD') + { + $checker++; + // add the full permissions + $user->access->{$area}->create = true; + $user->access->{$area}->read = true; + $user->access->{$area}->update = true; + $user->access->{$area}->delete = true; + } + else + { + // this user has fewer permissions + // set them one at a time + if (strpos($param->access, 'C') !== false) + { + $user->access->{$area}->create = true; + } + if (strpos($param->access, 'R') !== false) + { + $user->access->{$area}->read = true; + } + if (strpos($param->access, 'U') !== false) + { + $user->access->{$area}->update = true; + } + if (strpos($param->access, 'D') !== false) + { + $user->access->{$area}->delete = true; + } + } + $counter++; + } + // if this group has full access + if ($counter == $checker) + { + // we need to know when this is an admin user + $user->is_admin = true; + // we load the ids to use in user update, so we can prevent + // admin users from removing themselves from the admin group + $user->is_admin_groups[] = $group->id; + } + } + unset($group->params); + } + // keep the group details + $user->groups = $groups; + $user->groups_ids = $groups_ids; + } + + return $user; + } + return (object) ['id' => 0, 'sendEmail' => 0, 'block' => 1, 'aid' => 0, 'guest' => 1, 'groups' => []]; + } +} diff --git a/libraries/src/User/UserFactory.php b/libraries/src/User/UserFactory.php new file mode 100644 index 0000000..245c1dd --- /dev/null +++ b/libraries/src/User/UserFactory.php @@ -0,0 +1,645 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\User; + +use Joomla\Authentication\AuthenticationStrategyInterface; +use Joomla\Database\DatabaseInterface; +use Joomla\Authentication\Password\BCryptHandler; +use Joomla\Filter\InputFilter as InputFilterAlias; +use Joomla\String\StringHelper; +use Kumwe\CMS\Application\AdminApplication; +use Kumwe\CMS\Date\Date; +use Kumwe\CMS\Factory; +use Kumwe\CMS\Filter\InputFilter; +use Kumwe\CMS\Session\MetadataManager; +use Exception; +use RuntimeException; + +/** + * Default factory for creating User objects + * + * @since 1.0.0 + * source: https://github.com/joomla/joomla-cms/blob/4.2-dev/libraries/src/User/UserFactory.php + */ +class UserFactory implements UserFactoryInterface +{ + /** + * The database. + * + * @var DatabaseInterface + */ + private $db; + + /** + * @var AuthenticationStrategyInterface + */ + private $authentication; + + /** + * @var MetadataManager + */ + private $manager; + + /** + * The Admin Application + * + * @var AdminApplication + */ + private $app; + + /** + * The user objects. + * + * @var User[] + */ + private $users = []; + + /** + * @var InputFilter + */ + private $inputFilter; + + /** + * @var string[] + */ + private $userFilter = [ + 'name' => 'STRING', + 'username' => 'USERNAME', + 'email' => 'STRING', + 'password' => 'RAW', + 'password2' => 'RAW' + ]; + + /** + * @var BCryptHandler + */ + private $secure; + + /** + * UserFactory constructor. + * + * @param DatabaseInterface|null $db The database + * @param AuthenticationStrategyInterface|null $authentication + * + * @throws Exception + */ + public function __construct( + DatabaseInterface $db = null, + AuthenticationStrategyInterface $authentication = null, + MetadataManager $manager = null, + BCryptHandler $secure = null) + { + $this->db = ($db) ?: Factory::getApplication()->get(DatabaseInterface::class); + $this->authentication = ($authentication) ?: Factory::getApplication()->get(AuthenticationStrategyInterface::class); + $this->manager = ($manager) ?: Factory::getApplication()->get(MetadataManager::class); + $this->secure = ($secure) ?: new BCryptHandler(); + } + + /** + * Method to get an instance of a user for the given id or user in session. + * + * @param int|null $id The user id + * + * @return User + * + * @throws Exception + * @since 1.0.0 + */ + public function getUser(?int $id = null): User + { + // load the user + if (empty($id)) + { + return $this->loadUserBySession(); + } + return $this->loadUserById($id); + } + + /** + * Method to get an instance of a user for the given id. + * + * @param int $id The id + * + * @return User + * + * @throws Exception + * @since 1.0.0 + */ + public function loadUserById(int $id): User + { + // check if we already called for this user + if (isset($this->users[$id])) + { + return $this->users[$id]; + } + + $this->users[$id] = new User($id); + + return $this->users[$id]; + } + + /** + * Method to get an instance of a user for the session. + * + * @return User + * + * @throws Exception + * @since 1.0.0 + */ + public function loadUserBySession(): User + { + if (!$this->app instanceof AdminApplication) + { + $this->app = Factory::getApplication(); + } + // Grab the current session ID + $sessionId = $this->app->getSession()->getId(); + + // Get the session user ID + $query = $this->db->getQuery(true) + ->select($this->db->quoteName('userid')) + ->from($this->db->quoteName('#__session')) + ->where($this->db->quoteName('session_id') . ' = :sessionid') + ->bind(':sessionid', $sessionId) + ->setLimit(1); + $this->db->setQuery($query); + + return $this->loadUserById((int) $this->db->loadResult()); + } + + /** + * Method to get an instance of a user for the given username. + * + * @param string $username The username + * + * @return User + * + * @throws Exception + * @since 1.0.0 + */ + public function loadUserByUsername(string $username): User + { + // Initialise some variables + $query = $this->db->getQuery(true) + ->select($this->db->quoteName('id')) + ->from($this->db->quoteName('#__users')) + ->where($this->db->quoteName('username') . ' = :username') + ->bind(':username', $username) + ->setLimit(1); + $this->db->setQuery($query); + + return $this->loadUserById((int) $this->db->loadResult()); + } + + /** + * Check if user is active + * + * @return bool + * @throws Exception + */ + public function active(): bool + { + // get the user in the session + $user = $this->loadUserBySession(); + + // get the user ID + $user_id = $user->get('id', 0); + + // check if we have a user (and it's not blocked) + if ($user_id > 0) + { + // 1 == blocked + $blocked = $user->get('block', 1); + // 0 == not blocked + if ($blocked == 0) + { + return true; + } + // check if we have the application + if (!$this->app instanceof AdminApplication) + { + $this->app = Factory::getApplication(); + } + // Get the session + $session = $this->app->getSession(); + // Grab the current session ID (to purge the session) + $sessionId = $session->getId(); + + // Purge the session + $query = $this->db->getQuery(true) + ->delete($this->db->quoteName('#__session')) + ->where($this->db->quoteName('session_id') . ' = :sessionid') + ->bind(':sessionid', $sessionId); + try + { + $this->db->setQuery($query)->execute(); + } + catch (RuntimeException $e) + { + // The old session is already invalidated, don't let this block logging in + } + + // destroy session + $session->destroy(); + } + + // very basic for now.... + return false; + } + + /** + * Check if we have users + * + * @return bool true if we have + */ + public function has(): bool + { + try + { + $found = $this->db->setQuery( + $this->db->getQuery(true) + ->select($this->db->quoteName('id')) + ->from($this->db->quoteName('#__users')) + ->setLimit(1) + )->loadResult(); + } + catch (RuntimeException $exception) + { + return false; + } + + if ($found > 0) + { + return true; + } + return false; + } + + /** + * Check if a user exist based on give key value pair + * + * @param string $value + * @param string $key + * + * @return false|mixed on success return user ID + */ + public function exist(string $value, string $key = 'username') + { + try + { + $id = $this->db->setQuery( + $this->db->getQuery(true) + ->select($this->db->quoteName('id')) + ->from($this->db->quoteName('#__users')) + ->where($this->db->quoteName($key) . ' = ?') + ->bind(1, $value) + )->loadResult(); + } + catch (RuntimeException $exception) + { + return false; + } + + if ($id > 0) + { + return $id; + } + return false; + } + + /** + * Attempt to login user + * + * @return boolean true on success + * + * @throws Exception + * @since 1.0.0 + */ + public function login(): bool + { + // check if we have the application + if (!$this->app instanceof AdminApplication) + { + $this->app = Factory::getApplication(); + } + if (($username = $this->authenticate()) !== false) + { + // If loadUserByUsername returned an error, then pass it back. + $user = $this->loadUserByUsername($username); + + // If loadUserByUsername returned an error, then pass it back. + if ($user instanceof Exception) + { + $this->app->enqueueMessage('Login failure', 'Error'); + + return false; + } + + // check if this user is active + // 1 = blocked + // 0 = active (un blocked) + $blocked = $user->get('block', 1); + if ($blocked == 1) + { + $this->app->enqueueMessage('Login failure, user is blocked. Contact your system administrator.', 'Warning'); + + return false; + } + + return $this->setUserSession($user->toArray()); + } + // set authentication failure message + $this->app->enqueueMessage('Login failure, please try again.', 'Warning'); + + return false; + } + + /** + * Logout user + * + * @return bool + * @throws Exception + */ + public function logout(): bool + { + // check if we have the application + if (!$this->app instanceof AdminApplication) + { + $this->app = Factory::getApplication(); + } + // Get the session + $session = $this->app->getSession(); + // Grab the current session ID + $sessionId = $session->getId(); + + // Purge the session + $query = $this->db->getQuery(true) + ->delete($this->db->quoteName('#__session')) + ->where($this->db->quoteName('session_id') . ' = :sessionid') + ->bind(':sessionid', $sessionId); + try + { + $this->db->setQuery($query)->execute(); + } + catch (RuntimeException $e) + { + // The old session is already invalidated, don't let this block logging in + } + + // close session + $session->close(); + + // very basic for now.... + return true; + } + + /** + * Attempt to great user + * + * @param string|null $name + * @param string|null $username + * @param string|null $email + * @param string|null $password + * @param string|null $password2 + * + * @return boolean true on success + * + * @throws Exception + * @since 1.0.0 + */ + public function create( + string $name = null, + string $username = null, + string $email = null, + string $password = null, + string $password2 = null): bool + { + // check if we have the application + if (!$this->app instanceof AdminApplication) + { + $this->app = Factory::getApplication(); + } + $input = $this->app->getInput(); + + $user = []; + $user['name'] = ($name) ?: $input->getString('name', ''); + $user['username'] = ($username) ?: $input->getString('username', ''); + $user['email'] = ($email) ?: $input->getString('email', ''); + $user['password'] = ($password) ?: $input->getString('password', ''); + $user['password2'] = ($password2) ?: $input->getString('password2', ''); + // normally we don't add newly registered users to the admin group + $add_to_admin_group = false; + + // check if username exist + if (!empty($user['username']) && $this->exist($user['username'])) + { + $this->app->enqueueMessage('Username already exist, try another username.', 'Warning'); + + return false; + } + // check if email exist + if (!empty($user['email']) && $this->exist($user['email'], 'email')) + { + $this->app->enqueueMessage('Email already exist, try another email.', 'Warning'); + + return false; + } + + // load our filter + $this->inputFilter = InputFilter::getInstance( + [], + [], + InputFilterAlias::ONLY_BLOCK_DEFINED_TAGS, + InputFilterAlias::ONLY_BLOCK_DEFINED_ATTRIBUTES + ); + + // check that we have all the values set + $valid = true; + foreach ($user as $key => $detail) + { + // check if its empty + if (empty($detail)) + { + $valid = false; + $this->app->enqueueMessage($key . ' is required', 'error'); + } + // check if its valid + elseif (!$this->valid($key, $detail)) + { + $valid = false; + $this->app->enqueueMessage($key . ' is not valid', 'error'); + } + } + + // check passwords TODO: check that we have a valid email + if (isset($user['password2']) && $user['password'] != $user['password2']) + { + $valid = false; + $this->app->enqueueMessage('Passwords do not match', 'error'); + } + unset ($user['password2']); + + // continue only if valid + if ($valid) + { + // hash the password + $user['password'] = $this->secure->hashPassword($user['password']); + + // set the registration date + $user['registerDate'] = (new Date())->toSql(); + + // set other defaults for now + $user['sendEmail'] = 1; + // all auto created accounts are blocked (and require admin activation) except for first account + if ($this->has()) + { + $user['block'] = 1; + } + else + { + // this is the first account (so it's an admin account) + $user['block'] = 0; + // we must add this user to the admin group + $add_to_admin_group = true; + } + // there are no params at this stage + $user['params'] = ''; + + $insert = (object) $user; + + try + { + // Insert the user + $result = $this->db->insertObject('#__users', $insert, 'id'); + } + catch (RuntimeException $exception) + { + throw new RuntimeException($exception->getMessage(), 404); + } + + // only set session if success and not blocked + if ($result && $user['block'] == 0) + { + // get the user ID + $user['id'] = $this->db->insertid(); + // add to admin + if ($add_to_admin_group) + { + // build the mapped group link to admin + $group = []; + $group['user_id'] = $user['id']; + $group['group_id'] = 1; // admin group ID is normally 1 see /sq/install.sql (line 110) + + $insert = (object) $group; + + try + { + // Insert the user group link + $this->db->insertObject('#__user_usergroup_map', $insert); + } + catch (RuntimeException $exception) + { + // we ignore this... at this point + } + } + return $this->setUserSession($user); + } + elseif ($result) + { + $this->app->enqueueMessage('You account has been created, an administrator will active it shortly.', 'success'); + } + } + return false; + } + + /** + * Attempt to authenticate the username and password pair. + * + * @return string|boolean A string containing a username if authentication is successful, false otherwise. + * + * @since 1.1.0 + */ + private function authenticate() + { + return $this->authentication->authenticate(); + } + + /** + * Attempt validate user input (BASIC) + * + * @param string $key + * @param string $detail + * + * @return bool + */ + private function valid(string $key, string $detail): bool + { + if (isset($this->userFilter[$key])) + { + $valid = $this->inputFilter->clean($detail, $this->userFilter[$key]); + + if (StringHelper::strcmp($valid, $detail) == 0) + { + return true; + } + } + return false; + } + + /** + * Method to add the user to the session + * + * @param array $user + * + * @return bool + * @throws Exception + */ + private function setUserSession(array $user): bool + { + // check if we have the application + if (!$this->app instanceof AdminApplication) + { + $this->app = Factory::getApplication(); + } + // Get the session + $session = $this->app->getSession(); + // Grab the current session ID + $oldSessionId = $session->getId(); + + // Fork the session + $session->fork(); + + // Register the needed session variables + $session->set('user', $user); + + // Purge the old session + $query = $this->db->getQuery(true) + ->delete($this->db->quoteName('#__session')) + ->where($this->db->quoteName('session_id') . ' = :sessionid') + ->bind(':sessionid', $oldSessionId); + try + { + $this->db->setQuery($query)->execute(); + } + catch (RuntimeException $e) + { + // The old session is already invalidated, don't let this block logging in + } + + // creat or update the record for this user session + $this->manager->createOrUpdateRecord($session, $this->loadUserById($user['id'])); + + // show a success message + $this->app->enqueueMessage('Welcome ' . $user['name'] . ', you have successfully lodged in!', 'Success'); + + return true; + } +} diff --git a/libraries/src/User/UserFactoryInterface.php b/libraries/src/User/UserFactoryInterface.php new file mode 100644 index 0000000..22dc84d --- /dev/null +++ b/libraries/src/User/UserFactoryInterface.php @@ -0,0 +1,129 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\User; + +use Exception; + +/** + * Interface defining a factory which can create User objects + * + * @since 1.0.0 + */ +interface UserFactoryInterface +{ + /** + * Method to get an instance of a user for the given id or session. + * + * @param int|null $id The id + * + * @return User + * + * @throws Exception + * @since 1.0.0 + */ + public function getUser(?int $id = null): User; + + /** + * Method to get an instance of a user for the given id. + * + * @param int $id The id + * + * @return User + * + * @since 1.0.0 + */ + public function loadUserById(int $id): User; + + /** + * Method to get an instance of a user for the given username. + * + * @param string $username The username + * + * @return User + * + * @since 1.0.0 + */ + public function loadUserByUsername(string $username): User; + + /** + * Method to get an instance of a user for the session. + * + * @return User + * + * @throws Exception + * @since 1.0.0 + */ + public function loadUserBySession(): User; + + /** + * Check if user is active + * + * @return bool + * @throws Exception + */ + public function active(): bool; + + /** + * Check if we have users + * + * @return bool true if we have + */ + public function has(): bool; + + /** + * Check if a user exist based on give key value pair + * + * @param string $value + * @param string $key + * + * @return false|mixed on success return user ID + */ + public function exist(string $value, string $key = 'username'); + + /** + * Attempt to login user + * + * @return boolean true on success + * + * @throws Exception + * @since 1.0.0 + */ + public function login(): bool; + + /** + * Logout user + * + * @return bool + * @throws Exception + */ + public function logout(): bool; + + /** + * Attempt to great user + * + * @param string|null $name + * @param string|null $username + * @param string|null $email + * @param string|null $password + * @param string|null $password2 + * + * @return boolean true on success + * + * @throws Exception + * @since 1.0.0 + */ + public function create( + string $name = null, + string $username = null, + string $email = null, + string $password = null, + string $password2 = null): bool; +} diff --git a/libraries/src/Utilities/ArrayHelper.php b/libraries/src/Utilities/ArrayHelper.php new file mode 100644 index 0000000..d8daeb6 --- /dev/null +++ b/libraries/src/Utilities/ArrayHelper.php @@ -0,0 +1,79 @@ + + * @git Joomla Component Builder + * @copyright Copyright (C) 2015 Vast Development Method. All rights reserved. + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Utilities; + + +/** + * Some array tricks helper + * + * @since 3.0.9 + */ +abstract class ArrayHelper +{ + /** + * Check if have an array with a length + * + * @input array The array to check + * + * @returns bool/int number of items in array on success + * + * @since 3.0.9 + */ + public static function check($array, $removeEmptyString = false) + { + if (is_array($array) && ($nr = count((array)$array)) > 0) + { + // also make sure the empty strings are removed + if ($removeEmptyString) + { + foreach ($array as $key => $string) + { + if (empty($string)) + { + unset($array[$key]); + } + } + return self::check($array, false); + } + return $nr; + } + return false; + } + + /** + * Merge an array of array's + * + * @input array The arrays you would like to merge + * + * @returns array on success + * + * @since 3.0.9 + */ + public static function merge($arrays) + { + if(self::check($arrays)) + { + $arrayBuket = array(); + foreach ($arrays as $array) + { + if (self::check($array)) + { + $arrayBuket = array_merge($arrayBuket, $array); + } + } + return $arrayBuket; + } + return false; + } + +} + diff --git a/libraries/src/Utilities/StringHelper.php b/libraries/src/Utilities/StringHelper.php new file mode 100644 index 0000000..3436876 --- /dev/null +++ b/libraries/src/Utilities/StringHelper.php @@ -0,0 +1,322 @@ + + * @git Joomla Component Builder + * @copyright Copyright (C) 2015 Vast Development Method. All rights reserved. + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\Utilities; + + +/** + * Some string tricks + * + * @since 3.0.9 + */ +abstract class StringHelper +{ + + /** + * Check if we have a string with a length + * + * @input string $string The string to check + * + * @returns bool true on success + * + * @since 3.0.9 + */ + public static function check($string): bool + { + if (is_string($string) && strlen($string) > 0) + { + return true; + } + + return false; + } + + /** + * Shorten a string + * + * @input string The you would like to shorten + * + * @returns string on success + * + * @since 3.0.9 + */ + public static function shorten($string, $length = 40, $addTip = true) + { + if (self::check($string)) + { + $initial = strlen($string); + $words = preg_split('/([\s\n\r]+)/', $string, null, PREG_SPLIT_DELIM_CAPTURE); + $words_count = count((array)$words); + + $word_length = 0; + $last_word = 0; + for (; $last_word < $words_count; ++$last_word) + { + $word_length += strlen($words[$last_word]); + if ($word_length > $length) + { + break; + } + } + + $newString = implode(array_slice($words, 0, $last_word)); + $final = strlen($newString); + if ($initial != $final && $addTip) + { + $title = self::shorten($string, 400 , false); + return '' . trim($newString) . '...'; + } + elseif ($initial != $final && !$addTip) + { + return trim($newString) . '...'; + } + } + return $string; + } + + /** + * Making strings safe (various ways) + * + * @input string The you would like to make safe + * + * @returns string on success + * + * @since 3.0.9 + */ + public static function safe($string, $type = 'L', $spacer = '_', $replaceNumbers = true, $keepOnlyCharacters = true) + { + if ($replaceNumbers === true) + { + // remove all numbers and replace with english text version (works well only up to millions) + $string = self::numbers($string); + } + // 0nly continue if we have a string + if (self::check($string)) + { + // create file name without the extension that is safe + if ($type === 'filename') + { + // make sure VDM is not in the string + $string = str_replace('VDM', 'vDm', $string); + // Remove anything which isn't a word, whitespace, number + // or any of the following caracters -_() + // If you don't need to handle multi-byte characters + // you can use preg_replace rather than mb_ereg_replace + // Thanks @Łukasz Rysiak! + // $string = mb_ereg_replace("([^\w\s\d\-_\(\)])", '', $string); + $string = preg_replace("([^\w\s\d\-_\(\)])", '', $string); + + // http://stackoverflow.com/a/2021729/1429677 + return preg_replace('/\s+/', ' ', $string); + } + // remove all other characters + $string = trim($string); + $string = preg_replace('/'.$spacer.'+/', ' ', $string); + $string = preg_replace('/\s+/', ' ', $string); + // remove all and keep only characters + if ($keepOnlyCharacters) + { + $string = preg_replace("/[^A-Za-z ]/", '', $string); + } + // keep both numbers and characters + else + { + $string = preg_replace("/[^A-Za-z0-9 ]/", '', $string); + } + // select final adaptations + if ($type === 'L' || $type === 'strtolower') + { + // replace white space with underscore + $string = preg_replace('/\s+/', $spacer, $string); + // default is to return lower + return strtolower($string); + } + elseif ($type === 'W') + { + // return a string with all first letter of each word uppercase(no underscore) + return ucwords(strtolower($string)); + } + elseif ($type === 'w' || $type === 'word') + { + // return a string with all lowercase(no underscore) + return strtolower($string); + } + elseif ($type === 'Ww' || $type === 'Word') + { + // return a string with first letter of the first word uppercase and all the rest lowercase(no underscore) + return ucfirst(strtolower($string)); + } + elseif ($type === 'WW' || $type === 'WORD') + { + // return a string with all the uppercase(no underscore) + return strtoupper($string); + } + elseif ($type === 'U' || $type === 'strtoupper') + { + // replace white space with underscore + $string = preg_replace('/\s+/', $spacer, $string); + // return all upper + return strtoupper($string); + } + elseif ($type === 'F' || $type === 'ucfirst') + { + // replace white space with underscore + $string = preg_replace('/\s+/', $spacer, $string); + // return with first character to upper + return ucfirst(strtolower($string)); + } + elseif ($type === 'cA' || $type === 'cAmel' || $type === 'camelcase') + { + // convert all words to first letter uppercase + $string = ucwords(strtolower($string)); + // remove white space + $string = preg_replace('/\s+/', '', $string); + // now return first letter lowercase + return lcfirst($string); + } + // return string + return $string; + } + // not a string + return ''; + } + + /** + * Convert all int in a string to an English word string + * + * @input an string with numbers + * + * @returns a string + * + * @since 3.0.9 + */ + public static function numbers($string) + { + // set numbers array + $numbers = array(); + + // first get all numbers + preg_match_all('!\d+!', $string, $numbers); + + // check if we have any numbers + if (isset($numbers[0]) && ArrayHelper::check($numbers[0])) + { + foreach ($numbers[0] as $number) + { + $searchReplace[$number] = self::number((int)$number); + } + + // now replace numbers in string + $string = str_replace(array_keys($searchReplace), array_values($searchReplace), $string); + + // check if we missed any, strange if we did. + return self::numbers($string); + } + + // return the string with no numbers remaining. + return $string; + } + + /** + * Convert an integer into an English word string + * Thanks to Tom Nicholson + * + * @input an int + * @returns a string + * + * @since 3.0.9 + */ + public static function number($x) + { + $nwords = array( "zero", "one", "two", "three", "four", "five", "six", "seven", + "eight", "nine", "ten", "eleven", "twelve", "thirteen", + "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", + "nineteen", "twenty", 30 => "thirty", 40 => "forty", + 50 => "fifty", 60 => "sixty", 70 => "seventy", 80 => "eighty", + 90 => "ninety" ); + + if(!is_numeric($x)) + { + $w = $x; + } + elseif(fmod($x, 1) != 0) + { + $w = $x; + } + else + { + if($x < 0) + { + $w = 'minus '; + $x = -$x; + } + else + { + $w = ''; + // ... now $x is a non-negative integer. + } + + if($x < 21) // 0 to 20 + { + $w .= $nwords[$x]; + } + elseif($x < 100) // 21 to 99 + { + $w .= $nwords[10 * floor($x/10)]; + $r = fmod($x, 10); + if($r > 0) + { + $w .= ' ' . $nwords[$r]; + } + } + elseif($x < 1000) // 100 to 999 + { + $w .= $nwords[floor($x/100)] .' hundred'; + $r = fmod($x, 100); + if($r > 0) + { + $w .= ' and '. self::number($r); + } + } + elseif($x < 1000000) // 1000 to 999999 + { + $w .= self::number(floor($x/1000)) .' thousand'; + $r = fmod($x, 1000); + if($r > 0) + { + $w .= ' '; + if($r < 100) + { + $w .= 'and '; + } + $w .= self::number($r); + } + } + else // millions + { + $w .= self::number(floor($x/1000000)) .' million'; + $r = fmod($x, 1000000); + if($r > 0) + { + $w .= ' '; + if($r < 100) + { + $w .= 'and '; + } + $w .= self::number($r); + } + } + } + return $w; + } + +} + diff --git a/libraries/src/View/Admin/DashboardHtmlView.php b/libraries/src/View/Admin/DashboardHtmlView.php new file mode 100644 index 0000000..7a775d7 --- /dev/null +++ b/libraries/src/View/Admin/DashboardHtmlView.php @@ -0,0 +1,83 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\View\Admin; + +use Kumwe\CMS\Model\DashboardModel; +use Joomla\Renderer\RendererInterface; +use Joomla\View\HtmlView; + +/** + * HTML view class for the application + */ +class DashboardHtmlView extends HtmlView +{ + /** + * The id + * + * @var int + */ + private $id; + + /** + * The page model object. + * + * @var DashboardModel + */ + private $model; + + /** + * Instantiate the view. + * + * @param DashboardModel $model The model object. + * @param RendererInterface $renderer The renderer object. + */ + public function __construct(DashboardModel $model, RendererInterface $renderer) + { + parent::__construct($renderer); + + $this->model = $model; + } + + /** + * Method to render the view + * + * @return string The rendered view + */ + public function render(): string + { + $this->setData(['page' => $this->id]); + return parent::render(); + } + + /** + * Set the active view + * + * @param string $name The active page name + * + * @return void + */ + public function setActiveDashboard(string $name): void + { + $this->setLayout($this->model->getDashboard($name)); + } + + /** + * Set the active id + * + * @param int $id The active id + * + * @return void + */ + public function setActiveId(int $id): void + { + $this->id = $id; + } +} diff --git a/libraries/src/View/Admin/ItemHtmlView.php b/libraries/src/View/Admin/ItemHtmlView.php new file mode 100644 index 0000000..f02d951 --- /dev/null +++ b/libraries/src/View/Admin/ItemHtmlView.php @@ -0,0 +1,84 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\View\Admin; + +use Kumwe\CMS\Model\ItemModel; +use Joomla\Renderer\RendererInterface; +use Joomla\View\HtmlView; + +/** + * HTML view class for the application + */ +class ItemHtmlView extends HtmlView +{ + /** + * The id + * + * @var int + */ + private $id; + + /** + * The model object. + * + * @var ItemModel + */ + private $model; + + /** + * Instantiate the view. + * + * @param ItemModel $model The model object. + * @param RendererInterface $renderer The renderer object. + */ + public function __construct(ItemModel $model, RendererInterface $renderer) + { + parent::__construct($renderer); + + $this->model = $model; + } + + /** + * Method to render the view + * + * @return string The rendered view + * @throws \Exception + */ + public function render(): string + { + $this->setData(['form' => $this->model->getItem($this->id)]); + return parent::render(); + } + + /** + * Set the active view + * + * @param string $name The active view name + * + * @return void + */ + public function setActiveView(string $name): void + { + $this->setLayout($this->model->setLayout($name)); + } + + /** + * Set the active id + * + * @param int $id The active id + * + * @return void + */ + public function setActiveId(int $id): void + { + $this->id = $id; + } +} diff --git a/libraries/src/View/Admin/ItemsHtmlView.php b/libraries/src/View/Admin/ItemsHtmlView.php new file mode 100644 index 0000000..d482230 --- /dev/null +++ b/libraries/src/View/Admin/ItemsHtmlView.php @@ -0,0 +1,64 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\View\Admin; + +use Kumwe\CMS\Model\ItemsModel; +use Joomla\Renderer\RendererInterface; +use Joomla\View\HtmlView; + +/** + * HTML view class for the application + */ +class ItemsHtmlView extends HtmlView +{ + /** + * The model object. + * + * @var ItemsModel + */ + private $model; + + /** + * Instantiate the view. + * + * @param ItemsModel $model The model object. + * @param RendererInterface $renderer The renderer object. + */ + public function __construct(ItemsModel $model, RendererInterface $renderer) + { + parent::__construct($renderer); + + $this->model = $model; + } + + /** + * Method to render the view + * + * @return string The rendered view + */ + public function render(): string + { + $this->setData(['list' => $this->model->getItems()]); + return parent::render(); + } + + /** + * Set the active view + * + * @param string $name The active view name + * + * @return void + */ + public function setActiveView(string $name): void + { + $this->setLayout($this->model->setLayout($name)); + } +} diff --git a/libraries/src/View/Admin/MenuHtmlView.php b/libraries/src/View/Admin/MenuHtmlView.php new file mode 100644 index 0000000..d84d8fa --- /dev/null +++ b/libraries/src/View/Admin/MenuHtmlView.php @@ -0,0 +1,95 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\View\Admin; + +use Kumwe\CMS\Model\MenuModel; +use Joomla\Renderer\RendererInterface; +use Joomla\View\HtmlView; +use Kumwe\CMS\Model\Util\MenuInterface; + +/** + * HTML view class for the application + */ +class MenuHtmlView extends HtmlView +{ + /** + * The id + * + * @var int + */ + private $id; + + /** + * The model object. + * + * @var MenuModel + */ + private $model; + + /** + * Instantiate the view. + * + * @param MenuModel $model The model object. + * @param RendererInterface $renderer The renderer object. + */ + public function __construct(MenuModel $model, RendererInterface $renderer) + { + parent::__construct($renderer); + + $this->model = $model; + } + + /** + * Method to render the view + * + * @return string The rendered view + * @throws \Exception + */ + public function render(): string + { + // set the active menus if possible + $menus = []; + if ($this->model instanceof MenuInterface) + { + $menus = $this->model->getMenus($this->id); + } + $this->setData([ + 'form' => $this->model->getItem($this->id), + 'items' => $this->model->getItems(), + 'menus' => $menus + ]); + return parent::render(); + } + + /** + * Set the active view + * + * @param string $name The active view name + * + * @return void + */ + public function setActiveView(string $name): void + { + $this->setLayout($this->model->setLayout($name)); + } + + /** + * Set the active id + * + * @param int $id The active id + * + * @return void + */ + public function setActiveId(int $id): void + { + $this->id = $id; + } +} diff --git a/libraries/src/View/Admin/MenusHtmlView.php b/libraries/src/View/Admin/MenusHtmlView.php new file mode 100644 index 0000000..0dc6273 --- /dev/null +++ b/libraries/src/View/Admin/MenusHtmlView.php @@ -0,0 +1,64 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\View\Admin; + +use Kumwe\CMS\Model\MenusModel; +use Joomla\Renderer\RendererInterface; +use Joomla\View\HtmlView; + +/** + * HTML view class for the application + */ +class MenusHtmlView extends HtmlView +{ + /** + * The model object. + * + * @var MenusModel + */ + private $model; + + /** + * Instantiate the view. + * + * @param MenusModel $model The model object. + * @param RendererInterface $renderer The renderer object. + */ + public function __construct(MenusModel $model, RendererInterface $renderer) + { + parent::__construct($renderer); + + $this->model = $model; + } + + /** + * Method to render the view + * + * @return string The rendered view + */ + public function render(): string + { + $this->setData(['list' => $this->model->getItems()]); + return parent::render(); + } + + /** + * Set the active id + * + * @param string $name The active id + * + * @return void + */ + public function setActiveView(string $name): void + { + $this->setLayout($this->model->setLayout($name)); + } +} diff --git a/libraries/src/View/Admin/UserHtmlView.php b/libraries/src/View/Admin/UserHtmlView.php new file mode 100644 index 0000000..e3e8492 --- /dev/null +++ b/libraries/src/View/Admin/UserHtmlView.php @@ -0,0 +1,87 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\View\Admin; + +use Kumwe\CMS\Model\UserModel; +use Joomla\Renderer\RendererInterface; +use Joomla\View\HtmlView; + +/** + * HTML view class for the application + */ +class UserHtmlView extends HtmlView +{ + /** + * The id + * + * @var int + */ + private $id; + + /** + * The model object. + * + * @var UserModel + */ + private $model; + + /** + * Instantiate the view. + * + * @param UserModel $model The model object. + * @param RendererInterface $renderer The renderer object. + */ + public function __construct(UserModel $model, RendererInterface $renderer) + { + parent::__construct($renderer); + + $this->model = $model; + } + + /** + * Method to render the view + * + * @return string The rendered view + * @throws \Exception + */ + public function render(): string + { + $this->setData([ + 'form' => $this->model->getItem($this->id), + 'groups' => $this->model->getUsergroups() + ]); + return parent::render(); + } + + /** + * Set the active view + * + * @param string $name The active view name + * + * @return void + */ + public function setActiveView(string $name): void + { + $this->setLayout($this->model->setLayout($name)); + } + + /** + * Set the active id + * + * @param int $id The active id + * + * @return void + */ + public function setActiveId(int $id): void + { + $this->id = $id; + } +} diff --git a/libraries/src/View/Admin/UsergroupHtmlView.php b/libraries/src/View/Admin/UsergroupHtmlView.php new file mode 100644 index 0000000..a414f39 --- /dev/null +++ b/libraries/src/View/Admin/UsergroupHtmlView.php @@ -0,0 +1,85 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\View\Admin; + +use Kumwe\CMS\Model\UsergroupModel; +use Joomla\Renderer\RendererInterface; +use Joomla\View\HtmlView; + +/** + * HTML view class for the application + */ +class UsergroupHtmlView extends HtmlView +{ + /** + * The id + * + * @var int + */ + private $id; + + /** + * The model object. + * + * @var UsergroupModel + */ + private $model; + + /** + * Instantiate the view. + * + * @param UsergroupModel $model The model object. + * @param RendererInterface $renderer The renderer object. + */ + public function __construct(UsergroupModel $model, RendererInterface $renderer) + { + parent::__construct($renderer); + + $this->model = $model; + } + + /** + * Method to render the view + * + * @return string The rendered view + */ + public function render(): string + { + $this->setData([ + 'form' => $this->model->getItem($this->id) + ]); + return parent::render(); + } + + /** + * Set the active view + * + * @param string $name The active view name + * + * @return void + */ + public function setActiveView(string $name): void + { + $this->setLayout($this->model->setLayout($name)); + } + + /** + * Set the active id + * + * @param int $id The active id + * + * @return void + */ + public function setActiveId(int $id): void + { + $this->id = $id; + } +} diff --git a/libraries/src/View/Admin/UsergroupsHtmlView.php b/libraries/src/View/Admin/UsergroupsHtmlView.php new file mode 100644 index 0000000..1b9b5f8 --- /dev/null +++ b/libraries/src/View/Admin/UsergroupsHtmlView.php @@ -0,0 +1,64 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\View\Admin; + +use Kumwe\CMS\Model\UsergroupsModel; +use Joomla\Renderer\RendererInterface; +use Joomla\View\HtmlView; + +/** + * HTML view class for the application + */ +class UsergroupsHtmlView extends HtmlView +{ + /** + * The model object. + * + * @var UsergroupsModel + */ + private $model; + + /** + * Instantiate the view. + * + * @param UsergroupsModel $model The model object. + * @param RendererInterface $renderer The renderer object. + */ + public function __construct(UsergroupsModel $model, RendererInterface $renderer) + { + parent::__construct($renderer); + + $this->model = $model; + } + + /** + * Method to render the view + * + * @return string The rendered view + */ + public function render(): string + { + $this->setData(['list' => $this->model->getItems()]); + return parent::render(); + } + + /** + * Set the active view + * + * @param string $name The active view name + * + * @return void + */ + public function setActiveView(string $name): void + { + $this->setLayout($this->model->setLayout($name)); + } +} diff --git a/libraries/src/View/Admin/UsersHtmlView.php b/libraries/src/View/Admin/UsersHtmlView.php new file mode 100644 index 0000000..9012727 --- /dev/null +++ b/libraries/src/View/Admin/UsersHtmlView.php @@ -0,0 +1,64 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\View\Admin; + +use Kumwe\CMS\Model\UsersModel; +use Joomla\Renderer\RendererInterface; +use Joomla\View\HtmlView; + +/** + * HTML view class for the application + */ +class UsersHtmlView extends HtmlView +{ + /** + * The model object. + * + * @var UsersModel + */ + private $model; + + /** + * Instantiate the view. + * + * @param UsersModel $model The model object. + * @param RendererInterface $renderer The renderer object. + */ + public function __construct(UsersModel $model, RendererInterface $renderer) + { + parent::__construct($renderer); + + $this->model = $model; + } + + /** + * Method to render the view + * + * @return string The rendered view + */ + public function render(): string + { + $this->setData(['list' => $this->model->getItems()]); + return parent::render(); + } + + /** + * Set the active view + * + * @param string $name The active view name + * + * @return void + */ + public function setActiveView(string $name): void + { + $this->setLayout($this->model->setLayout($name)); + } +} diff --git a/libraries/src/View/Site/PageHtmlView.php b/libraries/src/View/Site/PageHtmlView.php new file mode 100644 index 0000000..55184ae --- /dev/null +++ b/libraries/src/View/Site/PageHtmlView.php @@ -0,0 +1,73 @@ + + * @git Kumwe CMS + * @license GNU General Public License version 2; see LICENSE.txt + */ + +namespace Kumwe\CMS\View\Site; + +use Kumwe\CMS\Model\PageModel; +use Joomla\Renderer\RendererInterface; +use Joomla\View\HtmlView; + +/** + * HTML view class for the application + */ +class PageHtmlView extends HtmlView +{ + /** + * The active page + * + * @var string + */ + private $page = ''; + + /** + * The model object. + * + * @var PageModel + */ + private $model; + + /** + * Instantiate the view. + * + * @param PageModel $model The model object. + * @param RendererInterface $renderer The renderer object. + */ + public function __construct(PageModel $model, RendererInterface $renderer) + { + parent::__construct($renderer); + + $this->model = $model; + } + + /** + * Method to render the view + * + * @return string The rendered view + */ + public function render() + { + // get and set the data needed in the view + $this->setData($this->model->getData($this->page)); + + return parent::render(); + } + + /** + * Set the active page + * + * @param string $page The active page name + * + * @return void + */ + public function setPage(string $page): void + { + $this->page = $page; + } +} diff --git a/libraries/web.config b/libraries/web.config new file mode 100644 index 0000000..f7a77db --- /dev/null +++ b/libraries/web.config @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/logs/framework.log b/logs/framework.log new file mode 100644 index 0000000..95032b5 --- /dev/null +++ b/logs/framework.log @@ -0,0 +1 @@ +[2022-05-24T06:45:35.013988+00:00] Framework.ERROR: Uncaught Throwable of type Joomla\Database\Exception\PrepareStatementFailureException caught. {"exception":"[object] (Joomla\\Database\\Exception\\PrepareStatementFailureException(code: 1146): Table 'vdm_io.kumwe_session' doesn't exist at /var/www/html/libraries/vendor/joomla/database/src/Mysqli/MysqliStatement.php:141)"} {"url":"/administrator/","ip":"172.22.0.3","http_method":"GET","server":"cms.builder.vdm","referrer":null} diff --git a/logs/index.html b/logs/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/logs/index.html @@ -0,0 +1 @@ + diff --git a/media/css/index.html b/media/css/index.html new file mode 100644 index 0000000..fa6d84e --- /dev/null +++ b/media/css/index.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/media/css/template.css b/media/css/template.css new file mode 100644 index 0000000..e69de29 diff --git a/media/images/index.html b/media/images/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/media/images/index.html @@ -0,0 +1 @@ + diff --git a/media/images/tutorial_thumb.jpg b/media/images/tutorial_thumb.jpg new file mode 100644 index 0000000000000000000000000000000000000000..391109de685bce4f8e468cca4a39aed7cf0f2e88 GIT binary patch literal 163071 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%0A@{eI|2_QqQyr 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 diff --git a/media/js/index.html b/media/js/index.html new file mode 100644 index 0000000..fa6d84e --- /dev/null +++ b/media/js/index.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/media/js/template.js b/media/js/template.js new file mode 100644 index 0000000..e69de29 diff --git a/media/mix-manifest.json b/media/mix-manifest.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/media/mix-manifest.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/media/sri-manifest.json b/media/sri-manifest.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/media/sri-manifest.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/robots.txt.dist b/robots.txt.dist new file mode 100644 index 0000000..97b1244 --- /dev/null +++ b/robots.txt.dist @@ -0,0 +1,28 @@ +# If the Joomla site is installed within a folder +# eg www.example.com/joomla/ then the robots.txt file +# MUST be moved to the site root +# eg www.example.com/robots.txt +# AND the joomla folder name MUST be prefixed to all of the +# paths. +# eg the Disallow rule for the /administrator/ folder MUST +# be changed to read +# Disallow: /joomla/administrator/ +# +# For more information about the robots.txt standard, see: +# https://www.robotstxt.org/orig.html + +User-agent: * +Disallow: /administrator/ +Disallow: /bin/ +Disallow: /cache/ +Disallow: /cli/ +Disallow: /components/ +Disallow: /includes/ +Disallow: /installation/ +Disallow: /language/ +Disallow: /layouts/ +Disallow: /libraries/ +Disallow: /logs/ +Disallow: /modules/ +Disallow: /plugins/ +Disallow: /tmp/ diff --git a/sql/index.html b/sql/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/sql/index.html @@ -0,0 +1 @@ + diff --git a/sql/install.sql b/sql/install.sql new file mode 100644 index 0000000..557984c --- /dev/null +++ b/sql/install.sql @@ -0,0 +1,263 @@ +-- phpMyAdmin SQL Dump +-- version 5.1.1 +-- https://www.phpmyadmin.net/ +-- +-- Host: mariadb_cms:3306 +-- Generation Time: Apr 22, 2022 at 03:25 PM +-- Server version: 10.6.5-MariaDB-1:10.6.5+maria~focal +-- PHP Version: 7.4.20 + +SET + SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; +START TRANSACTION; +SET + time_zone = "+00:00"; + + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT = @@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS = @@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION = @@COLLATION_CONNECTION */; +/*!40101 SET NAMES utf8mb4 */; + +-- +-- Database: `vdm_io` +-- + +-- -------------------------------------------------------- + +-- +-- Table structure for table `kumwe_item` +-- + +CREATE TABLE `kumwe_item` +( + `id` int(10) UNSIGNED NOT NULL, + `title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `alias` varchar(400) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '', + `introtext` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, + `fulltext` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, + `state` tinyint(4) NOT NULL DEFAULT 0, + `created` datetime NOT NULL, + `created_by` int(10) UNSIGNED NOT NULL DEFAULT 0, + `created_by_alias` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `modified` datetime NOT NULL, + `modified_by` int(10) UNSIGNED NOT NULL DEFAULT 0, + `checked_out` int(10) UNSIGNED DEFAULT NULL, + `checked_out_time` datetime DEFAULT NULL, + `publish_up` datetime DEFAULT NULL, + `publish_down` datetime DEFAULT NULL, + `version` int(10) UNSIGNED NOT NULL DEFAULT 1, + `ordering` int(11) NOT NULL DEFAULT 0, + `metakey` text COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `metadesc` text COLLATE utf8mb4_unicode_ci NOT NULL, + `hits` int(10) UNSIGNED NOT NULL DEFAULT 0, + `metadata` text COLLATE utf8mb4_unicode_ci NOT NULL, + `params` text COLLATE utf8mb4_unicode_ci NOT NULL, + `featured` tinyint(3) UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Set if article is featured.' +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_unicode_ci; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `kumwe_menu` +-- + +CREATE TABLE `kumwe_menu` +( + `id` int(11) NOT NULL, + `title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'The display title of the menu item.', + `alias` varchar(400) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'The SEF alias of the menu item.', + `path` varchar(1024) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'The computed path of the menu item based on the alias field.', + `published` tinyint(4) NOT NULL DEFAULT 0 COMMENT 'The published state of the menu link.', + `parent_id` int(10) UNSIGNED NOT NULL DEFAULT 1 COMMENT 'The parent menu item in the menu tree.', + `level` int(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT 'The relative level in the tree.', + `item_id` int(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT 'FK to kumwe_item.id', + `checked_out` int(10) UNSIGNED DEFAULT NULL COMMENT 'FK to kumwe_users.id', + `checked_out_time` datetime DEFAULT NULL COMMENT 'The time the menu item was checked out.', + `params` text COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'JSON encoded data for the menu item.', + `lft` int(11) NOT NULL DEFAULT 0 COMMENT 'Nested set lft.', + `rgt` int(11) NOT NULL DEFAULT 0 COMMENT 'Nested set rgt.', + `home` tinyint(3) UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Indicates if this menu item is the home or default page.', + `publish_up` datetime DEFAULT NULL, + `publish_down` datetime DEFAULT NULL +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_unicode_ci; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `kumwe_session` +-- + +CREATE TABLE `kumwe_session` +( + `session_id` varbinary(192) NOT NULL, + `guest` tinyint(3) UNSIGNED DEFAULT 1, + `time` int(11) NOT NULL DEFAULT 0, + `data` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `userid` int(11) DEFAULT 0, + `username` varchar(150) COLLATE utf8mb4_unicode_ci DEFAULT '' +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_unicode_ci; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `kumwe_usergroups` +-- + +CREATE TABLE `kumwe_usergroups` +( + `id` int(10) UNSIGNED NOT NULL COMMENT 'Primary Key', + `title` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `params` text COLLATE utf8mb4_unicode_ci NOT NULL +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_unicode_ci; + +-- +-- Dumping data for table `kumwe_usergroups` +-- + +INSERT INTO `kumwe_usergroups` (`id`, `title`, `params`) +VALUES (1, 'Administrator', + '[{\"area\":\"user\",\"access\":\"CRUD\"},{\"area\":\"usergroup\",\"access\":\"CRUD\"},{\"area\":\"menu\",\"access\":\"CRUD\"},{\"area\":\"item\",\"access\":\"CRUD\"}]'), + (2, 'Manager', + '[{\"area\":\"user\",\"access\":\"CR\"},{\"area\":\"usergroup\",\"access\":\"\"},{\"area\":\"menu\",\"access\":\"CRU\"},{\"area\":\"item\",\"access\":\"CRU\"}]'), + (3, 'Editor', + '[{\"area\":\"user\",\"access\":\"\"},{\"area\":\"usergroup\",\"access\":\"\"},{\"area\":\"menu\",\"access\":\"\"},{\"area\":\"item\",\"access\":\"CRU\"}]'); + +-- -------------------------------------------------------- + +-- +-- Table structure for table `kumwe_users` +-- + +CREATE TABLE `kumwe_users` +( + `id` int(11) NOT NULL, + `name` varchar(400) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `username` varchar(150) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `email` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `password` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `block` tinyint(4) NOT NULL DEFAULT 0, + `sendEmail` tinyint(4) DEFAULT 0, + `registerDate` datetime NOT NULL, + `lastvisitDate` datetime DEFAULT NULL, + `activation` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `params` text COLLATE utf8mb4_unicode_ci NOT NULL, + `lastResetTime` datetime DEFAULT NULL COMMENT 'Date of last password reset', + `resetCount` int(11) NOT NULL DEFAULT 0 COMMENT 'Count of password resets since lastResetTime', + `requireReset` tinyint(4) NOT NULL DEFAULT 0 COMMENT 'Require user to reset password on next login' +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_unicode_ci; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `kumwe_user_usergroup_map` +-- + +CREATE TABLE `kumwe_user_usergroup_map` +( + `user_id` int(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Foreign Key to kumwe_users.id', + `group_id` int(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Foreign Key to kumwe_usergroups.id' +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_unicode_ci; + +-- +-- Indexes for dumped tables +-- + +-- +-- Indexes for table `kumwe_item` +-- +ALTER TABLE `kumwe_item` + ADD PRIMARY KEY (`id`), + ADD KEY `idx_checkout` (`checked_out`), + ADD KEY `idx_state` (`state`), + ADD KEY `idx_createdby` (`created_by`), + ADD KEY `idx_alias` (`alias`(191)); + +-- +-- Indexes for table `kumwe_menu` +-- +ALTER TABLE `kumwe_menu` + ADD PRIMARY KEY (`id`), + ADD KEY `idx_item` (`item_id`), + ADD KEY `idx_left_right` (`lft`, `rgt`), + ADD KEY `idx_alias` (`alias`(100)), + ADD KEY `idx_path` (`path`(100)); + +-- +-- Indexes for table `kumwe_session` +-- +ALTER TABLE `kumwe_session` + ADD PRIMARY KEY (`session_id`), + ADD KEY `userid` (`userid`), + ADD KEY `time` (`time`), + ADD KEY `guest` (`guest`); + +-- +-- Indexes for table `kumwe_usergroups` +-- +ALTER TABLE `kumwe_usergroups` + ADD PRIMARY KEY (`id`), + ADD UNIQUE KEY `idx_usergroup_title_lookup` (`title`); + +-- +-- Indexes for table `kumwe_users` +-- +ALTER TABLE `kumwe_users` + ADD PRIMARY KEY (`id`), + ADD UNIQUE KEY `idx_username` (`username`), + ADD KEY `idx_name` (`name`(100)), + ADD KEY `idx_block` (`block`), + ADD KEY `email` (`email`); + +-- +-- Indexes for table `kumwe_user_usergroup_map` +-- +ALTER TABLE `kumwe_user_usergroup_map` + ADD PRIMARY KEY (`user_id`, `group_id`); + +-- +-- AUTO_INCREMENT for dumped tables +-- + +-- +-- AUTO_INCREMENT for table `kumwe_item` +-- +ALTER TABLE `kumwe_item` + MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT; + +-- +-- AUTO_INCREMENT for table `kumwe_menu` +-- +ALTER TABLE `kumwe_menu` + MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, + AUTO_INCREMENT = 102; + +-- +-- AUTO_INCREMENT for table `kumwe_usergroups` +-- +ALTER TABLE `kumwe_usergroups` + MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'Primary Key', + AUTO_INCREMENT = 3; + +-- +-- AUTO_INCREMENT for table `kumwe_users` +-- +ALTER TABLE `kumwe_users` + MODIFY `id` int(11) NOT NULL AUTO_INCREMENT; +COMMIT; + +/*!40101 SET CHARACTER_SET_CLIENT = @OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS = @OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION = @OLD_COLLATION_CONNECTION */; \ No newline at end of file diff --git a/templates/admin/dashboard.twig b/templates/admin/dashboard.twig new file mode 100644 index 0000000..c172a04 --- /dev/null +++ b/templates/admin/dashboard.twig @@ -0,0 +1,57 @@ +{% extends "index.twig" %} + +{% block title %}Kumwe Dashboard{% endblock %} + +{% block content %} +
+

Kumwe CMS Dashboard

+ {{ block("messages_queue", "message_queue.twig") }} +
+ {% set no_access = true %} + {% if user('access.user.read', false) %} + {% set no_access = false %} + + {% endif %} + {% if user('access.usergroup.read', false) %} + {% set no_access = false %} +
+
+
+ User Groups +
+
+ {% endif %} + {% if user('access.menu.read', false) %} + {% set no_access = false %} +
+
+
+ Menus +
+
+ {% endif %} + {% if user('access.item.read', false) %} + {% set no_access = false %} +
+
+
+ Items +
+
+ {% endif %} + {% if no_access %} +
+
+
+ No access found to any area, please contact your system administrator! +
+
+ {% endif %} +
+
+{% endblock %} \ No newline at end of file diff --git a/templates/admin/exception.twig b/templates/admin/exception.twig new file mode 100644 index 0000000..e297e37 --- /dev/null +++ b/templates/admin/exception.twig @@ -0,0 +1,29 @@ +{% extends 'index.twig' %} + +{% block bodyNavigation %}{% endblock %} + +{% block title %}Kumwe Error{% endblock %} + +{% block content %} +
+ {% if exception.code in [404, 405] %} +

We Couldn't Find It

+

Sorry, we couldn't find the page matching your request. Try using the navigation to find what you were looking for?

+ {% else %} +

Ouch, That's an Error

+

Well this is embarrassing, seems there was an error processing this request. Perhaps try again? Or file an issue so we can address it.

+ {% endif %} + + {% if appDebug %} +

{{ exception.code|default(0) }} {{ exception|get_class }}

+

{{ exception.message|strip_root_path }}

+ + {% if exception.previous %} + {% set _previous = exception.previous %} +

Previous Exception

+

{{ exception.code|default(0) }} {{ exception|get_class }}

+

{{ exception.message|strip_root_path }}

+ {% endif %} + {% endif %} +
+{% endblock %} \ No newline at end of file diff --git a/templates/admin/footer.twig b/templates/admin/footer.twig new file mode 100644 index 0000000..ee00897 --- /dev/null +++ b/templates/admin/footer.twig @@ -0,0 +1,9 @@ +{% macro load_footer() %} +
+
+
+ Copyright © Generic Company. All Rights Reserved. | Kumwe CMS +
+
+
+{% endmacro %} \ No newline at end of file diff --git a/templates/admin/header.twig b/templates/admin/header.twig new file mode 100644 index 0000000..e8fd7fd --- /dev/null +++ b/templates/admin/header.twig @@ -0,0 +1,45 @@ + + + + + + + + + + {% block headCSSLinks %}{% endblock %} + + + + {% block headJavaScriptLinks %}{% endblock %} + + + + {% block title %}Kumwe! Framework, a framework for developing PHP applications{% endblock %} + + + + + + + + + + + + + + {% block metadata %}{% endblock %} + + + + + + + {% block headCSS %}{% endblock %} + {% block headJavaScript %}{% endblock %} + +{% block body %}{% endblock %} + \ No newline at end of file diff --git a/templates/admin/index.html b/templates/admin/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/templates/admin/index.html @@ -0,0 +1 @@ + diff --git a/templates/admin/index.twig b/templates/admin/index.twig new file mode 100644 index 0000000..d7e5d6d --- /dev/null +++ b/templates/admin/index.twig @@ -0,0 +1,13 @@ +{% extends "header.twig" %} +{% set user = user_array() %} +{% block body %} + +{% block bodyNavigation %}{{ block("bodyNavigation", "nav.twig") }}{% endblock %} +
+ {% block content %}{% endblock %} +
+{% block footerContent %}{% import 'footer.twig' as macros %}{{ macros.load_footer() }}{% endblock %} +{% block bodyJavaScript %}{% endblock %} +{{ url }} + +{% endblock %} \ No newline at end of file diff --git a/templates/admin/item.twig b/templates/admin/item.twig new file mode 100644 index 0000000..6e9255e --- /dev/null +++ b/templates/admin/item.twig @@ -0,0 +1,120 @@ +{% extends "index.twig" %} + +{% block title %}{% if form.id == 0 %}Create{% else %}Edit{% endif %} Item {{ form.title|default('') }}{% endblock %} + +{% block headJavaScriptLinks %} + +{% endblock %} + +{% block content %} +
+ {{ block("messages_queue", "message_queue.twig") }} + {% if form.id == 0 %} +

Create Item

+ {% else %} +

Edit Item {{ form.title|default('') }}

+ {% endif %} +
+ {% if user('access.item.update', false) %} +
+ + Close +
+ {% else %} + Close + {% endif %} +
+ +
+ +
+
+ +
    +
  • +
    + +
    + +
    +
    +
  • +
  • +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    +
    + +
    + +
    +
    +
  • +
  • +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    +
  • +
+ + +
+
+ +{% endblock %} \ No newline at end of file diff --git a/templates/admin/items.twig b/templates/admin/items.twig new file mode 100644 index 0000000..e33d471 --- /dev/null +++ b/templates/admin/items.twig @@ -0,0 +1,79 @@ +{% extends "index.twig" %} + +{% block title %}Items{% endblock %} + +{% block content %} +
+

Items

+ {{ block("messages_queue", "message_queue.twig") }} + {% if user('access.item.create', false) %} + Create + {% endif %} + {% if list %} + + + + + + + + + + + + {% for item in list %} + + + + + + + + {% endfor %} + +
ActionTitleContentStateID
+
+ {% if user('access.item.update', false) %} + Edit + {% else %} + Read + {% endif %} + {% if user('access.item.delete', false) %} + + {% endif %} +
+
{{ item.title|escape('html') }}{% if item.introtext %}{{ shorten_string(item.introtext|striptags) }} {% endif %}{{ shorten_string(item.fulltext|striptags) }} + {% if item.state == 1 %} + + {% elseif item.state == 2 %} + + {% elseif item.state == -1 %} + + {% elseif item.state == 0 %} + + {% else %} + Error + {% endif %} + {{ item.id }}
+ {% else %} +
+ {% if user('access.item.create', false) %} +

There has no items been found, click create to add some.

+ {% else %} +

There has no items been found.

+ {% endif %} +
+ {% endif %} +
+{% if user('access.item.delete', false) %} + +{% endif %} +{% endblock %} \ No newline at end of file diff --git a/templates/admin/login.twig b/templates/admin/login.twig new file mode 100644 index 0000000..55b1dbf --- /dev/null +++ b/templates/admin/login.twig @@ -0,0 +1,20 @@ +{% extends "index.twig" %} + +{% block bodyNavigation %}{% endblock %} + +{% block content %} +
+
+ {{ block("messages_queue", "message_queue.twig") }} +

Login Here

+
+ + + + +
+ Create Account +
+
+{% endblock %} \ No newline at end of file diff --git a/templates/admin/menu.twig b/templates/admin/menu.twig new file mode 100644 index 0000000..db29567 --- /dev/null +++ b/templates/admin/menu.twig @@ -0,0 +1,154 @@ +{% extends "index.twig" %} + +{% block title %}{% if form.id == 0 %}Create{% else %}Edit{% endif %} Menu {{ form.title|default('') }}{% endblock %} + +{% block content %} +
+ {{ block("messages_queue", "message_queue.twig") }} + {% if form.id == 0 %} +

Create Menu

+ {% else %} +

Edit Menu {{ form.title|default('') }}

+ {% endif %} + {% if items %} + + + {% else %} + Create +
+

There has no items been found, click create to add items.

+
+ {% endif %} +
+{% endblock %} \ No newline at end of file diff --git a/templates/admin/menus.twig b/templates/admin/menus.twig new file mode 100644 index 0000000..0b84c4b --- /dev/null +++ b/templates/admin/menus.twig @@ -0,0 +1,90 @@ +{% extends "index.twig" %} + +{% block title %}Menus{% endblock %} + +{% block content %} +
+

Menus

+ {{ block("messages_queue", "message_queue.twig") }} + {% if user('access.menu.create', false) %} + Create + {% endif %} + {% if list %} + + + + + + + + + + + + + {% for item in list %} + + + + + + + + + {% endfor %} + +
ActionTitleItemPathStateID
+
+ {% if user('access.menu.update', false) %} + Edit + {% else %} + Read + {% endif %} + {% if user('access.menu.delete', false) %} + + {% endif %} +
+
{% if item.home == 1 %} {% endif %}{{ item.title|escape('html') }} + {% if user('access.item.update', user('access.item.read', false)) %} + + {{ shorten_string(item.item_title, 10) }} + + {% else %} + {{ shorten_string(item.item_title, 10) }} + {% endif %} + {{ item.path|escape('html') }} + {% if item.published == 1 %} + + {% elseif item.published == 2 %} + + {% elseif item.published == -1 %} + + {% elseif item.published == 0 %} + + {% else %} + Error + {% endif %} + {{ item.id }}
+ {% else %} +
+ {% if user('access.menu.create', false) %} +

There has no menus been found, click create to add some.

+ {% else %} +

There has no menus been found.

+ {% endif %} +
+ {% endif %} +
+{% if user('access.menu.delete', false) %} + +{% endif %} +{% endblock %} \ No newline at end of file diff --git a/templates/admin/message_queue.twig b/templates/admin/message_queue.twig new file mode 100644 index 0000000..9d89bab --- /dev/null +++ b/templates/admin/message_queue.twig @@ -0,0 +1,20 @@ +{% block messages_queue %} +{% set message_queue = message_queue() %} +{% if message_queue|length > 0 %} + {% for messages in message_queue|sort %} + {% if messages.type == 'error' %} + {% set messages_type = 'uk-alert-danger' %} + {% elseif messages.type == 'success' %} + {% set messages_type = 'uk-alert-success' %} + {% elseif messages.type == 'warning' %} + {% set messages_type = 'uk-alert-warning' %} + {% else %} + {% set messages_type = 'uk-alert-primary' %} + {% endif %} +
+ +

{{ messages.message }}

+
+ {% endfor %} +{% endif %} +{% endblock %} \ No newline at end of file diff --git a/templates/admin/nav.twig b/templates/admin/nav.twig new file mode 100644 index 0000000..d8d0f34 --- /dev/null +++ b/templates/admin/nav.twig @@ -0,0 +1,30 @@ +{% block bodyNavigation %} + +{% endblock %} \ No newline at end of file diff --git a/templates/admin/signup.twig b/templates/admin/signup.twig new file mode 100644 index 0000000..85bccde --- /dev/null +++ b/templates/admin/signup.twig @@ -0,0 +1,23 @@ +{% extends "index.twig" %} + +{% block bodyNavigation %}{% endblock %} + +{% block content %} +
+
+ {{ block("messages_queue", "message_queue.twig") }} +

Create Account

+
+ + + + + + + +
+ Login +
+
+{% endblock %} \ No newline at end of file diff --git a/templates/admin/user.twig b/templates/admin/user.twig new file mode 100644 index 0000000..4ca6c07 --- /dev/null +++ b/templates/admin/user.twig @@ -0,0 +1,116 @@ +{% extends "index.twig" %} + +{% block title %}{% if form.id == 0 %}Create{% else %}Edit{% endif %} User {{ form.name|default('') }}{% endblock %} + +{% block content %} +
+ {{ block("messages_queue", "message_queue.twig") }} + {% if form.id == 0 %} +

Create User

+ {% else %} +

Edit User {{ form.name|default('') }}

+ {% endif %} +
+ {% if user('access.user.update', false) %} +
+ + Close +
+ {% else %} + Close + {% endif %} +
+ +
+ +
+
+ +
    +
  • +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    +
  • +
  • +
    +
    + +
    + +
    +
    +
    + +
    + {% if groups %} + {% for group in groups %} +
    + + {% endfor %} + {% endif %} +
    +
    +
    +
  • +
+ + +
+
+{% endblock %} \ No newline at end of file diff --git a/templates/admin/usergroup.twig b/templates/admin/usergroup.twig new file mode 100644 index 0000000..6ffd9c8 --- /dev/null +++ b/templates/admin/usergroup.twig @@ -0,0 +1,46 @@ +{% extends "index.twig" %} + +{% block title %}{% if form.id == 0 %}Create{% else %}Edit {% endif %}{{ form.title|default('') }} User Group{% endblock %} + +{% block content %} +
+ {{ block("messages_queue", "message_queue.twig") }} + {% if form.id == 0 %} +

Create User Group

+ {% else %} +

Edit {{ form.title|default('') }} User Group

+ {% endif %} +
+ {% if user('access.usergroup.update', false) %} +
+ + Close +
+ {% else %} + Close + {% endif %} +
+ +
+ +
+
+

C = Create | R = Read | U = Update | D = Delete

+ Use these keys in this order, or empty for no access, or CRUD for full access, or C for just create, or RU for just read and update. Select any pre/area, have fun! +
+ {% if form.params %} + {% for params in form.params %} +
+ +
+ +
+
+ {% endfor %} + {% endif %} +
+ + +
+
+{% endblock %} \ No newline at end of file diff --git a/templates/admin/usergroups.twig b/templates/admin/usergroups.twig new file mode 100644 index 0000000..79b260d --- /dev/null +++ b/templates/admin/usergroups.twig @@ -0,0 +1,82 @@ +{% extends "index.twig" %} + +{% block title %}User Groups{% endblock %} + +{% block content %} +
+

User Groups

+ {{ block("messages_queue", "message_queue.twig") }} + {% if user('access.usergroup.create', false) %} + Create + {% endif %} + {% if list %} + + + + + + + + + + {% for item in list %} + + + + + + {% endfor %} + +
ActionGroup NameID
+
+ {% if user('access.usergroup.update', false) %} + {% if item.id == 1 %}Read{% else %}Edit{% endif %} + {% else %} + Read + {% endif %} + {% if user('access.usergroup.delete', false) %} + {% if item.id != 1 %} + + {% endif %} + {% endif %} +
+
+ {% if item.params %} +
    +
  • + {{ item.title|escape('html') }} +
    +
      + {% for areas in item.params %} +
    • {{ areas.area|upper }}: {{ areas.access|default('N') }}
    • + {% endfor %} +
    +
    +
  • +
+ {% else %} + {{ item.title|escape('html') }} + {% endif %} +
{{ item.id }}
+ {% else %} +
+ {% if user('access.usergroup.create', false) %} +

There has no user groups been found, click create to add some.

+ {% else %} +

There has no user groups been found.

+ {% endif %} +
+ {% endif %} +
+{% if user('access.usergroup.delete', false) %} + +{% endif %} +{% endblock %} \ No newline at end of file diff --git a/templates/admin/users.twig b/templates/admin/users.twig new file mode 100644 index 0000000..e975e55 --- /dev/null +++ b/templates/admin/users.twig @@ -0,0 +1,112 @@ +{% extends "index.twig" %} + +{% block title %}Users{% endblock %} + +{% block content %} +
+

Users

+ {{ block("messages_queue", "message_queue.twig") }} + {% if user('access.user.create', false) %} + Create + {% endif %} + {% if list %} + + + + + + + + + + + + + {% for item in list %} + + + + + + + + + {% endfor %} + +
ActionNameEmailGroupsStateID
+
+ {% if user('access.user.update', false) %} + Edit + {% else %} + Read + {% endif %} + {% if user('access.user.delete', false) %} + + {% endif %} +
+
{{ item.name|escape('html') }}
username: {{ item.username|escape('html') }}
{{ item.email|escape('html') }}{% if item.groups %} +
    + {% for group in item.groups %} +
  • + {{ group.title|escape('html') }} + +
  • + {% endfor %} +
+ {% else %} + None set + {% endif %} +
+ {% if item.block == 0 %} + + {% else %} + + {% endif %} + {{ item.id }}
+ {% else %} +
+ {% if user('access.user.create', false) %} +

There has no users been found, click create to add some. (this should never happen!!!)

+ {% else %} +

There has no users been found. (this should never happen!!!)

+ {% endif %} +
+ {% endif %} +
+{% if user('access.user.delete', false) %} + +{% endif %} +{% endblock %} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/templates/index.html @@ -0,0 +1 @@ + diff --git a/templates/site/exception.twig b/templates/site/exception.twig new file mode 100644 index 0000000..577e84a --- /dev/null +++ b/templates/site/exception.twig @@ -0,0 +1,27 @@ +{% extends 'index.twig' %} + +{% block title %}Kumwe Error{% endblock %} + +{% block content %} +
+ {% if exception.code in [404, 405] %} +

We Couldn't Find It

+

Sorry, we couldn't find the page matching your request. Try using the navigation to find what you were looking for?

+ {% else %} +

Ouch, That's an Error

+

Well this is embarrassing, seems there was an error processing this request. Perhaps try again? Or file an issue so we can address it.

+ {% endif %} + + {% if appDebug %} +

{{ exception.code|default(0) }} {{ exception|get_class }}

+

{{ exception.message|strip_root_path }}

+ + {% if exception.previous %} + {% set _previous = exception.previous %} +

Previous Exception

+

{{ exception.code|default(0) }} {{ exception|get_class }}

+

{{ exception.message|strip_root_path }}

+ {% endif %} + {% endif %} +
+{% endblock %} \ No newline at end of file diff --git a/templates/site/footer.twig b/templates/site/footer.twig new file mode 100644 index 0000000..ee00897 --- /dev/null +++ b/templates/site/footer.twig @@ -0,0 +1,9 @@ +{% macro load_footer() %} +
+
+
+ Copyright © Generic Company. All Rights Reserved. | Kumwe CMS +
+
+
+{% endmacro %} \ No newline at end of file diff --git a/templates/site/header.twig b/templates/site/header.twig new file mode 100644 index 0000000..c695e29 --- /dev/null +++ b/templates/site/header.twig @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + {% block title %}Kumwe! Framework, a framework for developing PHP applications{% endblock %} + + + + + + + + + + + + + + {% block metadata %}{% endblock %} + + + + + + + {% block headCSS %}{% endblock %} + {% block headJavaScript %}{% endblock %} + +{% block body %}{% endblock %} + \ No newline at end of file diff --git a/templates/site/index.html b/templates/site/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/templates/site/index.html @@ -0,0 +1 @@ + diff --git a/templates/site/index.twig b/templates/site/index.twig new file mode 100644 index 0000000..b844748 --- /dev/null +++ b/templates/site/index.twig @@ -0,0 +1,12 @@ +{% extends "header.twig" %} + +{% block body %} + +{% block bodyNavigation %}{{ block("bodyNavigation", "nav.twig") }}{% endblock %} +
+ {% block content %}{% endblock %} +
+{% block footerContent %}{% import 'footer.twig' as macros %}{{ macros.load_footer() }}{% endblock %} +{% block bodyJavaScript %}{% endblock %} + +{% endblock %} \ No newline at end of file diff --git a/templates/site/nav.twig b/templates/site/nav.twig new file mode 100644 index 0000000..944528a --- /dev/null +++ b/templates/site/nav.twig @@ -0,0 +1,258 @@ +{% block bodyNavigation %} +{% set center = false %} +{% set right = false %} +{% if menus %} + {% for menu in menus %} + {% if menu.parent == 0 and menu.position == 'center' %} + {% set center = true %} + {% elseif menu.parent == 0 and menu.position == 'right' %} + {% set right = true %} + {% endif %} + {% endfor %} +{% endif %} +{% if center and right %} +
+{% endif %} + +{% if center and right %} +
+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/templates/site/page.twig b/templates/site/page.twig new file mode 100644 index 0000000..5722fda --- /dev/null +++ b/templates/site/page.twig @@ -0,0 +1,21 @@ +{% extends "index.twig" %} + +{% block title %}{{ title|escape('html') }}{% endblock %} + +{% block metaDescription %}{% if metadesc == '' %}The Kumwe! Framework provides a structurally sound foundation on which to build applications in PHP, which is easy to adapt and extend. Let's find out more!{% else %}{{ metadesc|escape('html') }}{% endif %}{% endblock %} + +{% block content %} +{% if fulltext == '' %} +
+

We Couldn't Find It

+

Sorry, we couldn't find the page matching your request. Try using the navigation to find what you were looking for?

+
+{% else %} +
+
+

{{ title|escape('html') }}

+ {{ fulltext|raw }} +
+
+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/templates/system/build_incomplete.html b/templates/system/build_incomplete.html new file mode 100644 index 0000000..2a4f1dc --- /dev/null +++ b/templates/system/build_incomplete.html @@ -0,0 +1,34 @@ + + + + + + + Kumwe: Environment Setup Incomplete + + + +
+
+
+

Environment Setup Incomplete

+

It looks like you are trying to run Kumwe! from our git repository. To do so requires you complete a couple of extra steps first.

+

+ 0. Make sure you have composer installed on your system. +
+ 1. In your terminal go to the root folder of your Kumwe website where you will find the composer.json file. +
+ 2. Run the following command composer install to install all PHP packages. +

+
+ +
+
+ + + diff --git a/templates/system/incompatible.html b/templates/system/incompatible.html new file mode 100644 index 0000000..cd7bd6e --- /dev/null +++ b/templates/system/incompatible.html @@ -0,0 +1,27 @@ + + + + + + + Kumwe: unsupported PHP version + + + +
+
+
+

Sorry, your PHP version is not supported

+

Your host needs to use PHP version {{phpversion}} or newer to run this version of Kumwe!

+
+ +
+
+ + + \ No newline at end of file diff --git a/templates/system/index.html b/templates/system/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/templates/system/index.html @@ -0,0 +1 @@ + diff --git a/templates/system/install_notice.html b/templates/system/install_notice.html new file mode 100644 index 0000000..fd071ab --- /dev/null +++ b/templates/system/install_notice.html @@ -0,0 +1,38 @@ + + + + + + + Kumwe: Installation Instructions + + + +
+
+
+

Installation Instructions

+

You need to manually do the following few tasks.

+

+ 1. Import the SQL tables into your database found in /sql/install.sql +
+ 2. Copy the /config.php.example file to /config.php +
+ 3 .Update the /config.php to reflect your CMS details +
+ 4. Copy the /htaccess.txt file to /.htaccess +
+ 5. Remove the /installation folder from you root directory +

+
+ +
+
+ + + \ No newline at end of file diff --git a/tmp/index.html b/tmp/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/tmp/index.html @@ -0,0 +1 @@ + diff --git a/web.config.txt b/web.config.txt new file mode 100644 index 0000000..e15eb64 --- /dev/null +++ b/web.config.txt @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +