From a17a88e9d44e42cef3d996fc66b4dd1134b85def Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=91cze=20Bence?= Date: Fri, 7 Apr 2023 19:32:15 +0200 Subject: [PATCH] initial commit for soko-web --- .devcontainer/devcontainer.json | 14 + .gitignore | 1 + Dockerfile | 4 + Jenkinsfile | 59 + LICENSE.md | 674 ++++ assets/test_views/view_complex.php | 22 + .../view_invalid_multiple_extends.php | 5 + .../view_invalid_multiple_sections_open.php | 6 + .../view_invalid_section_not_open.php | 4 + .../view_invalid_section_without_extends.php | 3 + assets/test_views/view_with_assets.php | 4 + assets/test_views/view_with_extends.php | 5 + assets/test_views/view_without_extends.php | 1 + composer.json | 20 + composer.lock | 2989 +++++++++++++++++ dummy.php | 13 + phpstan.neon | 7 + src/Database/Mysql/Connection.php | 114 + src/Database/Mysql/ResultSet.php | 62 + src/Database/Mysql/Statement.php | 79 + src/Database/Query/Modify.php | 140 + src/Database/Query/Select.php | 445 +++ src/Database/RawExpression.php | 16 + src/Database/Utils.php | 8 + src/Http/Request.php | 102 + src/Http/Response.php | 26 + src/Interfaces/Authentication/IUser.php | 16 + src/Interfaces/Authorization/ISecured.php | 6 + src/Interfaces/Database/IConnection.php | 20 + src/Interfaces/Database/IResultSet.php | 16 + src/Interfaces/Database/IStatement.php | 8 + src/Interfaces/Http/IRequest.php | 18 + src/Interfaces/Http/IResponse.php | 8 + src/Interfaces/Repository/IUserRepository.php | 8 + src/Interfaces/Request/IRequest.php | 20 + src/Interfaces/Request/ISession.php | 12 + src/Interfaces/Response/IContent.php | 12 + src/Interfaces/Response/IRedirect.php | 12 + src/Interfaces/Session/ISessionHandler.php | 9 + src/Mailing/Mail.php | 87 + src/OAuth/GoogleOAuth.php | 56 + src/PersistentData/Model/Model.php | 69 + src/PersistentData/PersistentDataManager.php | 235 ++ src/Request/Request.php | 86 + src/Request/Session.php | 37 + src/Response/ContentBase.php | 22 + src/Response/HtmlContent.php | 35 + src/Response/JsonContent.php | 23 + src/Response/Redirect.php | 41 + src/Routing/Route.php | 73 + src/Routing/RouteCollection.php | 88 + src/Session/DatabaseSessionHandler.php | 104 + src/Util/CaptchaValidator.php | 19 + src/Util/JwtParser.php | 33 + src/View/Linker.php | 152 + src/View/ParsedFragment.php | 48 + src/View/Parser.php | 159 + templates/app.php.tpl | 23 + templates/cli.tpl | 12 + templates/database/database.sql.tpl | 15 + templates/src/Cli/AddUserCommand.php.tpl | 51 + templates/src/Cli/LinkViewCommand.php.tpl | 69 + .../src/Cli/MigrateDatabaseCommand.php.tpl | 122 + .../src/Controller/HomeController.php.tpl | 20 + .../src/PersistentData/Model/User.php.tpl | 133 + templates/views/index.php.tpl | 11 + templates/web.php.tpl | 56 + tests/OAuth/GoogleOAuthTest.php | 86 + tests/PersistentData/Model/ModelTest.php | 93 + tests/Util/JwtParserTest.php | 51 + tests/View/ParserTest.php | 144 + 71 files changed, 7241 insertions(+) create mode 100644 .devcontainer/devcontainer.json create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 Jenkinsfile create mode 100644 LICENSE.md create mode 100644 assets/test_views/view_complex.php create mode 100644 assets/test_views/view_invalid_multiple_extends.php create mode 100644 assets/test_views/view_invalid_multiple_sections_open.php create mode 100644 assets/test_views/view_invalid_section_not_open.php create mode 100644 assets/test_views/view_invalid_section_without_extends.php create mode 100644 assets/test_views/view_with_assets.php create mode 100644 assets/test_views/view_with_extends.php create mode 100644 assets/test_views/view_without_extends.php create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 dummy.php create mode 100644 phpstan.neon create mode 100644 src/Database/Mysql/Connection.php create mode 100644 src/Database/Mysql/ResultSet.php create mode 100644 src/Database/Mysql/Statement.php create mode 100755 src/Database/Query/Modify.php create mode 100644 src/Database/Query/Select.php create mode 100644 src/Database/RawExpression.php create mode 100644 src/Database/Utils.php create mode 100644 src/Http/Request.php create mode 100644 src/Http/Response.php create mode 100644 src/Interfaces/Authentication/IUser.php create mode 100644 src/Interfaces/Authorization/ISecured.php create mode 100644 src/Interfaces/Database/IConnection.php create mode 100644 src/Interfaces/Database/IResultSet.php create mode 100644 src/Interfaces/Database/IStatement.php create mode 100644 src/Interfaces/Http/IRequest.php create mode 100644 src/Interfaces/Http/IResponse.php create mode 100644 src/Interfaces/Repository/IUserRepository.php create mode 100644 src/Interfaces/Request/IRequest.php create mode 100644 src/Interfaces/Request/ISession.php create mode 100644 src/Interfaces/Response/IContent.php create mode 100644 src/Interfaces/Response/IRedirect.php create mode 100644 src/Interfaces/Session/ISessionHandler.php create mode 100644 src/Mailing/Mail.php create mode 100644 src/OAuth/GoogleOAuth.php create mode 100644 src/PersistentData/Model/Model.php create mode 100644 src/PersistentData/PersistentDataManager.php create mode 100644 src/Request/Request.php create mode 100644 src/Request/Session.php create mode 100644 src/Response/ContentBase.php create mode 100644 src/Response/HtmlContent.php create mode 100644 src/Response/JsonContent.php create mode 100644 src/Response/Redirect.php create mode 100644 src/Routing/Route.php create mode 100644 src/Routing/RouteCollection.php create mode 100644 src/Session/DatabaseSessionHandler.php create mode 100644 src/Util/CaptchaValidator.php create mode 100644 src/Util/JwtParser.php create mode 100644 src/View/Linker.php create mode 100644 src/View/ParsedFragment.php create mode 100644 src/View/Parser.php create mode 100644 templates/app.php.tpl create mode 100755 templates/cli.tpl create mode 100644 templates/database/database.sql.tpl create mode 100644 templates/src/Cli/AddUserCommand.php.tpl create mode 100644 templates/src/Cli/LinkViewCommand.php.tpl create mode 100644 templates/src/Cli/MigrateDatabaseCommand.php.tpl create mode 100644 templates/src/Controller/HomeController.php.tpl create mode 100644 templates/src/PersistentData/Model/User.php.tpl create mode 100644 templates/views/index.php.tpl create mode 100644 templates/web.php.tpl create mode 100644 tests/OAuth/GoogleOAuthTest.php create mode 100644 tests/PersistentData/Model/ModelTest.php create mode 100644 tests/Util/JwtParserTest.php create mode 100644 tests/View/ParserTest.php diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..b4c1516 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,14 @@ +{ + "name": "SokoWeb", + "build": { + "dockerfile": "Dockerfile" + }, + "customizations": { + "vscode": { + "extensions": [ + "bmewburn.vscode-intelephense-client", + "xdebug.php-debug" + ] + } + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..61ead86 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/vendor diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6c9cf9a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,4 @@ +FROM php:7.4.7-cli-buster + +RUN apt-get update && apt-get install -y unzip +RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..1f8a45e --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,59 @@ +pipeline { + agent { + node { + label 'soko-web' + customWorkspace 'workspace/soko-web' + } + } + + stages { + stage('Install composer') { + agent { + dockerfile { + filename 'Dockerfile' + dir '.' + reuseNode true + } + } + steps { + sh 'composer install' + } + } + + stage('Unit Testing') { + agent { + dockerfile { + filename 'Dockerfile' + dir '.' + reuseNode true + } + } + steps { + sh 'vendor/bin/phpunit --log-junit unit_test_results.xml --testdox tests' + } + post { + success { + archiveArtifacts 'unit_test_results.xml' + } + } + } + + stage('Static Code Analysis') { + agent { + dockerfile { + filename 'Dockerfile' + dir '.' + reuseNode true + } + } + steps { + sh 'php vendor/bin/phpstan analyse -c phpstan.neon --error-format=prettyJson > static_code_analysis_results.json' + } + post { + success { + archiveArtifacts 'static_code_analysis_results.json' + } + } + } + } +} diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. 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 +them 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 prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. 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. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey 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; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If 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 convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU 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 that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + 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. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +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. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + 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 +state 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 3 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, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program 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, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU 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 Lesser General +Public License instead of this License. But first, please read +. diff --git a/assets/test_views/view_complex.php b/assets/test_views/view_complex.php new file mode 100644 index 0000000..e9255eb --- /dev/null +++ b/assets/test_views/view_complex.php @@ -0,0 +1,22 @@ +@css(test1.css) +@js(test1.js) +@js(test2_) + +@extra + +@endextra + +@extends(parent) + +@section(section1) +
Test HTML with @extends - section 1
+@endsection + +@section(section2) +
Test HTML with @extends - section 2
+@endsection + +@extra + +EXTRA +@endextra diff --git a/assets/test_views/view_invalid_multiple_extends.php b/assets/test_views/view_invalid_multiple_extends.php new file mode 100644 index 0000000..f9fea38 --- /dev/null +++ b/assets/test_views/view_invalid_multiple_extends.php @@ -0,0 +1,5 @@ +@extends(parent1) + +@extends(parent2) + +
Test HTML with multiple @extends
diff --git a/assets/test_views/view_invalid_multiple_sections_open.php b/assets/test_views/view_invalid_multiple_sections_open.php new file mode 100644 index 0000000..7832e04 --- /dev/null +++ b/assets/test_views/view_invalid_multiple_sections_open.php @@ -0,0 +1,6 @@ +@extends(parent) + +@section(section1) +@section(section2) + +
Test HTML with opening @section before previous closed
diff --git a/assets/test_views/view_invalid_section_not_open.php b/assets/test_views/view_invalid_section_not_open.php new file mode 100644 index 0000000..855a336 --- /dev/null +++ b/assets/test_views/view_invalid_section_not_open.php @@ -0,0 +1,4 @@ +@extends(parent) + +
Test HTML with @endsection when no section is open
+@endsection diff --git a/assets/test_views/view_invalid_section_without_extends.php b/assets/test_views/view_invalid_section_without_extends.php new file mode 100644 index 0000000..ec143a6 --- /dev/null +++ b/assets/test_views/view_invalid_section_without_extends.php @@ -0,0 +1,3 @@ +@section(section1) + +
Test HTML with @section but without @extends
diff --git a/assets/test_views/view_with_assets.php b/assets/test_views/view_with_assets.php new file mode 100644 index 0000000..d5076a7 --- /dev/null +++ b/assets/test_views/view_with_assets.php @@ -0,0 +1,4 @@ +@css(test.css) +@js(test.js) +@js(test_) +
Test HTML with @css and @js
diff --git a/assets/test_views/view_with_extends.php b/assets/test_views/view_with_extends.php new file mode 100644 index 0000000..0c3f301 --- /dev/null +++ b/assets/test_views/view_with_extends.php @@ -0,0 +1,5 @@ +@extends(parent) + +@section(section1) +
Test HTML with @extends
+@endsection diff --git a/assets/test_views/view_without_extends.php b/assets/test_views/view_without_extends.php new file mode 100644 index 0000000..29864cd --- /dev/null +++ b/assets/test_views/view_without_extends.php @@ -0,0 +1 @@ +
Test HTML without @extends
\ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..f9868fe --- /dev/null +++ b/composer.json @@ -0,0 +1,20 @@ +{ + "name": "esoko/soko-web", + "type": "library", + "description": "Lightweight web framework", + "license": "GNU GPL 3.0", + "require": { + "vlucas/phpdotenv": "^5.5", + "symfony/console": "^5.4", + "phpmailer/phpmailer": "^6.8" + }, + "require-dev": { + "phpunit/phpunit": "^9.6", + "phpstan/phpstan": "^1.10" + }, + "autoload": { + "psr-4": { + "SokoWeb\\": "src" + } + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..7107952 --- /dev/null +++ b/composer.lock @@ -0,0 +1,2989 @@ +{ + "_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": "079e8a8443fdc25cbe3b03322494b73b", + "packages": [ + { + "name": "graham-campbell/result-type", + "version": "v1.1.1", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "672eff8cf1d6fe1ef09ca0f89c4b287d6a3eb831" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/672eff8cf1d6fe1ef09ca0f89c4b287d6a3eb831", + "reference": "672eff8cf1d6fe1ef09ca0f89c4b287d6a3eb831", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.1" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.32 || ^9.6.3 || ^10.0.12" + }, + "type": "library", + "autoload": { + "psr-4": { + "GrahamCampbell\\ResultType\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "An Implementation Of The Result Type", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Result Type", + "Result-Type", + "result" + ], + "support": { + "issues": "https://github.com/GrahamCampbell/Result-Type/issues", + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "type": "tidelift" + } + ], + "time": "2023-02-25T20:23:15+00:00" + }, + { + "name": "phpmailer/phpmailer", + "version": "v6.8.0", + "source": { + "type": "git", + "url": "https://github.com/PHPMailer/PHPMailer.git", + "reference": "df16b615e371d81fb79e506277faea67a1be18f1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/df16b615e371d81fb79e506277faea67a1be18f1", + "reference": "df16b615e371d81fb79e506277faea67a1be18f1", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-filter": "*", + "ext-hash": "*", + "php": ">=5.5.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.2", + "doctrine/annotations": "^1.2.6 || ^1.13.3", + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.3.2", + "phpcompatibility/php-compatibility": "^9.3.5", + "roave/security-advisories": "dev-latest", + "squizlabs/php_codesniffer": "^3.7.1", + "yoast/phpunit-polyfills": "^1.0.4" + }, + "suggest": { + "ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses", + "ext-openssl": "Needed for secure SMTP sending and DKIM signing", + "greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication", + "hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication", + "league/oauth2-google": "Needed for Google XOAUTH2 authentication", + "psr/log": "For optional PSR-3 debug logging", + "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)", + "thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication" + }, + "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.8.0" + }, + "funding": [ + { + "url": "https://github.com/Synchro", + "type": "github" + } + ], + "time": "2023-03-06T14:43:22+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.9.1", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "dd3a383e599f49777d8b628dadbb90cae435b87e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/dd3a383e599f49777d8b628dadbb90cae435b87e", + "reference": "dd3a383e599f49777d8b628dadbb90cae435b87e", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.32 || ^9.6.3 || ^10.0.12" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": true + }, + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.9.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2023-02-25T19:38:58+00:00" + }, + { + "name": "psr/container", + "version": "1.1.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "513e0666f7216c7459170d56df27dfcefe1689ea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/513e0666f7216c7459170d56df27dfcefe1689ea", + "reference": "513e0666f7216c7459170d56df27dfcefe1689ea", + "shasum": "" + }, + "require": { + "php": ">=7.4.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.2" + }, + "time": "2021-11-05T16:50:12+00:00" + }, + { + "name": "symfony/console", + "version": "v5.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "3cd51fd2e6c461ca678f84d419461281bd87a0a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/3cd51fd2e6c461ca678f84d419461281bd87a0a8", + "reference": "3cd51fd2e6c461ca678f84d419461281bd87a0a8", + "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.22" + }, + "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": "2023-03-25T09:27:28+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v2.5.2", + "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.2" + }, + "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/polyfill-ctype", + "version": "v1.27.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "5bbc823adecdae860bb64756d639ecfec17b050a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a", + "reference": "5bbc823adecdae860bb64756d639ecfec17b050a", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.27-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.27.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-11-03T14:55:06+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.27.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "511a08c03c1960e08a883f4cffcacd219b758354" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/511a08c03c1960e08a883f4cffcacd219b758354", + "reference": "511a08c03c1960e08a883f4cffcacd219b758354", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.27-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.27.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-11-03T14:55:06+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.27.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/19bd1e4fcd5b91116f14d8533c57831ed00571b6", + "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.27-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.27.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-11-03T14:55:06+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.27.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534", + "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.27-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.27.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-11-03T14:55:06+00:00" + }, + { + "name": "symfony/polyfill-php73", + "version": "v1.27.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "9e8ecb5f92152187c4799efd3c96b78ccab18ff9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/9e8ecb5f92152187c4799efd3c96b78ccab18ff9", + "reference": "9e8ecb5f92152187c4799efd3c96b78ccab18ff9", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.27-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.27.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-11-03T14:55:06+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.27.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", + "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.27-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.27.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-11-03T14:55:06+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v2.5.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "4b426aac47d6427cc1a1d0f7e2ac724627f5966c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/4b426aac47d6427cc1a1d0f7e2ac724627f5966c", + "reference": "4b426aac47d6427cc1a1d0f7e2ac724627f5966c", + "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.2" + }, + "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-05-30T19:17:29+00:00" + }, + { + "name": "symfony/string", + "version": "v5.4.22", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "8036a4c76c0dd29e60b6a7cafcacc50cf088ea62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/8036a4c76c0dd29e60b6a7cafcacc50cf088ea62", + "reference": "8036a4c76c0dd29e60b6a7cafcacc50cf088ea62", + "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.22" + }, + "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": "2023-03-14T06:11:53+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v5.5.0", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "1a7ea2afc49c3ee6d87061f5a233e3a035d0eae7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/1a7ea2afc49c3ee6d87061f5a233e3a035d0eae7", + "reference": "1a7ea2afc49c3ee6d87061f5a233e3a035d0eae7", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "graham-campbell/result-type": "^1.0.2", + "php": "^7.1.3 || ^8.0", + "phpoption/phpoption": "^1.8", + "symfony/polyfill-ctype": "^1.23", + "symfony/polyfill-mbstring": "^1.23.1", + "symfony/polyfill-php80": "^1.23.1" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "ext-filter": "*", + "phpunit/phpunit": "^7.5.20 || ^8.5.30 || ^9.5.25" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": true + }, + "branch-alias": { + "dev-master": "5.5-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v5.5.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2022-10-16T01:01:54+00:00" + } + ], + "packages-dev": [ + { + "name": "doctrine/instantiator", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b", + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^11", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^0.16 || ^1", + "phpstan/phpstan": "^1.4", + "phpstan/phpstan-phpunit": "^1", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.30 || ^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/1.5.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2022-12-30T00:15:36+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.11.1", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3,<3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2023-03-08T13:26:56+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v4.15.4", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "6bb5176bc4af8bcb7d926f88718db9b96a2d4290" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/6bb5176bc4af8bcb7d926f88718db9b96a2d4290", + "reference": "6bb5176bc4af8bcb7d926f88718db9b96a2d4290", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=7.0" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.4" + }, + "time": "2023-03-05T19:49:14+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "97803eca37d319dfa7826cc2437fc020857acb53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", + "reference": "97803eca37d319dfa7826cc2437fc020857acb53", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.3" + }, + "time": "2021-07-20T11:28:43+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "1.10.11", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "8aa62e6ea8b58ffb650e02940e55a788cbc3fe21" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/8aa62e6ea8b58ffb650e02940e55a788cbc3fe21", + "reference": "8aa62e6ea8b58ffb650e02940e55a788cbc3fe21", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", + "type": "tidelift" + } + ], + "time": "2023-04-04T19:17:42+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "9.2.26", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "443bc6912c9bd5b409254a40f4b0f4ced7c80ea1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/443bc6912c9bd5b409254a40f4b0f4ced7c80ea1", + "reference": "443bc6912c9bd5b409254a40f4b0f4ced7c80ea1", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.15", + "php": ">=7.3", + "phpunit/php-file-iterator": "^3.0.3", + "phpunit/php-text-template": "^2.0.2", + "sebastian/code-unit-reverse-lookup": "^2.0.2", + "sebastian/complexity": "^2.0", + "sebastian/environment": "^5.1.2", + "sebastian/lines-of-code": "^1.0.3", + "sebastian/version": "^3.0.1", + "theseer/tokenizer": "^1.2.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.26" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-03-06T12:58:08+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "3.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-12-02T12:48:52+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:58:55+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T05:33:50+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:16:10+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "9.6.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "b65d59a059d3004a040c16a82e07bbdf6cfdd115" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b65d59a059d3004a040c16a82e07bbdf6cfdd115", + "reference": "b65d59a059d3004a040c16a82e07bbdf6cfdd115", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.3.1 || ^2", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.10.1", + "phar-io/manifest": "^2.0.3", + "phar-io/version": "^3.0.2", + "php": ">=7.3", + "phpunit/php-code-coverage": "^9.2.13", + "phpunit/php-file-iterator": "^3.0.5", + "phpunit/php-invoker": "^3.1.1", + "phpunit/php-text-template": "^2.0.3", + "phpunit/php-timer": "^5.0.2", + "sebastian/cli-parser": "^1.0.1", + "sebastian/code-unit": "^1.0.6", + "sebastian/comparator": "^4.0.8", + "sebastian/diff": "^4.0.3", + "sebastian/environment": "^5.1.3", + "sebastian/exporter": "^4.0.5", + "sebastian/global-state": "^5.0.1", + "sebastian/object-enumerator": "^4.0.3", + "sebastian/resource-operations": "^3.0.3", + "sebastian/type": "^3.2", + "sebastian/version": "^3.0.2" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.6-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.6" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2023-03-27T11:43:46+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:08:49+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:08:54+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:30:19+00:00" + }, + { + "name": "sebastian/comparator", + "version": "4.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-09-14T12:41:17+00:00" + }, + { + "name": "sebastian/complexity", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", + "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.7", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T15:52:27+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:10:38+00:00" + }, + { + "name": "sebastian/environment", + "version": "5.1.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:03:51+00:00" + }, + { + "name": "sebastian/exporter", + "version": "4.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-09-14T06:03:37+00:00" + }, + { + "name": "sebastian/global-state", + "version": "5.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/0ca8db5a5fc9c8646244e629625ac486fa286bf2", + "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-02-14T08:28:10+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.6", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-11-28T06:42:11+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:12:34+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:14:26+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "4.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:07:39+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "issues": "https://github.com/sebastianbergmann/resource-operations/issues", + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:45:17+00:00" + }, + { + "name": "sebastian/type", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:13:03+00:00" + }, + { + "name": "sebastian/version", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c6c1022351a901512170118436c764e473f6de8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:39:44+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2021-07-28T10:34:58+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.3.0" +} diff --git a/dummy.php b/dummy.php new file mode 100644 index 0000000..f3e0693 --- /dev/null +++ b/dummy.php @@ -0,0 +1,13 @@ +connection = new mysqli($host, $user, $password, $db, $port, $socket); + + if ($this->connection->connect_error) { + throw new \Exception('Connection failed: ' . $this->connection->connect_error); + } + + if (!$this->connection->set_charset('utf8mb4')) { + throw new \Exception($this->connection->error); + } + } + + public function __destruct() + { + $this->connection->close(); + } + + public function startTransaction(): void + { + if (!$this->connection->autocommit(false)) { + throw new \Exception($this->connection->error); + } + } + + public function commit(): void + { + if (!$this->connection->commit() || !$this->connection->autocommit(true)) { + throw new \Exception($this->connection->error); + } + } + + public function rollback(): void + { + if (!$this->connection->rollback() || !$this->connection->autocommit(true)) { + throw new \Exception($this->connection->error); + } + } + + public function query(string $query): ?IResultSet + { + if (!($result = $this->connection->query($query))) { + throw new \Exception($this->connection->error . '. Query: ' . $query); + } + + if ($result !== true) { + return new ResultSet($result); + } + + return null; + } + + public function multiQuery(string $query): array + { + if (!$this->connection->multi_query($query)) { + throw new \Exception($this->connection->error . '. Query: ' . $query); + } + + $ret = []; + do { + if ($result = $this->connection->store_result()) { + $ret[] = new ResultSet($result); + } else { + $ret[] = null; + } + + $this->connection->more_results(); + } while ($this->connection->next_result()); + + if ($this->connection->error) { + throw new \Exception($this->connection->error . '. Query: ' . $query); + } + + return $ret; + } + + public function prepare(string $query): IStatement + { + if (!($stmt = $this->connection->prepare($query))) { + throw new \Exception($this->connection->error . '. Query: ' . $query); + } + + return new Statement($stmt); + } + + public function lastId(): int + { + return $this->connection->insert_id; + } + + public function getAffectedRows(): int + { + return $this->connection->affected_rows; + } +} diff --git a/src/Database/Mysql/ResultSet.php b/src/Database/Mysql/ResultSet.php new file mode 100644 index 0000000..960171c --- /dev/null +++ b/src/Database/Mysql/ResultSet.php @@ -0,0 +1,62 @@ +result = $result; + } + + public function fetch(int $type = IResultSet::FETCH_ASSOC): ?array + { + return $this->result->fetch_array($this->convertFetchType($type)); + } + + public function fetchAll(int $type = IResultSet::FETCH_ASSOC): array + { + return $this->result->fetch_all($this->convertFetchType($type)); + } + + public function fetchOneColumn(string $valueName, string $keyName = null): array + { + $array = []; + + while ($r = $this->fetch(IResultSet::FETCH_ASSOC)) { + if (isset($keyName)) { + $array[$r[$keyName]] = $r[$valueName]; + } else { + $array[] = $r[$valueName]; + } + } + + return $array; + } + + private function convertFetchType(int $type): int + { + switch ($type) { + case IResultSet::FETCH_ASSOC: + $internal_type = MYSQLI_ASSOC; + break; + + case IResultSet::FETCH_BOTH: + $internal_type = MYSQLI_BOTH; + break; + + case IResultSet::FETCH_NUM: + $internal_type = MYSQLI_NUM; + break; + + default: + $internal_type = MYSQLI_BOTH; + break; + } + + return $internal_type; + } +} diff --git a/src/Database/Mysql/Statement.php b/src/Database/Mysql/Statement.php new file mode 100644 index 0000000..aae2774 --- /dev/null +++ b/src/Database/Mysql/Statement.php @@ -0,0 +1,79 @@ +stmt = $stmt; + } + + public function __destruct() + { + $this->stmt->close(); + } + + public function execute(array $params = []): ?IResultSet + { + if ($params) { + $ref_params = ['']; + + foreach ($params as &$param) { + $type = gettype($param); + + switch ($type) { + case 'integer': + case 'double': + case 'string': + $t = $type[0]; + break; + + case 'NULL': + $t = 's'; + break; + + case 'boolean': + $param = (string) (int) $param; + $t = 's'; + break; + + case 'array': + $param = json_encode($param); + $t = 's'; + break; + } + + if (!isset($t)) { + throw new \Exception('Data type ' . $type . ' not supported!'); + } + + $ref_params[] = &$param; + $ref_params[0] .= $t; + } + + if (!call_user_func_array([$this->stmt, 'bind_param'], $ref_params)) { + throw new \Exception($this->stmt->error); + } + } + + if (!$this->stmt->execute()) { + throw new \Exception($this->stmt->error); + } + + if ($result_set = $this->stmt->get_result()) { + return new ResultSet($result_set); + } + + return null; + } + + public function getAffectedRows(): int + { + return $this->stmt->affected_rows; + } +} diff --git a/src/Database/Query/Modify.php b/src/Database/Query/Modify.php new file mode 100755 index 0000000..1f554c8 --- /dev/null +++ b/src/Database/Query/Modify.php @@ -0,0 +1,140 @@ +connection = $connection; + $this->table = $table; + } + + public function setIdName(string $idName): Modify + { + $this->idName = $idName; + + return $this; + } + + public function setExternalId($id): Modify + { + $this->externalId = $id; + + return $this; + } + + public function setAutoIncrement(bool $autoIncrement = true): Modify + { + $this->autoIncrement = $autoIncrement; + + return $this; + } + + public function fill(array $attributes): Modify + { + $this->attributes = array_merge($this->attributes, $attributes); + + return $this; + } + + public function set(string $name, $value): Modify + { + $this->attributes[$name] = $value; + + return $this; + } + + public function setId($id): Modify + { + $this->attributes[$this->idName] = $id; + + return $this; + } + + public function getId() + { + return $this->attributes[$this->idName]; + } + + public function save(): void + { + if (isset($this->attributes[$this->idName])) { + $this->update(); + } else { + $this->insert(); + } + } + + public function delete(): void + { + if (!isset($this->attributes[$this->idName])) { + throw new \Exception('No primary key specified!'); + } + + $query = 'DELETE FROM ' . Utils::backtick($this->table) . ' WHERE ' . Utils::backtick($this->idName) . '=?'; + + $stmt = $this->connection->prepare($query); + $stmt->execute([$this->idName => $this->attributes[$this->idName]]); + } + + private function insert(): void + { + if ($this->externalId !== null) { + $this->attributes[$this->idName] = $this->externalId; + } elseif (!$this->autoIncrement) { + $this->attributes[$this->idName] = $this->generateKey(); + } + + $set = $this->generateColumnsWithBinding(array_keys($this->attributes)); + + $query = 'INSERT INTO ' . Utils::backtick($this->table) . ' SET ' . $set; + + $stmt = $this->connection->prepare($query); + $stmt->execute($this->attributes); + + if ($this->autoIncrement) { + $this->attributes[$this->idName] = $this->connection->lastId(); + } + } + + private function update(): void + { + $attributes = $this->attributes; + unset($attributes[$this->idName]); + + $set = $this->generateColumnsWithBinding(array_keys($attributes)); + + $query = 'UPDATE ' . Utils::backtick($this->table) . ' SET ' . $set . ' WHERE ' . Utils::backtick($this->idName) . '=?'; + + $stmt = $this->connection->prepare($query); + $stmt->execute(array_merge($attributes, [$this->idName => $this->attributes[$this->idName]])); + } + + public static function generateColumnsWithBinding(array $columns): string + { + array_walk($columns, function(&$value, $key) { + $value = Utils::backtick($value) . '=?'; + }); + + return implode(',', $columns); + } + + private function generateKey(): string + { + return substr(hash('sha256', serialize($this->attributes) . random_bytes(5) . microtime()), 0, 7); + } +} diff --git a/src/Database/Query/Select.php b/src/Database/Query/Select.php new file mode 100644 index 0000000..8acbb0d --- /dev/null +++ b/src/Database/Query/Select.php @@ -0,0 +1,445 @@ + [], self::CONDITION_HAVING => []]; + + private array $groups = []; + + private array $orders = []; + + private ?array $limit; + + public function __construct(IConnection $connection, ?string $table = null) + { + $this->connection = $connection; + + if ($table !== null) { + $this->table = $table; + } + } + + public function setIdName(string $idName): Select + { + $this->idName = $idName; + + return $this; + } + + public function setTableAliases(array $tableAliases): Select + { + $this->tableAliases = array_merge($this->tableAliases, $tableAliases); + + return $this; + } + + public function setDerivedTableAlias(string $tableAlias): Select + { + return $this->setTableAliases([Select::DERIVED_TABLE_KEY => $tableAlias]); + } + + public function from(string $table): Select + { + $this->table = $table; + + return $this; + } + + public function columns(array $columns): Select + { + $this->columns = array_merge($this->columns, $columns); + + return $this; + } + + public function innerJoin($table, $column1, string $relation, $column2): Select + { + $this->addJoin('INNER', $table, $column1, $relation, $column2); + + return $this; + } + + public function leftJoin($table, $column1, string $relation, $column2): Select + { + $this->addJoin('LEFT', $table, $column1, $relation, $column2); + + return $this; + } + + public function whereId($value): Select + { + $this->addWhereCondition('AND', $this->idName, '=', $value); + + return $this; + } + + public function where($column, string $relation = null, $value = null): Select + { + $this->addWhereCondition('AND', $column, $relation, $value); + + return $this; + } + + public function orWhere($column, string $relation = null, $value = null): Select + { + $this->addWhereCondition('OR', $column, $relation, $value); + + return $this; + } + + public function having($column, string $relation = null, $value = null): Select + { + $this->addHavingCondition('AND', $column, $relation, $value); + + return $this; + } + + public function orHaving($column, string $relation = null, $value = null): Select + { + $this->addHavingCondition('OR', $column, $relation, $value); + + return $this; + } + + public function groupBy($column): Select + { + $this->groups[] = $column; + + return $this; + } + + public function orderBy($column, string $type = 'ASC'): Select + { + $this->orders[] = [$column, $type]; + + return $this; + } + + public function limit(int $limit, int $offset = 0): Select + { + $this->limit = [$limit, $offset]; + + return $this; + } + + public function resetLimit(): void + { + $this->limit = null; + } + + public function paginate(int $page, int $itemsPerPage): Select + { + $this->limit($itemsPerPage, ($page - 1) * $itemsPerPage); + + return $this; + } + + public function execute(): IResultSet + { + list($query, $params) = $this->generateQuery(); + + return $this->connection->prepare($query)->execute($params); + } + + public function count(): int + { + if (count($this->groups) > 0 || count($this->conditions[self::CONDITION_HAVING]) > 0) { + $orders = $this->orders; + + $this->orders = []; + + list($query, $params) = $this->generateQuery(); + + $result = $this->connection->prepare('SELECT COUNT(*) num_rows FROM (' . $query . ') x') + ->execute($params) + ->fetch(IResultSet::FETCH_NUM); + + $this->orders = $orders; + + return $result[0]; + } else { + $columns = $this->columns; + $orders = $this->orders; + + $this->columns = [new RawExpression('COUNT(*) num_rows')]; + $this->orders = []; + + list($query, $params) = $this->generateQuery(); + + $result = $this->connection->prepare($query) + ->execute($params) + ->fetch(IResultSet::FETCH_NUM); + + $this->columns = $columns; + $this->orders = $orders; + + return $result[0]; + } + } + + private function isDerivedTable(): bool + { + return array_key_exists(Select::DERIVED_TABLE_KEY, $this->tableAliases); + } + + private function addJoin(string $type, $table, $column1, string $relation, $column2): void + { + $this->joins[] = [$type, $table, $column1, $relation, $column2]; + } + + private function addWhereCondition(string $logic, $column, string $relation, $value): void + { + $this->conditions[self::CONDITION_WHERE][] = [$logic, $column, $relation, $value]; + } + + private function addHavingCondition(string $logic, $column, string $relation, $value): void + { + $this->conditions[self::CONDITION_HAVING][] = [$logic, $column, $relation, $value]; + } + + private function generateQuery(): array + { + list($tableQuery, $tableParams) = $this->generateTable($this->table, true); + $queryString = 'SELECT ' . $this->generateColumns() . ' FROM ' . $tableQuery; + + if (count($this->joins) > 0) { + list($joinQuery, $joinParams) = $this->generateJoins(); + $queryString .= ' ' . $joinQuery; + } else { + $joinParams = []; + } + + if (count($this->conditions[self::CONDITION_WHERE]) > 0) { + list($wheres, $whereParams) = $this->generateConditions(self::CONDITION_WHERE); + + $queryString .= ' WHERE ' . $wheres; + } else { + $whereParams = []; + } + + if (count($this->groups) > 0) { + $queryString .= ' GROUP BY ' . $this->generateGroupBy(); + } + + if (count($this->conditions[self::CONDITION_HAVING]) > 0) { + list($havings, $havingParams) = $this->generateConditions(self::CONDITION_HAVING); + + $queryString .= ' HAVING ' . $havings; + } else { + $havingParams = []; + } + + if (count($this->orders) > 0) { + $queryString .= ' ORDER BY ' . $this->generateOrderBy(); + } + + if (isset($this->limit)) { + $queryString .= ' LIMIT ' . $this->limit[1] . ', ' . $this->limit[0]; + } + + if ($this->isDerivedTable()) { + $queryString = '(' . $queryString . ') AS ' . $this->tableAliases[Select::DERIVED_TABLE_KEY]; + } + + return [$queryString, array_merge($tableParams, $joinParams, $whereParams, $havingParams)]; + } + + private function generateTable($table, bool $defineAlias = false): array + { + $params = []; + + if ($table instanceof RawExpression) { + return [(string) $table, $params]; + } + + if ($table instanceof Select) + { + return $table->generateQuery(); + } + + if (isset($this->tableAliases[$table])) { + $queryString = ($defineAlias ? Utils::backtick($this->tableAliases[$table]) . ' ' . Utils::backtick($table) : Utils::backtick($table)); + return [$queryString, $params]; + } + + return [Utils::backtick($table), $params]; + } + + private function generateColumn($column): string + { + if ($column instanceof RawExpression) { + return (string) $column; + } + + if (is_array($column)) { + $out = ''; + + if ($column[0]) { + list($tableName, $params) = $this->generateTable($column[0]); + $out .= $tableName . '.'; + } + + $out .= Utils::backtick($column[1]); + + if (!empty($column[2])) { + $out .= ' ' . Utils::backtick($column[2]); + } + + return $out; + } else { + return Utils::backtick($column); + } + } + + private function generateColumns(): string + { + $columns = $this->columns; + + array_walk($columns, function (&$value, $key) { + $value = $this->generateColumn($value); + }); + + return implode(',', $columns); + } + + private function generateJoins(): array + { + + $joinQueries = []; + $params = []; + + foreach ($this->joins as $join) { + list($joinQueryFragment, $paramsFragment) = $this->generateTable($join[1], true); + $joinQueries[] = $join[0] . ' JOIN ' . $joinQueryFragment . ' ON ' . $this->generateColumn($join[2]) . ' ' . $join[3] . ' ' . $this->generateColumn($join[4]); + $params = array_merge($params, $paramsFragment); + } + + return [implode(' ', $joinQueries), $params]; + } + + private function generateConditions(int $type): array + { + $conditions = ''; + $params = []; + + foreach ($this->conditions[$type] as $condition) { + list($logic, $column, $relation, $value) = $condition; + + if ($column instanceof Closure) { + list($conditionsStringFragment, $paramsFragment) = $this->generateComplexConditionFragment($type, $column); + } else { + list($conditionsStringFragment, $paramsFragment) = $this->generateConditionFragment($condition); + } + + if ($conditions !== '') { + $conditions .= ' ' . $logic . ' '; + } + + $conditions .= $conditionsStringFragment; + $params = array_merge($params, $paramsFragment); + } + + return [$conditions, $params]; + } + + private function generateConditionFragment(array $condition): array + { + list($logic, $column, $relation, $value) = $condition; + + if ($column instanceof RawExpression) { + return [(string) $column, []]; + } + + $conditionsString = $this->generateColumn($column) . ' '; + + if ($value === null) { + return [$conditionsString . ($relation == '=' ? 'IS NULL' : 'IS NOT NULL'), []]; + } + + $conditionsString .= strtoupper($relation) . ' ';; + + switch ($relation = strtolower($relation)) { + case 'between': + $params = [$value[0], $value[1]]; + + $conditionsString .= '? AND ?'; + break; + + case 'in': + case 'not in': + $params = $value; + + if (count($value) > 0) { + $conditionsString .= '(' . implode(', ', array_fill(0, count($value), '?')) . ')'; + } else { + $conditionsString = $relation == 'in' ? '0' : '1'; + } + break; + + default: + $params = [$value]; + + $conditionsString .= '?'; + } + + return [$conditionsString, $params]; + } + + private function generateComplexConditionFragment(int $type, Closure $conditionCallback): array + { + $instance = new self($this->connection, $this->table); + $instance->tableAliases = $this->tableAliases; + + $conditionCallback($instance); + + list($conditions, $params) = $instance->generateConditions($type); + + return ['(' . $conditions . ')', $params]; + } + + private function generateGroupBy(): string + { + $groups = $this->groups; + + array_walk($groups, function (&$value, $key) { + $value = $this->generateColumn($value); + }); + + return implode(',', $groups); + } + + private function generateOrderBy(): string + { + $orders = $this->orders; + + array_walk($orders, function (&$value, $key) { + $value = $this->generateColumn($value[0]) . ' ' . strtoupper($value[1]); + }); + + return implode(',', $orders); + } +} diff --git a/src/Database/RawExpression.php b/src/Database/RawExpression.php new file mode 100644 index 0000000..7e3ce6c --- /dev/null +++ b/src/Database/RawExpression.php @@ -0,0 +1,16 @@ +expression = $expression; + } + + public function __toString(): string + { + return $this->expression; + } +} diff --git a/src/Database/Utils.php b/src/Database/Utils.php new file mode 100644 index 0000000..658b13c --- /dev/null +++ b/src/Database/Utils.php @@ -0,0 +1,8 @@ +url = $url; + $this->method = $method; + } + + public function setUrl(string $url): void + { + $this->url = $url; + } + + public function setMethod(int $method): void + { + $this->method = $method; + } + + public function setQuery($query): void + { + if (is_string($query)) { + $this->query = $query; + } else { + $this->query = http_build_query($query); + } + } + + public function setHeaders(array $headers): void + { + $this->headers = array_merge($this->headers, $headers); + } + + public function send(): IResponse + { + $ch = curl_init(); + + if ($this->method === self::HTTP_POST) { + $url = $this->url; + + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, $this->query); + } else { + $url = $this->url . '?' . $this->query; + } + + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); + curl_setopt($ch, CURLOPT_TIMEOUT, 20); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); + curl_setopt($ch, CURLOPT_USERAGENT, 'SokoWeb cURL/1.0'); + + if (count($this->headers) > 0) { + curl_setopt($ch, CURLOPT_HTTPHEADER, $this->headers); + } + + $responseHeaders = []; + curl_setopt( + $ch, + CURLOPT_HEADERFUNCTION, + function ($ch, $header) use (&$responseHeaders) { + $len = strlen($header); + $header = explode(':', $header, 2); + + if (count($header) < 2) { + return $len; + } + + $responseHeaders[strtolower(trim($header[0]))][] = trim($header[1]); + + return $len; + } + ); + + $responseBody = curl_exec($ch); + + if ($responseBody === false) { + $error = curl_error($ch); + + curl_close($ch); + + throw new \Exception($error); + } + + curl_close($ch); + + return new Response($responseBody, $responseHeaders); + } +} diff --git a/src/Http/Response.php b/src/Http/Response.php new file mode 100644 index 0000000..3ff5fa1 --- /dev/null +++ b/src/Http/Response.php @@ -0,0 +1,26 @@ +body = $body; + $this->headers = $headers; + } + + public function getBody(): string + { + return $this->body; + } + + public function getHeaders(): array + { + return $this->headers; + } +} diff --git a/src/Interfaces/Authentication/IUser.php b/src/Interfaces/Authentication/IUser.php new file mode 100644 index 0000000..06b424e --- /dev/null +++ b/src/Interfaces/Authentication/IUser.php @@ -0,0 +1,16 @@ +recipients[] = [$mail, $name]; + } + + public function setSubject(string $subject): void + { + $this->subject = $subject; + } + + public function setBody(string $body): void + { + $this->body = $body; + } + + public function setBodyFromTemplate(string $template, array $params = []): void + { + $this->body = file_get_contents(ROOT . '/mail/' . $template . '.html'); + + $baseParameters = [ + 'APP_NAME' => $_ENV['APP_NAME'], + 'BASE_URL' => $_ENV['APP_URL'], + ]; + + $params = array_merge($baseParameters, $params); + + foreach ($params as $key => $param) { + $this->body = str_replace('{{' . $key . '}}', $param, $this->body); + } + } + + public function send(): void + { + $mailer = new PHPMailer(true); + + $mailer->CharSet = 'utf-8'; + $mailer->Hostname = substr($_ENV['MAIL_FROM'], strpos($_ENV['MAIL_FROM'], '@') + 1); + + if (!empty($_ENV['MAIL_HOST'])) { + $mailer->Mailer = 'smtp'; + $mailer->Host = $_ENV['MAIL_HOST']; + $mailer->Port = !empty($_ENV['MAIL_PORT']) ? $_ENV['MAIL_PORT'] : 25; + $mailer->SMTPSecure = !empty($_ENV['MAIL_SECURE']) ? $_ENV['MAIL_SECURE'] : ''; + + if (!empty($_ENV['MAIL_USER'])) { + $mailer->SMTPAuth = true; + $mailer->Username = $_ENV['MAIL_USER']; + $mailer->Password = $_ENV['MAIL_PASSWORD']; + } else { + $mailer->SMTPAuth = false; + } + } else { + $mailer->Mailer = 'mail'; + } + + $mailer->setFrom($_ENV['MAIL_FROM'], $_ENV['APP_NAME']); + $mailer->addReplyTo($_ENV['MAIL_FROM'], $_ENV['APP_NAME']); + + $mailer->Sender = !empty($_ENV['MAIL_BOUNCE']) ? $_ENV['MAIL_BOUNCE'] : $_ENV['MAIL_FROM']; + $mailer->Subject = $this->subject; + $mailer->msgHTML($this->body); + + foreach ($this->recipients as $recipient) { + $this->sendMail($mailer, $recipient); + } + } + + private function sendMail(PHPMailer $mailer, array $recipient): void + { + $mailer->clearAddresses(); + $mailer->addAddress($recipient[0], $recipient[1]); + + $mailer->send(); + } +} diff --git a/src/OAuth/GoogleOAuth.php b/src/OAuth/GoogleOAuth.php new file mode 100644 index 0000000..5b92909 --- /dev/null +++ b/src/OAuth/GoogleOAuth.php @@ -0,0 +1,56 @@ +request = $request; + } + + public function getDialogUrl(string $state, string $redirectUrl, ?string $nonce = null, ?string $loginHint = null): string + { + $oauthParams = [ + 'response_type' => 'code', + 'client_id' => $_ENV['GOOGLE_OAUTH_CLIENT_ID'], + 'scope' => 'openid email', + 'redirect_uri' => $redirectUrl, + 'state' => $state, + ]; + + if ($nonce !== null) { + $oauthParams['nonce'] = $nonce; + } + + if ($loginHint !== null) { + $oauthParams['login_hint'] = $loginHint; + } + + return self::$dialogUrlBase . '?' . http_build_query($oauthParams); + } + + public function getToken(string $code, string $redirectUrl): array + { + $tokenParams = [ + 'code' => $code, + 'client_id' => $_ENV['GOOGLE_OAUTH_CLIENT_ID'], + 'client_secret' => $_ENV['GOOGLE_OAUTH_CLIENT_SECRET'], + 'redirect_uri' => $redirectUrl, + 'grant_type' => 'authorization_code', + ]; + + $this->request->setUrl(self::$tokenUrlBase); + $this->request->setMethod(IRequest::HTTP_POST); + $this->request->setQuery($tokenParams); + $response = $this->request->send(); + + return json_decode($response->getBody(), true); + } +} diff --git a/src/PersistentData/Model/Model.php b/src/PersistentData/Model/Model.php new file mode 100644 index 0000000..23b2840 --- /dev/null +++ b/src/PersistentData/Model/Model.php @@ -0,0 +1,69 @@ +id = $id; + } + + public function getId() + { + return $this->id; + } + + public function toArray(): array + { + $array = []; + + foreach (self::getFields() as $key) { + $method = 'get' . str_replace('_', '', ucwords($key, '_')); + + if (method_exists($this, $method)) { + $array[$key] = $this->$method(); + } + } + + return $array; + } + + public function saveSnapshot(): void + { + $this->snapshot = $this->toArray(); + } + + public function resetSnapshot(): void + { + $this->snapshot = []; + } + + public function getSnapshot(): array + { + return $this->snapshot; + } +} diff --git a/src/PersistentData/PersistentDataManager.php b/src/PersistentData/PersistentDataManager.php new file mode 100644 index 0000000..aeb7949 --- /dev/null +++ b/src/PersistentData/PersistentDataManager.php @@ -0,0 +1,235 @@ +createSelect($select, $type, $useRelations, $withRelations); + + $data = $select->execute()->fetch(IResultSet::FETCH_ASSOC); + + if ($data === null) { + return null; + } + + $model = new $type(); + $this->fillWithData($data, $model, $withRelations); + + return $model; + } + + public function selectMultipleFromDb(Select $select, string $type, bool $useRelations = false, array $withRelations = []): Generator + { + $select = $this->createSelect($select, $type, $useRelations, $withRelations); + $result = $select->execute(); + + while ($data = $result->fetch(IResultSet::FETCH_ASSOC)) { + $model = new $type(); + $this->fillWithData($data, $model, $withRelations); + + yield $model; + } + } + + public function selectFromDbById($id, string $type, bool $useRelations = false) + { + $select = new Select(\Container::$dbConnection); + $select->whereId($id); + + return $this->selectFromDb($select, $type, $useRelations); + } + + public function fillWithData(array &$data, Model $model, array $withRelations = [], ?string $modelKey = null): void + { + $relations = $model::getRelations(); + if (count($withRelations)) { + $relations = array_intersect($relations, $withRelations); + } + + while (key($data)) { + $key = key($data); + $value = current($data); + $relation = key($relations); + + if (strpos($key, '__') == false) { + $method = 'set' . str_replace('_', '', ucwords($key, '_')); + + if (method_exists($model, $method) && isset($value)) { + $model->$method($value); + } + + next($data); + } else if (isset($modelKey) && substr($key, 0, strlen($modelKey . '__')) === $modelKey . '__') { + $key = substr($key, strlen($modelKey) + 2); + + $method = 'set' . str_replace('_', '', ucwords($key, '_')); + + if (method_exists($model, $method) && isset($value)) { + $model->$method($value); + } + + next($data); + } else if (substr($key, 0, strlen($relation . '__')) === $relation . '__') { + $relationType = current($relations); + $relationModel = new $relationType(); + $this->fillWithData($data, $relationModel, $withRelations, $relation); + + $method = 'set' . str_replace('_', '', ucwords($relation, '_')); + $model->$method($relationModel); + + next($relations); + } else { + return; + } + } + + $model->saveSnapshot(); + } + + public function loadRelationsFromDb(Model $model, bool $recursive): void + { + foreach ($model::getRelations() as $relation => $relationType) { + $camel = str_replace('_', '', ucwords($relation, '_')); + + $methodGet = 'get' . $camel . 'Id'; + $methodSet = 'set' . $camel; + + $relationId = $model->$methodGet(); + + if ($relationId !== null) { + $relationModel = $this->selectFromDbById($relationId, $relationType, $recursive); + + $model->$methodSet($relationModel); + } + } + } + + public function saveToDb(Model $model): void + { + $this->syncRelations($model); + + $modified = $model->toArray(); + $id = $model->getId(); + + $modify = new Modify(\Container::$dbConnection, $model::getTable()); + + if ($id !== null) { + $original = $model->getSnapshot(); + + foreach ($original as $key => $value) { + if ($value === $modified[$key]) { + unset($modified[$key]); + } + } + + if (count($modified) > 0) { + $modify->setId($id); + $modify->fill($modified); + $modify->save(); + } + } else { + $modify->fill($modified); + $modify->save(); + + $model->setId($modify->getId()); + } + + $model->saveSnapshot(); + } + + public function deleteFromDb(Model $model): void + { + $modify = new Modify(\Container::$dbConnection, $model::getTable()); + $modify->setId($model->getId()); + $modify->delete(); + + $model->setId(null); + $model->resetSnapshot(); + } + + private function createSelect(Select $select, string $type, bool $useRelations = false, array $withRelations = []): Select + { + $table = call_user_func([$type, 'getTable']); + $fields = call_user_func([$type, 'getFields']); + + $columns = []; + + foreach ($fields as $field) { + $columns[] = [$table, $field]; + } + + $select->from($table); + + if ($useRelations) { + $relations = call_user_func([$type, 'getRelations']); + if (count($withRelations)) { + $relations = array_intersect($relations, $withRelations); + } + + $columns = array_merge($columns, $this->getRelationColumns($relations, $withRelations)); + + $this->leftJoinRelations($select, $table, $relations, $withRelations); + $select->columns($columns); + } else { + $select->columns($columns); + } + + return $select; + } + + private function getRelationColumns(array $relations, array $withRelations): array + { + $columns = []; + + foreach ($relations as $relation => $relationType) { + $relationTable = call_user_func([$relationType, 'getTable']); + foreach (call_user_func([$relationType, 'getFields']) as $relationField) { + $columns[] = [$relationTable, $relationField, $relation . '__' . $relationField]; + } + + $nextOrderRelations = call_user_func([$relationType, 'getRelations']); + if (count($withRelations)) { + $nextOrderRelations = array_intersect($nextOrderRelations, $withRelations); + } + $columns = array_merge($columns, $this->getRelationColumns($nextOrderRelations, $withRelations)); + } + + return $columns; + } + + private function leftJoinRelations(Select $select, string $table, array $relations, array $withRelations): void + { + foreach ($relations as $relation => $relationType) { + $relationTable = call_user_func([$relationType, 'getTable']); + $select->leftJoin($relationTable, [$relationTable, 'id'], '=', [$table, $relation . '_id']); + + $nextOrderRelations = call_user_func([$relationType, 'getRelations']); + if (count($withRelations)) { + $nextOrderRelations = array_intersect($nextOrderRelations, $withRelations); + } + $this->leftJoinRelations($select, $relationTable, $nextOrderRelations, $withRelations); + } + } + + private function syncRelations(Model $model): void + { + foreach ($model::getRelations() as $relation => $relationType) { + $camel = str_replace('_', '', ucwords($relation, '_')); + + $methodGet = 'get' . $camel; + $methodSet = 'set' . $camel . 'Id'; + + $relationModel = $model->$methodGet(); + + if ($relationModel !== null) { + $model->$methodSet($relationModel->getId()); + } + } + } +} diff --git a/src/Request/Request.php b/src/Request/Request.php new file mode 100644 index 0000000..0184fdc --- /dev/null +++ b/src/Request/Request.php @@ -0,0 +1,86 @@ +base = $base; + $this->get = &$get; + $this->post = &$post; + $this->session = new Session($session); + + $userId = $this->session->get('userId'); + if ($userId !== null) { + $this->user = $userRepository->getById($userId); + } + } + + public function setParsedRouteParams(array &$routeParams): void + { + $this->routeParams = &$routeParams; + } + + public function getBase(): string + { + return $this->base; + } + + public function query($key) + { + if (isset($this->get[$key])) { + return $this->get[$key]; + } + + if (isset($this->routeParams[$key])) { + return $this->routeParams[$key]; + } + + return null; + } + + public function post($key) + { + if (isset($this->post[$key])) { + return $this->post[$key]; + } + + return null; + } + + public function session(): ISession + { + return $this->session; + } + + public function setUser(?IUser $user): void + { + if ($user === null) { + $this->session->delete('userId'); + return; + } + + $this->session->set('userId', $user->getUniqueId()); + } + + public function user(): ?IUser + { + return $this->user; + } +} diff --git a/src/Request/Session.php b/src/Request/Session.php new file mode 100644 index 0000000..687decc --- /dev/null +++ b/src/Request/Session.php @@ -0,0 +1,37 @@ +data = &$data; + } + + public function has(string $key): bool + { + return isset($this->data[$key]); + } + + public function get(string $key) + { + if (isset($this->data[$key])) { + return $this->data[$key]; + } + + return null; + } + + public function set(string $key, $value): void + { + $this->data[$key] = $value; + } + + public function delete(string $key): void + { + unset($this->data[$key]); + } +} diff --git a/src/Response/ContentBase.php b/src/Response/ContentBase.php new file mode 100644 index 0000000..ef46373 --- /dev/null +++ b/src/Response/ContentBase.php @@ -0,0 +1,22 @@ +data = $data; + } + + public function getData(): array + { + return $this->data; + } + + abstract public function render(): void; + + abstract public function getContentType(): string; +} diff --git a/src/Response/HtmlContent.php b/src/Response/HtmlContent.php new file mode 100644 index 0000000..7465231 --- /dev/null +++ b/src/Response/HtmlContent.php @@ -0,0 +1,35 @@ +view = $view; + $this->data = $data; + } + + public function render(): void + { + if (!empty($_ENV['DEV'])) { + $generator = new Linker($this->view); + $generator->generate(); + } + + extract($this->data); + + if (defined('SCRIPT_STARTED')) { + $__debug_runtime = round((hrtime(true) - SCRIPT_STARTED) / 1e+6, 1); + } + + require ROOT . '/cache/views/' . $this->view . '.php'; + } + + public function getContentType(): string + { + return 'text/html'; + } +} diff --git a/src/Response/JsonContent.php b/src/Response/JsonContent.php new file mode 100644 index 0000000..c01ad52 --- /dev/null +++ b/src/Response/JsonContent.php @@ -0,0 +1,23 @@ +data = $data; + } + + public function render(): void + { + if (defined('SCRIPT_STARTED')) { + $this->data['__debug_runtime'] = round((hrtime(true) - SCRIPT_STARTED) / 1e+6, 1); + } + + echo json_encode($this->data); + } + + public function getContentType(): string + { + return 'application/json'; + } +} diff --git a/src/Response/Redirect.php b/src/Response/Redirect.php new file mode 100644 index 0000000..d17b48b --- /dev/null +++ b/src/Response/Redirect.php @@ -0,0 +1,41 @@ +target = $target; + $this->type = $type; + } + + public function getUrl(): string + { + if (preg_match('/^http(s)?/', $this->target) === 1) { + $link = $this->target; + } else { + $link = \Container::$request->getBase() . '/' . $this->target; + } + + return $link; + } + + public function getHttpCode(): int + { + switch ($this->type) { + case IRedirect::PERMANENT: + return 301; + + case IRedirect::TEMPORARY: + return 302; + + default: + return 302; + } + } +} diff --git a/src/Routing/Route.php b/src/Routing/Route.php new file mode 100644 index 0000000..ad80cce --- /dev/null +++ b/src/Routing/Route.php @@ -0,0 +1,73 @@ +id = $id; + $this->pattern = $pattern; + $this->handler = $handler; + } + + public function getId(): string + { + return $this->id; + } + + public function getHandler(): array + { + return $this->handler; + } + + public function generateLink(array $parameters = []): string + { + $link = []; + + foreach ($this->pattern as $fragment) { + if (preg_match('/^{(\\w+)(\\?)?}$/', $fragment, $matches) === 1) { + if (isset($parameters[$matches[1]])) { + $link[] = $parameters[$matches[1]]; + unset($parameters[$matches[1]]); + } elseif (!isset($matches[2])) {//TODO: why? parameter not found but not optional + $link[] = $fragment; + } + } else { + $link[] = $fragment; + } + } + + $queryParams = []; + foreach ($parameters as $key => $value) { + if ($value === null) { + continue; + } + + $queryParams[$key] = $value; + } + + $query = count($queryParams) > 0 ? '?' . http_build_query($queryParams) : ''; + + return implode('/', $link) . $query; + } + + public function testAgainst(array $path): ?array + { + $parameters = []; + + foreach ($path as $i => $fragment) { + if (preg_match('/^{(\\w+)(?:\\?)?}$/', $this->pattern[$i], $matches) === 1) { + $parameters[$matches[1]] = $fragment; + } elseif ($fragment != $this->pattern[$i]) { + return null; + } + } + + return $parameters; + } +} diff --git a/src/Routing/RouteCollection.php b/src/Routing/RouteCollection.php new file mode 100644 index 0000000..a554fdb --- /dev/null +++ b/src/Routing/RouteCollection.php @@ -0,0 +1,88 @@ + [], + 'post' => [] + ]; + + private array $groupStack = []; + + public function get(string $id, string $pattern, array $handler): void + { + $this->addRoute('get', $id, $pattern, $handler); + } + + public function post(string $id, string $pattern, array $handler): void + { + $this->addRoute('post', $id, $pattern, $handler); + } + + public function group(string $pattern, Closure $group): void + { + $this->groupStack[] = $pattern; + + $group($this); + + array_pop($this->groupStack); + } + + public function getRoute(string $id): ?Route + { + if (!isset($this->routes[$id])) { + return null; + } + + return $this->routes[$id]; + } + + public function match(string $method, array $uri): ?array + { + $groupNumber = count($uri); + + // response to HEAD request with the GET content + if ($method === 'head') { + $method = 'get'; + } + + if (!isset($this->searchTable[$method][$groupNumber])) { + return null; + } + + foreach ($this->searchTable[$method][$groupNumber] as $route) { + if (($parameters = $route->testAgainst($uri)) !== null) { + return [$route, $parameters]; + } + } + + return null; + } + + private function addRoute(string $method, string $id, string $pattern, array $handler): void + { + if (isset($this->routes[$id])) { + throw new \Exception('Route already exists: ' . $id); + } + + $pattern = array_merge($this->groupStack, $pattern === '' ? [] : explode('/', $pattern)); + $route = new Route($id, $pattern, $handler); + + $groupNumber = count($pattern); + + $this->searchTable[$method][$groupNumber][] = $route; + + while (preg_match('/^{\\w+\\?}$/', end($pattern)) === 1) { + $groupNumber--; + array_pop($pattern); + + $this->searchTable[$method][$groupNumber][] = $route; + } + + $this->routes[$id] = $route; + } +} diff --git a/src/Session/DatabaseSessionHandler.php b/src/Session/DatabaseSessionHandler.php new file mode 100644 index 0000000..c094431 --- /dev/null +++ b/src/Session/DatabaseSessionHandler.php @@ -0,0 +1,104 @@ +columns(['data']); + $select->whereId(substr($id, 0, 32)); + + $result = $select->execute()->fetch(IResultSet::FETCH_ASSOC); + + if ($result === null) { + return ''; + } + + $this->exists = true; + + return $result['data']; + } + + public function write($id, $data): bool + { + $modify = new Modify(\Container::$dbConnection, 'sessions'); + + if ($this->exists) { + $modify->setId(substr($id, 0, 32)); + } else { + $modify->setExternalId(substr($id, 0, 32)); + } + + $modify->set('data', $data); + $modify->set('updated', (new DateTime())->format('Y-m-d H:i:s')); + $modify->save(); + + $this->written = true; + + return true; + } + + public function destroy($id): bool + { + $modify = new Modify(\Container::$dbConnection, 'sessions'); + $modify->setId(substr($id, 0, 32)); + $modify->delete(); + + $this->exists = false; + + return true; + } + + public function gc($maxlifetime): bool + { + // empty on purpose + // old sessions are deleted by MaintainDatabaseCommand + + return true; + } + + public function create_sid(): string + { + return bin2hex(random_bytes(16)); + } + + public function validateId($id): bool + { + return preg_match('/^[a-f0-9]{32}$/', $id) === 1; + } + + public function updateTimestamp($id, $data): bool + { + if ($this->written) { + return true; + } + + $modify = new Modify(\Container::$dbConnection, 'sessions'); + + $modify->setId(substr($id, 0, 32)); + $modify->set('updated', (new DateTime())->format('Y-m-d H:i:s')); + $modify->save(); + + return true; + } +} diff --git a/src/Util/CaptchaValidator.php b/src/Util/CaptchaValidator.php new file mode 100644 index 0000000..99161d8 --- /dev/null +++ b/src/Util/CaptchaValidator.php @@ -0,0 +1,19 @@ +setQuery([ + 'secret' => $_ENV['RECAPTCHA_SECRET'], + 'response' => $response + ]); + + $response = $request->send(); + + return json_decode($response->getBody(), true); + } +} diff --git a/src/Util/JwtParser.php b/src/Util/JwtParser.php new file mode 100644 index 0000000..865fc5c --- /dev/null +++ b/src/Util/JwtParser.php @@ -0,0 +1,33 @@ +setToken($token); + } + } + + public function setToken(string $token): void + { + $this->token = explode('.', str_replace(['_', '-'], ['/', '+'], $token)); + } + + public function getHeader(): array + { + return json_decode(base64_decode($this->token[0]), true); + } + + public function getPayload(): array + { + return json_decode(base64_decode($this->token[1]), true); + } + + public function getSignature(): string + { + return base64_decode($this->token[2]); + } +} diff --git a/src/View/Linker.php b/src/View/Linker.php new file mode 100644 index 0000000..a068e61 --- /dev/null +++ b/src/View/Linker.php @@ -0,0 +1,152 @@ +view = $view; + } + + public function generate(): void + { + $input = ROOT . '/views/' . $this->view . '.php'; + + $temporaryFiles = []; + $sections = ['externalCss' => '', 'inlineCss' => '', 'externalJs' => '', 'inlineJs' => '']; + $extra = ['', '']; + + do { + $parser = new Parser($input); + $fragment = $parser->parse(); + + $extends = $fragment->getExtends(); + + $this->generateAssets($fragment, $sections); + + $sections = array_merge($sections, $fragment->getSections()); //TODO: detect if section defined multiple times + $extra[0] = $fragment->getExtra()[0] . $extra[0]; + $extra[1] = $extra[1] . $fragment->getExtra()[1]; + + if ($extends === null) { + $this->writeFinal($extra, $input, ROOT . '/cache/views/' . $this->view . '.php'); + break; + } + + $tmpFile = tempnam(sys_get_temp_dir(), 'mapg-view-'); + $temporaryFiles[] = $tmpFile; + + $this->extendTemplate($sections, ROOT . '/views/' . $extends . '.php', $tmpFile); + + $input = $tmpFile; + } while (true); + + foreach ($temporaryFiles as $tmpFile) { + unlink($tmpFile); + } + } + + private function extendTemplate(array $sections, string $file, string $output): void + { + $inputFileHandle = fopen($file, 'r'); + if (!$inputFileHandle) { + throw new \Exception('Cannot open file ' . $file); + } + + $outputFileHandle = fopen($output, 'w'); + if (!$outputFileHandle) { + throw new \Exception('Cannot open file ' . $output . 'for writing.'); + } + + $lineNumber = 0; + while (($line = fgets($inputFileHandle)) !== false) { + ++$lineNumber; + + if (preg_match('/^\s*@yields\(\'([\w\/]+)\'\)\s*$/', $line, $matches) === 1) { + if (isset($sections[$matches[1]])) { + fwrite($outputFileHandle, $sections[$matches[1]]); + } + } else { + fwrite($outputFileHandle, $line); + } + } + + fclose($inputFileHandle); + fclose($outputFileHandle); + } + + private function writeFinal(array $extra, string $file, string $output): void + { + $dirname = pathinfo($output, PATHINFO_DIRNAME); + if (!is_dir($dirname)) { + mkdir($dirname, 0755, true); + } + + $inputFileHandle = fopen($file, 'r'); + if (!$inputFileHandle) { + throw new \Exception('Cannot open file ' . $file); + } + + $outputFileHandle = fopen($output, 'w'); + if (!$outputFileHandle) { + throw new \Exception('Cannot open file ' . $output . 'for writing.'); + } + + fwrite($outputFileHandle, $extra[0]); + while (($line = fgets($inputFileHandle)) !== false) { + fwrite($outputFileHandle, $line); + } + fwrite($outputFileHandle, $extra[1]); + + fclose($inputFileHandle); + fclose($outputFileHandle); + } + + private function generateAssets(ParsedFragment $fragment, array &$sections): void + { + foreach ($fragment->getCss() as $cssFile) { + $asset = $this->parseAsset($cssFile); + if (isset($asset['code'])) { + $sections['inlineCss'] .= '' . PHP_EOL; + } elseif (isset($asset['file'])) { + $sections['externalCss'] .= '' . PHP_EOL; + } + } + + foreach ($fragment->getJs() as $jsFile) { + $asset = $this->parseAsset($jsFile); + if (isset($asset['code'])) { + $sections['inlineJs'] .= '' . PHP_EOL; + } elseif (isset($asset['file'])) { + $sections['externalJs'] .= '' . PHP_EOL; + } + } + } + + private function parseAsset(string $asset): array + { + $output = []; + + if (preg_match('/^[\w\/\.]+$/', $asset) === 1) { + if ( + empty($_ENV['DEV']) && + filesize(ROOT . '/public/static/' . $asset) < self::INLINE_ASSET_LIMIT + ) { + $output['code'] = file_get_contents(ROOT . '/public/static/' . $asset); + } else { + $output['file'] = '/' . $asset . '?rev='; + } + } else { + $output['file'] = $asset; + } + + return $output; + } +} diff --git a/src/View/ParsedFragment.php b/src/View/ParsedFragment.php new file mode 100644 index 0000000..435e587 --- /dev/null +++ b/src/View/ParsedFragment.php @@ -0,0 +1,48 @@ +extends = $extends; + $this->css = $css; + $this->js = $js; + $this->sections = $sections; + $this->extra = $extra; + } + + public function getExtends(): ?string + { + return $this->extends; + } + + public function getCss(): array + { + return $this->css; + } + + public function getJs(): array + { + return $this->js; + } + + public function getSections(): array + { + return $this->sections; + } + + public function getExtra(): array + { + return $this->extra; + } +} diff --git a/src/View/Parser.php b/src/View/Parser.php new file mode 100644 index 0000000..179b347 --- /dev/null +++ b/src/View/Parser.php @@ -0,0 +1,159 @@ +file = $file; + } + + public function parse(): ParsedFragment + { + $sectionOpen = null; + $extraOpen = false; + + $extends = null; + $js = []; + $css = []; + $sections = []; + $extra = ['', '']; + + $fileHandle = fopen($this->file, 'r'); + if (!$fileHandle) { + throw new \Exception('Cannot open file ' . $this->file); + } + + $lineNumber = 0; + while (($line = fgets($fileHandle)) !== false) { + ++$lineNumber; + + if (($cssMatched = $this->matchCss($line)) !== null) { + $css[] = $cssMatched; + + continue; + } + + if (($jsMatched = $this->matchJs($line)) !== null) { + $js[] = $jsMatched; + + continue; + } + + if (($extendsMatched = $this->matchExtends($line)) !== null) { + if ($extends !== null) { + throw new \Exception('Error in file ' . $this->file . ' in line ' . $lineNumber . ' - There is already an \'@extends\' declared.'); + } + + $extends = $extendsMatched; + + continue; + } + + if (($sectionMatched = $this->matchSection($line)) !== null) { + if ($extends === null) { + throw new \Exception('Error in file ' . $this->file . ' in line ' . $lineNumber . ' - \'@section\' has no meaning if view extends nothing.'); + } + if ($sectionOpen !== null) { + throw new \Exception('Parse error in file ' . $this->file . ' in line ' . $lineNumber . ' - A \'@section\' is already open (no \'@endsection\' found).'); + } + + $sectionOpen = $sectionMatched; + $sections[$sectionOpen] = ''; + + continue; + } + + if ($this->matchEndSection($line)) { + if ($sectionOpen === null) { + throw new \Exception('Parse error in file ' . $this->file . ' in line ' . $lineNumber . ' - Cannot end section until no \'@section\' is open.'); + } + + $sectionOpen = null; + } + + if ($this->matchExtra($line)) { + if ($extraOpen) { + throw new \Exception('Parse error in file ' . $this->file . ' in line ' . $lineNumber . ' - An \'@extra\' is already open (no \'@endextra\' found).'); + } + + $extraOpen = true; + + continue; + } + + if ($this->matchEndExtra($line)) { + if (!$extraOpen) { + throw new \Exception('Parse error in file ' . $this->file . ' in line ' . $lineNumber . ' - Cannot end extra until no \'@extra\' is open.'); + } + + $extraOpen = false; + } + + if ($sectionOpen !== null) { + $sections[$sectionOpen] .= $line; + } + + if ($extraOpen) { + $offset = $extends === null ? 0 : 1; + $extra[$offset] .= $line; + } + } + + fclose($fileHandle); + + return new ParsedFragment($extends, $css, $js, $sections, $extra); + } + + private function matchCss(string $line): ?string + { + if (preg_match('/^\s*@css\((.*)\)\s*$/', $line, $matches) === 1) { + return $matches[1]; + } + + return null; + } + + private function matchJs(string $line): ?string + { + if (preg_match('/^\s*@js\((.*)\)\s*$/', $line, $matches) === 1) { + return $matches[1]; + } + + return null; + } + + private function matchExtends(string $line): ?string + { + if (preg_match('/^\s*@extends\(([\w\/]+)\)\s*$/', $line, $matches) === 1) { + return $matches[1]; + } + + return null; + } + + private function matchSection(string $line): ?string + { + if (preg_match('/^\s*@section\((\w+)\)\s*$/', $line, $matches) === 1) { + return $matches[1]; + } + + return null; + } + + private function matchEndSection(string $line): bool + { + return preg_match('/^\s*@endsection(?:\(\))?\s*$/', $line) === 1; + } + + private function matchExtra(string $line): bool + { + return preg_match('/^\s*@extra(?:\(\))?\s*$/', $line) === 1; + } + + private function matchEndExtra(string $line): bool + { + return preg_match('/^\s*@endextra(?:\(\))?\s*$/', $line) === 1; + } +} diff --git a/templates/app.php.tpl b/templates/app.php.tpl new file mode 100644 index 0000000..f3a2309 --- /dev/null +++ b/templates/app.php.tpl @@ -0,0 +1,23 @@ +load(); + +class Container +{ + static SokoWeb\Interfaces\Database\IConnection $dbConnection; + static SokoWeb\Routing\RouteCollection $routeCollection; + static SokoWeb\Interfaces\Session\ISessionHandler $sessionHandler; + static SokoWeb\Interfaces\Request\IRequest $request; +} + +Container::$dbConnection = new SokoWeb\Database\Mysql\Connection($_ENV['DB_HOST'], $_ENV['DB_USER'], $_ENV['DB_PASSWORD'], $_ENV['DB_NAME']); diff --git a/templates/cli.tpl b/templates/cli.tpl new file mode 100755 index 0000000..0fa3905 --- /dev/null +++ b/templates/cli.tpl @@ -0,0 +1,12 @@ +#!/usr/bin/env php +add(new {app}\Cli\MigrateDatabaseCommand()); +$app->add(new {app}\Cli\AddUserCommand()); +$app->add(new {app}\Cli\LinkViewCommand()); + +$app->run(); diff --git a/templates/database/database.sql.tpl b/templates/database/database.sql.tpl new file mode 100644 index 0000000..83f8ea7 --- /dev/null +++ b/templates/database/database.sql.tpl @@ -0,0 +1,15 @@ +SET NAMES utf8mb4; +SET foreign_key_checks = 0; + +CREATE TABLE `users` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `email` varchar(100) NOT NULL, + `password` varchar(60) DEFAULT NULL, + `type` enum('user','admin') NOT NULL, + `active` tinyint(1) NOT NULL DEFAULT 0, + `google_sub` varchar(255) CHARACTER SET ascii COLLATE ascii_bin DEFAULT NULL, + `created` timestamp NOT NULL DEFAULT current_timestamp(), + PRIMARY KEY (`id`), + UNIQUE KEY `email` (`email`), + UNIQUE KEY `google_sub` (`google_sub`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; diff --git a/templates/src/Cli/AddUserCommand.php.tpl b/templates/src/Cli/AddUserCommand.php.tpl new file mode 100644 index 0000000..599c710 --- /dev/null +++ b/templates/src/Cli/AddUserCommand.php.tpl @@ -0,0 +1,51 @@ +setName('user:add') + ->setDescription('Adding of user.') + ->addArgument('email', InputArgument::REQUIRED, 'Email of user') + ->addArgument('password', InputArgument::REQUIRED, 'Password of user') + ->addArgument('type', InputArgument::OPTIONAL, 'Type of user'); + } + + public function execute(InputInterface $input, OutputInterface $output): int + { + $user = new User(); + $user->setEmail($input->getArgument('email')); + $user->setPlainPassword($input->getArgument('password')); + $user->setActive(true); + $user->setCreatedDate(new DateTime()); + + if ($input->hasArgument('type') && $input->getArgument('type') !== null) { + $user->setType($input->getArgument('type')); + } + + try { + $pdm = new PersistentDataManager(); + $pdm->saveToDb($user); + } catch (\Exception $e) { + $output->writeln('Adding user failed!'); + $output->writeln(''); + + $output->writeln((string) $e); + $output->writeln(''); + + return 1; + } + + $output->writeln('User was successfully added!'); + + return 0; + } +} diff --git a/templates/src/Cli/LinkViewCommand.php.tpl b/templates/src/Cli/LinkViewCommand.php.tpl new file mode 100644 index 0000000..cb7a3d8 --- /dev/null +++ b/templates/src/Cli/LinkViewCommand.php.tpl @@ -0,0 +1,69 @@ +setName('view:link') + ->setDescription('Linking of views.') + ->addArgument('view', InputArgument::OPTIONAL, 'View file to be linked.'); + } + + public function execute(InputInterface $input, OutputInterface $output): int + { + $views = []; + + $view = $input->getArgument('view'); + + if ($view !== null) { + $views[] = $view; + } else { + $folder = ROOT . '/views'; + $folderLength = strlen($folder) + 1; + + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($folder, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST); + + foreach ($iterator as $file) { + if ($file->isDir() || $file->getExtension() !== 'php') { + continue; + } + + $view = substr($file->getPath(), $folderLength) . '/' . $file->getBasename('.php'); + + if (strpos($view, 'templates') === 0 || strpos($view, 'tests') === 0) { + continue; + } + + $views[] = $view; + } + } + + try { + foreach ($views as $view) { + $generator = new Linker($view); + $generator->generate(); + } + } catch (\Exception $e) { + $output->writeln('Linking view(s) failed!'); + $output->writeln(''); + + $output->writeln((string) $e); + $output->writeln(''); + + return 1; + } + + $output->writeln('View(s) successfully linked!'); + + return 0; + } +} diff --git a/templates/src/Cli/MigrateDatabaseCommand.php.tpl b/templates/src/Cli/MigrateDatabaseCommand.php.tpl new file mode 100644 index 0000000..73f2977 --- /dev/null +++ b/templates/src/Cli/MigrateDatabaseCommand.php.tpl @@ -0,0 +1,122 @@ +setName('db:migrate') + ->setDescription('Migration of database changes.'); + } + + public function execute(InputInterface $input, OutputInterface $output): int + { + $db = \Container::$dbConnection; + + $db->startTransaction(); + + $success = []; + try { + foreach ($this->readDir('structure') as $file) { + $db->multiQuery(file_get_contents($file)); + + $success[] = $this->saveToDB($file, 'structure'); + } + + foreach ($this->readDir('data') as $file) { + require $file; + + $success[] = $this->saveToDB($file, 'data'); + } + } catch (\Exception $e) { + $db->rollback(); + + $output->writeln('Migration failed!'); + $output->writeln(''); + + $output->writeln((string) $e); + $output->writeln(''); + + return 1; + } + + $db->commit(); + + $output->writeln('Migration was successful!'); + $output->writeln(''); + + if (count($success) > 0) { + foreach ($success as $migration) { + $output->writeln($migration); + } + + $output->writeln(''); + } + + return 0; + } + + private function readDir(string $type): array + { + $done = []; + + $migrationTableExists = \Container::$dbConnection->query('SELECT count(*) + FROM information_schema.tables + WHERE table_schema = \'' . $_ENV['DB_NAME'] . '\' + AND table_name = \'migrations\';') + ->fetch(IResultSet::FETCH_NUM)[0]; + + if ($migrationTableExists != 0) { + $select = new Select(\Container::$dbConnection, 'migrations'); + $select->columns(['migration']); + $select->where('type', '=', $type); + $select->orderBy('migration'); + + $result = $select->execute(); + + while ($migration = $result->fetch(IResultSet::FETCH_ASSOC)) { + $done[] = $migration['migration']; + } + } + + $path = ROOT . '/database/migrations/' . $type; + $dir = opendir($path); + + if ($dir === false) { + throw new \Exception('Cannot open dir: ' . $path); + } + + $files = []; + while ($file = readdir($dir)) { + $filePath = $path . '/' . $file; + + if (!is_file($filePath) || in_array(pathinfo($file, PATHINFO_FILENAME), $done)) { + continue; + } + + $files[] = $filePath; + } + + natsort($files); + + return $files; + } + + private function saveToDB(string $file, string $type): string + { + $baseName = pathinfo($file, PATHINFO_FILENAME); + + $modify = new Modify(\Container::$dbConnection, 'migrations'); + $modify->set('migration', $baseName); + $modify->set('type', $type); + $modify->save(); + + return $baseName . ' (' . $type . ')'; + } +} diff --git a/templates/src/Controller/HomeController.php.tpl b/templates/src/Controller/HomeController.php.tpl new file mode 100644 index 0000000..74f6d1f --- /dev/null +++ b/templates/src/Controller/HomeController.php.tpl @@ -0,0 +1,20 @@ +request = $request; + } + + public function getIndex(): IContent + { + return new HtmlContent('index'); + } +} diff --git a/templates/src/PersistentData/Model/User.php.tpl b/templates/src/PersistentData/Model/User.php.tpl new file mode 100644 index 0000000..16906d8 --- /dev/null +++ b/templates/src/PersistentData/Model/User.php.tpl @@ -0,0 +1,133 @@ +email = $email; + } + + public function setPassword(?string $hashedPassword): void + { + $this->password = $hashedPassword; + } + + public function setPlainPassword(string $plainPassword): void + { + $this->password = password_hash($plainPassword, PASSWORD_BCRYPT); + } + + public function setType(string $type): void + { + if (in_array($type, self::$types)) { + $this->type = $type; + } + } + + public function setActive(bool $active): void + { + $this->active = $active; + } + + public function setGoogleSub(?string $googleSub): void + { + $this->googleSub = $googleSub; + } + + public function setCreatedDate(DateTime $created): void + { + $this->created = $created; + } + + public function setCreated(string $created): void + { + $this->created = new DateTime($created); + } + + public function getEmail(): string + { + return $this->email; + } + + public function getPassword(): ?string + { + return $this->password; + } + + public function getType(): string + { + return $this->type; + } + + public function getActive(): bool + { + return $this->active; + } + + public function getGoogleSub(): ?string + { + return $this->googleSub; + } + + public function getCreatedDate(): DateTime + { + return $this->created; + } + + public function getCreated(): string + { + return $this->created->format('Y-m-d H:i:s'); + } + + public function hasPermission(int $permission): bool + { + switch ($permission) { + case IUser::PERMISSION_NORMAL: + return true; + case IUser::PERMISSION_ADMIN: + return $this->type === 'admin'; + default: + throw new \Exception('Permission does not exist: ' . $permission); + } + } + + public function getUniqueId() + { + return $this->id; + } + + public function getDisplayName(): string + { + return $this->email; + } + + public function checkPassword(string $password): bool + { + if ($this->password === null) { + return false; + } + + return password_verify($password, $this->password); + } +} diff --git a/templates/views/index.php.tpl b/templates/views/index.php.tpl new file mode 100644 index 0000000..9e6c154 --- /dev/null +++ b/templates/views/index.php.tpl @@ -0,0 +1,11 @@ + + + + + + {app} + + +

Hello World! This is {app}.

+ + diff --git a/templates/web.php.tpl b/templates/web.php.tpl new file mode 100644 index 0000000..1a97365 --- /dev/null +++ b/templates/web.php.tpl @@ -0,0 +1,56 @@ +get('index', '', [{app}\Controller\HomeController::class, 'getIndex']); + +if (isset($_COOKIE['COOKIES_CONSENT'])) { + Container::$sessionHandler = new SokoWeb\Session\DatabaseSessionHandler(); + + session_set_save_handler(Container::$sessionHandler, true); + session_start([ + 'gc_probability' => 0, // old sessions are deleted by MaintainDatabaseCommand + 'cookie_lifetime' => 604800, + 'cookie_path' => '/', + 'cookie_httponly' => true, + 'cookie_samesite' => 'Lax' + ]); + + if (isset($_COOKIE[session_name()])) { + // extend session cookie lifetime is cookie already exists + setcookie(session_name(), session_id(), [ + 'expires' => time() + 604800, + 'path' => '/', + 'httponly' => true, + 'samesite' => 'Lax' + ]); + } + + // this is needed to handle old type of session IDs + if (!Container::$sessionHandler->validateId(session_id())) { + session_regenerate_id(true); + } +} else { + $_SESSION = []; +} + +Container::$request = new MapGuesser\Request\Request( + $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST'], + $_GET, + $_POST, + $_SESSION, + new MapGuesser\Repository\UserRepository() +); + +if (!Container::$request->session()->has('anti_csrf_token')) { + Container::$request->session()->set('anti_csrf_token', bin2hex(random_bytes(16))); +} diff --git a/tests/OAuth/GoogleOAuthTest.php b/tests/OAuth/GoogleOAuthTest.php new file mode 100644 index 0000000..7a04d62 --- /dev/null +++ b/tests/OAuth/GoogleOAuthTest.php @@ -0,0 +1,86 @@ +getMockBuilder(IRequest::class) + ->setMethods(['setUrl', 'setMethod', 'setQuery', 'setHeaders', 'send']) + ->getMock(); + $googleOAuth = new GoogleOAuth($requestMock); + + $dialogUrl = $googleOAuth->getDialogUrl($state, $redirectUrl, $nonce); + $dialogUrlParsed = explode('?', $dialogUrl); + + $this->assertEquals('https://accounts.google.com/o/oauth2/v2/auth', $dialogUrlParsed[0]); + + parse_str($dialogUrlParsed[1], $dialogUrlQueryParams); + + $expectedQueryParams = [ + 'response_type' => 'code', + 'client_id' => $_ENV['GOOGLE_OAUTH_CLIENT_ID'], + 'scope' => 'openid email', + 'redirect_uri' => $redirectUrl, + 'state' => $state, + 'nonce' => $nonce, + ]; + + $this->assertEquals($expectedQueryParams, $dialogUrlQueryParams); + } + + public function testCanRequestToken(): void + { + $_ENV['GOOGLE_OAUTH_CLIENT_ID'] = 'abc'; + $_ENV['GOOGLE_OAUTH_CLIENT_SECRET'] = 'xxx'; + $code = 'code_from_google'; + $redirectUrl = 'http://example.com/oauth'; + + $requestMock = $this->getMockBuilder(IRequest::class) + ->setMethods(['setUrl', 'setMethod', 'setQuery', 'setHeaders', 'send']) + ->getMock(); + $responseMock = $this->getMockBuilder(IResponse::class) + ->setMethods(['getBody', 'getHeaders']) + ->getMock(); + $googleOAuth = new GoogleOAuth($requestMock); + + $expectedQueryParams = [ + 'code' => $code, + 'client_id' => $_ENV['GOOGLE_OAUTH_CLIENT_ID'], + 'client_secret' => $_ENV['GOOGLE_OAUTH_CLIENT_SECRET'], + 'redirect_uri' => $redirectUrl, + 'grant_type' => 'authorization_code', + ]; + + $requestMock->expects($this->once()) + ->method('setUrl') + ->with($this->equalTo('https://oauth2.googleapis.com/token')); + $requestMock->expects($this->once()) + ->method('setMethod') + ->with($this->equalTo(IRequest::HTTP_POST)); + $requestMock->expects($this->once()) + ->method('setQuery') + ->with($this->equalTo($expectedQueryParams)); + $requestMock->expects($this->once()) + ->method('send') + ->will($this->returnValue($responseMock)); + $responseMock->expects($this->once()) + ->method('getBody') + ->will($this->returnValue('{"test":"json"}')); + + $token = $googleOAuth->getToken($code, $redirectUrl); + + $this->assertEquals(['test' => 'json'], $token); + } +} diff --git a/tests/PersistentData/Model/ModelTest.php b/tests/PersistentData/Model/ModelTest.php new file mode 100644 index 0000000..9feee9e --- /dev/null +++ b/tests/PersistentData/Model/ModelTest.php @@ -0,0 +1,93 @@ + OtherModel::class]; + + private string $name; + + private bool $valid; + + public function setName(string $name): void + { + $this->name = $name; + } + + public function setValid(bool $valid): void + { + $this->valid = $valid; + } + + public function getName(): string + { + return $this->name; + } + + public function getValid(): bool + { + return $this->valid; + } +} + +final class ModelTest extends TestCase +{ + public function testCanReturnTable(): void + { + $this->assertEquals('test_table', DummyModel::getTable()); + } + + public function testCanReturnFields(): void + { + $this->assertEquals(['id', 'name', 'valid'], DummyModel::getFields()); + } + + public function testCanReturnRelations(): void + { + $this->assertEquals(['other_model' => OtherModel::class], DummyModel::getRelations()); + } + + public function testCanBeConvertedToArray(): void + { + $model = new DummyModel(); + $model->setId(123); + $model->setName('John'); + $model->setValid(true); + + $this->assertEquals([ + 'id' => 123, + 'name' => 'John', + 'valid' => true + ], $model->toArray()); + } + + public function testCanSaveAndResetSnapshot(): void + { + $model = new DummyModel(); + $model->setId(123); + $model->setName('John'); + $model->setValid(true); + + $model->saveSnapshot(); + + $this->assertEquals([ + 'id' => 123, + 'name' => 'John', + 'valid' => true + ], $model->getSnapshot()); + + $model->resetSnapshot(); + + $this->assertEquals([], $model->getSnapshot()); + } +} diff --git a/tests/Util/JwtParserTest.php b/tests/Util/JwtParserTest.php new file mode 100644 index 0000000..c519e8c --- /dev/null +++ b/tests/Util/JwtParserTest.php @@ -0,0 +1,51 @@ +jwtParser = new JwtParser( + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c' + ); + } + + public function testSettingTokenIsTheSameAsCreatingWithToken(): void + { + $jwtParser2 = new JwtParser(); + $jwtParser2->setToken( + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c' + ); + + $this->assertEquals($this->jwtParser, $jwtParser2); + } + + public function testCanParseTokenHeader(): void + { + $this->assertEquals([ + 'alg' => 'HS256', + 'typ' => 'JWT' + ], $this->jwtParser->getHeader()); + } + + public function testCanParseTokenPayload(): void + { + $this->assertEquals([ + 'sub' => '1234567890', + 'name' => 'John Doe', + 'iat' => 1516239022 + ], $this->jwtParser->getPayload()); + } + + public function testCanParseTokenSignature(): void + { + $this->assertEquals( + '49f94ac7044948c78a285d904f87f0a4c7897f7e8f3a4eb2255fda750b2cc397', + bin2hex($this->jwtParser->getSignature()) + ); + } +} diff --git a/tests/View/ParserTest.php b/tests/View/ParserTest.php new file mode 100644 index 0000000..f4e22cb --- /dev/null +++ b/tests/View/ParserTest.php @@ -0,0 +1,144 @@ +parse(); + + $expected = new ParsedFragment( + null, + [], + [], + [], + ['', ''] + ); + + $this->assertEquals($expected, $fragment); + } + + public function testCanParseViewWithAssets(): void + { + $file = realpath(self::TEST_VIEWS_PATH . '/view_with_assets.php'); + + $parser = new Parser($file); + $fragment = $parser->parse(); + + $expected = new ParsedFragment( + null, + [ + 'test.css' + ], + [ + 'test.js', + 'test_' + ], + [], + ['', ''] + ); + + $this->assertEquals($expected, $fragment); + } + + public function testCanParseViewWithExtends(): void + { + $file = realpath(self::TEST_VIEWS_PATH . '/view_with_extends.php'); + + $parser = new Parser($file); + $fragment = $parser->parse(); + + $expected = new ParsedFragment( + 'parent', + [], + [], + [ + 'section1' => '
Test HTML with @extends
' . "\n" + ], + ['', ''] + ); + + $this->assertEquals($expected, $fragment); + } + + public function testCanParseComplexView(): void + { + $file = realpath(self::TEST_VIEWS_PATH . '/view_complex.php'); + + $parser = new Parser($file); + $fragment = $parser->parse(); + + $expected = new ParsedFragment( + 'parent', + [ + 'test1.css' + ], + [ + 'test1.js', + 'test2_' + ], + [ + 'section1' => '
Test HTML with @extends - section 1
' . "\n", + 'section2' => '
Test HTML with @extends - section 2
' . "\n" + ], + [ + '' . "\n", + '' . "\n" . 'EXTRA' . "\n", + ] + ); + + $this->assertEquals($expected, $fragment); + } + + public function testFailsIfMultipleExtendsGiven(): void + { + $file = realpath(self::TEST_VIEWS_PATH . '/view_invalid_multiple_extends.php'); + + $parser = new Parser($file); + + $this->expectExceptionMessage('Error in file ' . $file . ' in line 3 - There is already an \'@extends\' declared.'); + + $parser->parse(); + } + + public function testFailsIfSectionWithoutExtendsGiven(): void + { + $file = realpath(self::TEST_VIEWS_PATH . '/view_invalid_section_without_extends.php'); + + $parser = new Parser($file); + + $this->expectExceptionMessage('Error in file ' . $file . ' in line 1 - \'@section\' has no meaning if view extends nothing.'); + + $parser->parse(); + } + + public function testFailsIfOpeningSectionBeforePreviousClosed(): void + { + $file = realpath(self::TEST_VIEWS_PATH . '/view_invalid_multiple_sections_open.php'); + + $parser = new Parser($file); + + $this->expectExceptionMessage('Parse error in file ' . $file . ' in line 4 - A \'@section\' is already open (no \'@endsection\' found).'); + + $parser->parse(); + } + + public function testFailsIfClosingSectionWhenNoSectionIsOpen(): void + { + $file = realpath(self::TEST_VIEWS_PATH . '/view_invalid_section_not_open.php'); + + $parser = new Parser($file); + + $this->expectExceptionMessage('Parse error in file ' . $file . ' in line 4 - Cannot end section until no \'@section\' is open.'); + + $parser->parse(); + } +}