diff --git a/week-03/project/.gitignore b/week-03/project/.gitignore new file mode 100644 index 0000000..91d907b --- /dev/null +++ b/week-03/project/.gitignore @@ -0,0 +1,28 @@ +# IDE & System Related Files +.buildpath +.project +.settings +.DS_Store +.idea +.vscode +.docker +/docker-compose.yml +/nbproject + +# Local System File +/octoconfig.php +/.htaccess +/web.config +/php.ini + +# Vendor directory handling +/libraries/vendor + +# OSX +._* +.Spotlight-V100 +.Trashes + +# Windows +Thumbs.db +Desktop.ini \ No newline at end of file diff --git a/week-03/project/LICENSE b/week-03/project/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/week-03/project/LICENSE @@ -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/week-03/project/administrator/includes/app.php b/week-03/project/administrator/includes/app.php new file mode 100644 index 0000000..ecdaf99 --- /dev/null +++ b/week-03/project/administrator/includes/app.php @@ -0,0 +1,63 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +defined('_JEXEC') or die; + +// Saves the start time and memory usage. +$startTime = microtime(1); +$startMem = memory_get_usage(); + +if (file_exists(dirname(__DIR__) . '/defines.php')) +{ + include_once dirname(__DIR__) . '/defines.php'; +} + +if (!defined('_JDEFINES')) +{ + define('JPATH_BASE', dirname(__DIR__)); + require_once JPATH_BASE . '/includes/defines.php'; +} + +// Check for presence of vendor dependencies not included in the git repository +if (!file_exists(JPATH_LIBRARIES . '/vendor/autoload.php') || !is_dir(JPATH_ROOT . '/media/vendor')) +{ + echo file_get_contents(JPATH_ROOT . '/templates/system/build_incomplete.html'); + + exit; +} + +require_once JPATH_BASE . '/includes/framework.php'; + +// Set profiler start time and memory usage and mark afterLoad in the profiler. +JDEBUG ? \Joomla\CMS\Profiler\Profiler::getInstance('Application')->setStart($startTime, $startMem)->mark('afterLoad') : null; + +// Boot the DI container +$container = \Joomla\CMS\Factory::getContainer(); + +/* + * Alias the session service keys to the web session service as that is the primary session backend for this application + * + * In addition to aliasing "common" service keys, we also create aliases for the PHP classes to ensure autowiring objects + * is supported. This includes aliases for aliased class names, and the keys for aliased class names should be considered + * deprecated to be removed when the class name alias is removed as well. + */ +$container->alias('session.web', 'session.web.administrator') + ->alias('session', 'session.web.administrator') + ->alias('JSession', 'session.web.administrator') + ->alias(\Joomla\CMS\Session\Session::class, 'session.web.administrator') + ->alias(\Joomla\Session\Session::class, 'session.web.administrator') + ->alias(\Joomla\Session\SessionInterface::class, 'session.web.administrator'); + +// Instantiate the application. +$app = $container->get(\Joomla\CMS\Application\AdministratorApplication::class); + +// Set the application as global app +\Joomla\CMS\Factory::$application = $app; + +// Execute the application. +$app->execute(); diff --git a/week-03/project/administrator/includes/defines.php b/week-03/project/administrator/includes/defines.php new file mode 100644 index 0000000..e43728b --- /dev/null +++ b/week-03/project/administrator/includes/defines.php @@ -0,0 +1,27 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +defined('_JEXEC') or die; + +// Global definitions +$parts = explode(DIRECTORY_SEPARATOR, JPATH_BASE); +array_pop($parts); + +// Defines +define('JPATH_ROOT', implode(DIRECTORY_SEPARATOR, $parts)); +define('JPATH_SITE', JPATH_ROOT); +define('JPATH_CONFIGURATION', JPATH_ROOT); +define('JPATH_ADMINISTRATOR', JPATH_ROOT . DIRECTORY_SEPARATOR . 'administrator'); +define('JPATH_LIBRARIES', JPATH_ROOT . DIRECTORY_SEPARATOR . 'libraries'); +define('JPATH_PLUGINS', JPATH_ROOT . DIRECTORY_SEPARATOR . 'plugins'); +define('JPATH_INSTALLATION', JPATH_ROOT . DIRECTORY_SEPARATOR . 'installation'); +define('JPATH_THEMES', JPATH_BASE . DIRECTORY_SEPARATOR . 'templates'); +define('JPATH_CACHE', JPATH_ADMINISTRATOR . DIRECTORY_SEPARATOR . 'cache'); +define('JPATH_MANIFESTS', JPATH_ADMINISTRATOR . DIRECTORY_SEPARATOR . 'manifests'); +define('JPATH_API', JPATH_ROOT . DIRECTORY_SEPARATOR . 'api'); +define('JPATH_CLI', JPATH_ROOT . DIRECTORY_SEPARATOR . 'cli'); diff --git a/week-03/project/administrator/includes/framework.php b/week-03/project/administrator/includes/framework.php new file mode 100644 index 0000000..10cf971 --- /dev/null +++ b/week-03/project/administrator/includes/framework.php @@ -0,0 +1,120 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +defined('_JEXEC') or die; + +use Joomla\CMS\Version; +use Joomla\Utilities\IpHelper; + +// System includes +require_once JPATH_LIBRARIES . '/bootstrap.php'; + +// Installation check, and check on removal of the install directory. +if (!file_exists(JPATH_CONFIGURATION . '/configuration.php') + || (filesize(JPATH_CONFIGURATION . '/configuration.php') < 10) + || (file_exists(JPATH_INSTALLATION . '/index.php') && (false === (new Version)->isInDevelopmentState()))) +{ + if (file_exists(JPATH_INSTALLATION . '/index.php')) + { + header('Location: ../installation/index.php'); + + exit(); + } + else + { + echo 'No configuration file found and no installation code available. Exiting...'; + + exit; + } +} + +// Pre-Load configuration. Don't remove the Output Buffering due to BOM issues, see JCode 26026 +ob_start(); +require_once JPATH_CONFIGURATION . '/configuration.php'; +ob_end_clean(); + +// System configuration. +$config = new JConfig; + +// Set the error_reporting, and adjust a global Error Handler +switch ($config->error_reporting) +{ + case 'default': + case '-1': + + break; + + case 'none': + case '0': + error_reporting(0); + + break; + + case 'simple': + error_reporting(E_ERROR | E_WARNING | E_PARSE); + ini_set('display_errors', 1); + + break; + + case 'maximum': + case 'development': // <= Stays for backward compatibility, @TODO: can be removed in 5.0 + error_reporting(E_ALL); + ini_set('display_errors', 1); + + break; + + default: + error_reporting($config->error_reporting); + ini_set('display_errors', 1); + + break; +} + +define('JDEBUG', $config->debug); + +// Check deprecation logging +if (empty($config->log_deprecated)) +{ + // Reset handler for E_USER_DEPRECATED + set_error_handler(null, E_USER_DEPRECATED); +} +else +{ + // Make sure handler for E_USER_DEPRECATED is registered + set_error_handler(['Joomla\CMS\Exception\ExceptionHandler', 'handleUserDeprecatedErrors'], E_USER_DEPRECATED); +} + +if (JDEBUG || $config->error_reporting === 'maximum') +{ + // Set new Exception handler with debug enabled + $errorHandler->setExceptionHandler( + [ + new \Symfony\Component\ErrorHandler\ErrorHandler(null, true), + 'renderException' + ] + ); +} + +/** + * Correctly set the allowing of IP Overrides if behind a trusted proxy/load balancer. + * + * We need to do this as high up the stack as we can, as the default in \Joomla\Utilities\IpHelper is to + * $allowIpOverride = true which is the wrong default for a generic site NOT behind a trusted proxy/load balancer. + */ +if (property_exists($config, 'behind_loadbalancer') && $config->behind_loadbalancer == 1) +{ + // If Joomla is configured to be behind a trusted proxy/load balancer, allow HTTP Headers to override the REMOTE_ADDR + IpHelper::setAllowIpOverrides(true); +} +else +{ + // We disable the allowing of IP overriding using headers by default. + IpHelper::setAllowIpOverrides(false); +} + +unset($config); diff --git a/week-03/project/administrator/includes/index.html b/week-03/project/administrator/includes/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/week-03/project/administrator/includes/index.html @@ -0,0 +1 @@ + diff --git a/week-03/project/administrator/index.php b/week-03/project/administrator/index.php new file mode 100644 index 0000000..6514d2f --- /dev/null +++ b/week-03/project/administrator/index.php @@ -0,0 +1,34 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +// NOTE: This file should remain compatible with PHP 5.2 to allow us to run our PHP minimum check and show a friendly error message + +// Define the application's minimum supported PHP version as a constant so it can be referenced within the application. +define('JOOMLA_MINIMUM_PHP', '7.2.5'); + +if (version_compare(PHP_VERSION, JOOMLA_MINIMUM_PHP, '<')) +{ + die( + str_replace( + '{{phpversion}}', + JOOMLA_MINIMUM_PHP, + file_get_contents(dirname(__FILE__) . '/../templates/system/incompatible.html') + ) + ); +} + +/** + * Constant that is checked in included files to prevent direct access. + * define() is used rather than "const" to not error for PHP 5.2 and lower + */ +define('_JEXEC', 1); + +// Run the application - All executable code should be triggered through this file +require_once dirname(__FILE__) . '/includes/app.php'; + + diff --git a/week-03/project/htaccess.txt b/week-03/project/htaccess.txt new file mode 100644 index 0000000..b05371d --- /dev/null +++ b/week-03/project/htaccess.txt @@ -0,0 +1,196 @@ +## +# @copyright Copyright (C) 2014 - 2017 Open Source Matters, Inc. All rights reserved. +# @license http://www.gnu.org/licenses/gpl-2.0.txt GNU General Public License Version 2 or Later +## + +########################################### +# ======= Set basic Security header ======= + +# Just enable the rules on the live site and exclude the dev sites + + + # X-XSS-Protection + Header always set X-XSS-Protection "1; mode=block" + # X-Frame-Options + Header always set X-Frame-Options DENY + # X-Content-Type nosniff + Header always set X-Content-Type-Options nosniff + # Referrer Policy + Header always set Referrer-Policy "no-referrer-when-downgrade" + # Strict-Transport-Security + Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains" + # Cross-Origin-Opener-Policy + Header always set Cross-Origin-Opener-Policy "same-origin" + # Content-Security-Policy + Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'sha256-IxJ2MRv31XGmZD5ovlgSBrPmMjftYTJ3OM9/kLh6nBo=' 'sha256-QIsD5w9F5MPp3vlmsPHmbv0GZjN9ja3BuhcwpaWZuUw=' https://*.google-analytics.com https://www.googletagmanager.com https://*.pingdom.net; style-src 'self' 'sha256-4Su6mBWzEIFnH4pAGMOuaeBrstwJN4Z3pq/s1Kn4/KQ=' https://*.joomla.org https://fonts.googleapis.com; connect-src 'self' https://*.pingdom.net https://*.doubleclick.net https://*.google-analytics.com; frame-src 'self' https://*.googletagmanager.com; font-src 'self' https://fonts.gstatic.com https://*.joomla.org; img-src 'self' https://*.google-analytics.com https://*.googletagmanager.com https://*.joomla.org https://*.pingdom.net https://*.doubleclick.net https://*.githubusercontent.com https://*.travis-ci.org https://travis-ci.org; frame-ancestors 'self'; report-uri https://joomla.report-uri.com/r/t/csp/enforce" + + + +########################################### + +########################################### +# ======= Enable the Rewrite Engine ======= + +RewriteEngine On + +########################################### + + +########################################### +# ======= No directory listings ======= + +IndexIgnore * +Options +FollowSymLinks +Options -Indexes + +########################################### + + +########################################### +# === Optimal default expiration time === + + + # Enable expiration control + ExpiresActive On + + # Default expiration: 1 hour after request + ExpiresDefault "now plus 1 hour" + + # CSS and JS expiration: 1 week after request + ExpiresByType text/css "now plus 1 week" + ExpiresByType application/javascript "now plus 1 week" + ExpiresByType application/x-javascript "now plus 1 week" + + # Image files expiration: 1 month after request + ExpiresByType image/bmp "now plus 1 month" + ExpiresByType image/gif "now plus 1 month" + ExpiresByType image/jpeg "now plus 1 month" + ExpiresByType image/jp2 "now plus 1 month" + ExpiresByType image/pipeg "now plus 1 month" + ExpiresByType image/png "now plus 1 month" + ExpiresByType image/svg+xml "now plus 1 month" + ExpiresByType image/tiff "now plus 1 month" + ExpiresByType image/vnd.microsoft.icon "now plus 1 month" + ExpiresByType image/x-icon "now plus 1 month" + ExpiresByType image/ico "now plus 1 month" + ExpiresByType image/icon "now plus 1 month" + ExpiresByType text/ico "now plus 1 month" + ExpiresByType application/ico "now plus 1 month" + ExpiresByType image/vnd.wap.wbmp "now plus 1 month" + ExpiresByType application/vnd.wap.wbxml "now plus 1 month" + ExpiresByType application/smil "now plus 1 month" + + # Audio files expiration: 1 month after request + ExpiresByType audio/basic "now plus 1 month" + ExpiresByType audio/mid "now plus 1 month" + ExpiresByType audio/midi "now plus 1 month" + ExpiresByType audio/mpeg "now plus 1 month" + ExpiresByType audio/x-aiff "now plus 1 month" + ExpiresByType audio/x-mpegurl "now plus 1 month" + ExpiresByType audio/x-pn-realaudio "now plus 1 month" + ExpiresByType audio/x-wav "now plus 1 month" + + # Movie files expiration: 1 month after request + ExpiresByType application/x-shockwave-flash "now plus 1 month" + ExpiresByType x-world/x-vrml "now plus 1 month" + ExpiresByType video/x-msvideo "now plus 1 month" + ExpiresByType video/mpeg "now plus 1 month" + ExpiresByType video/mp4 "now plus 1 month" + ExpiresByType video/quicktime "now plus 1 month" + ExpiresByType video/x-la-asf "now plus 1 month" + ExpiresByType video/x-ms-asf "now plus 1 month" + +########################################### + + +########################################### +# == Automatic compression of resources == + + + AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css application/xml application/xhtml+xml application/rss+xml application/javascript application/x-javascript + + + mod_gzip_on Yes + mod_gzip_dechunk Yes + mod_gzip_keep_workfiles No + mod_gzip_can_negotiate Yes + mod_gzip_add_header_count Yes + mod_gzip_send_vary Yes + mod_gzip_min_http 1000 + mod_gzip_minimum_file_size 300 + mod_gzip_maximum_file_size 512000 + mod_gzip_maximum_inmem_size 60000 + mod_gzip_handle_methods GET + mod_gzip_item_include file \.(html?|txt|css|js|php|pl|xml|rb|py)$ + mod_gzip_item_include mime ^text/plain$ + mod_gzip_item_include mime ^text/html$ + mod_gzip_item_include mime ^text/xml$ + mod_gzip_item_include mime ^text/css$ + mod_gzip_item_include mime ^application/xml$ + mod_gzip_item_include mime ^application/xhtml+xml$ + mod_gzip_item_include mime ^application/rss+xml$ + mod_gzip_item_include mime ^application/javascript$ + mod_gzip_item_include mime ^application/x-javascript$ + mod_gzip_item_exclude rspheader ^Content-Encoding:.*gzip.* + mod_gzip_item_include handler ^cgi-script$ + mod_gzip_item_include handler ^server-status$ + mod_gzip_item_include handler ^server-info$ + mod_gzip_item_include handler ^application/x-httpd-php + mod_gzip_item_exclude mime ^image/.* + +########################################### + + +########################################### +# ======== Remove multiple slashes ======== + +RewriteCond %{HTTP_HOST} !="" +RewriteCond %{THE_REQUEST} ^[A-Z]+\s//+(.*)\sHTTP/[0-9.]+$ [OR] +RewriteCond %{THE_REQUEST} ^[A-Z]+\s(.*/)/+\sHTTP/[0-9.]+$ +RewriteRule .* http://%{HTTP_HOST}/%1 [R=301,L] + +########################################### + + +########################################### +# ======== Remove trailing slashes ======== + +RewriteCond %{REQUEST_FILENAME} !-d +RewriteRule ^(.*)/$ /$1 [R=301,L] + +########################################### + + +########################################### +# ======== Redirect HTTP to HTTPS ========= + +#RewriteCond %{HTTPS} off +#RewriteCond %{HTTP_HOST} ^framework.joomla.org$ [NC] +#RewriteRule ^(.*)$ https://framework.joomla.org/$1 [R=301,L,QSA] + +########################################### + + +########################################### +# ======== Redirect removed pages ========= + +#RewriteCond %{REQUEST_URI} ^/about +#RewriteRule ^(.*)$ /contributors [R=301,L,QSA] + +########################################### + + +########################################### +# ======== SEF URL Routing ======== + +# If the request is not for a static asset +RewriteCond %{REQUEST_URI} !^/media/ + +# Or for a file that exists in the web directory +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d + +# Rewrite the request to run the application +RewriteRule (.*) index.php + +########################################### \ No newline at end of file diff --git a/week-03/project/includes/app.php b/week-03/project/includes/app.php new file mode 100644 index 0000000..15fdbdd --- /dev/null +++ b/week-03/project/includes/app.php @@ -0,0 +1,63 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +defined('_JEXEC') or die; + +// Saves the start time and memory usage. +$startTime = microtime(1); +$startMem = memory_get_usage(); + +if (file_exists(dirname(__DIR__) . '/defines.php')) +{ + include_once dirname(__DIR__) . '/defines.php'; +} + +if (!defined('_JDEFINES')) +{ + define('JPATH_BASE', dirname(__DIR__)); + require_once JPATH_BASE . '/includes/defines.php'; +} + +// Check for presence of vendor dependencies not included in the git repository +if (!file_exists(JPATH_LIBRARIES . '/vendor/autoload.php') || !is_dir(JPATH_ROOT . '/media/vendor')) +{ + echo file_get_contents(JPATH_ROOT . '/templates/system/build_incomplete.html'); + + exit; +} + +require_once JPATH_BASE . '/includes/framework.php'; + +// Set profiler start time and memory usage and mark afterLoad in the profiler. +JDEBUG && \Joomla\CMS\Profiler\Profiler::getInstance('Application')->setStart($startTime, $startMem)->mark('afterLoad'); + +// Boot the DI container +$container = \Joomla\CMS\Factory::getContainer(); + +/* + * Alias the session service keys to the web session service as that is the primary session backend for this application + * + * In addition to aliasing "common" service keys, we also create aliases for the PHP classes to ensure autowiring objects + * is supported. This includes aliases for aliased class names, and the keys for aliased class names should be considered + * deprecated to be removed when the class name alias is removed as well. + */ +$container->alias('session.web', 'session.web.site') + ->alias('session', 'session.web.site') + ->alias('JSession', 'session.web.site') + ->alias(\Joomla\CMS\Session\Session::class, 'session.web.site') + ->alias(\Joomla\Session\Session::class, 'session.web.site') + ->alias(\Joomla\Session\SessionInterface::class, 'session.web.site'); + +// Instantiate the application. +$app = $container->get(\Joomla\CMS\Application\SiteApplication::class); + +// Set the application as global app +\Joomla\CMS\Factory::$application = $app; + +// Execute the application. +$app->execute(); diff --git a/week-03/project/includes/defines.php b/week-03/project/includes/defines.php new file mode 100644 index 0000000..1b89ecb --- /dev/null +++ b/week-03/project/includes/defines.php @@ -0,0 +1,26 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +defined('_JEXEC') or die; + +// Global definitions +$parts = explode(DIRECTORY_SEPARATOR, JPATH_BASE); + +// Defines. +define('JPATH_ROOT', implode(DIRECTORY_SEPARATOR, $parts)); +define('JPATH_SITE', JPATH_ROOT); +define('JPATH_CONFIGURATION', JPATH_ROOT); +define('JPATH_ADMINISTRATOR', JPATH_ROOT . DIRECTORY_SEPARATOR . 'administrator'); +define('JPATH_LIBRARIES', JPATH_ROOT . DIRECTORY_SEPARATOR . 'libraries'); +define('JPATH_PLUGINS', JPATH_ROOT . DIRECTORY_SEPARATOR . 'plugins'); +define('JPATH_INSTALLATION', JPATH_ROOT . DIRECTORY_SEPARATOR . 'installation'); +define('JPATH_THEMES', JPATH_BASE . DIRECTORY_SEPARATOR . 'templates'); +define('JPATH_CACHE', JPATH_ADMINISTRATOR . DIRECTORY_SEPARATOR . 'cache'); +define('JPATH_MANIFESTS', JPATH_ADMINISTRATOR . DIRECTORY_SEPARATOR . 'manifests'); +define('JPATH_API', JPATH_ROOT . DIRECTORY_SEPARATOR . 'api'); +define('JPATH_CLI', JPATH_ROOT . DIRECTORY_SEPARATOR . 'cli'); diff --git a/week-03/project/includes/framework.php b/week-03/project/includes/framework.php new file mode 100644 index 0000000..94d5e7f --- /dev/null +++ b/week-03/project/includes/framework.php @@ -0,0 +1,123 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +defined('_JEXEC') or die; + +use Joomla\CMS\Version; +use Joomla\Utilities\IpHelper; + +// System includes +require_once JPATH_LIBRARIES . '/bootstrap.php'; + +// Installation check, and check on removal of the install directory. +if (!file_exists(JPATH_CONFIGURATION . '/configuration.php') + || (filesize(JPATH_CONFIGURATION . '/configuration.php') < 10) + || (file_exists(JPATH_INSTALLATION . '/index.php') && (false === (new Version)->isInDevelopmentState()))) +{ + if (file_exists(JPATH_INSTALLATION . '/index.php')) + { + header('Location: ' . substr($_SERVER['REQUEST_URI'], 0, strpos($_SERVER['REQUEST_URI'], 'index.php')) . 'installation/index.php'); + + exit; + } + else + { + echo 'No configuration file found and no installation code available. Exiting...'; + + exit; + } +} + +// Pre-Load configuration. Don't remove the Output Buffering due to BOM issues, see JCode 26026 +ob_start(); +require_once JPATH_CONFIGURATION . '/configuration.php'; +ob_end_clean(); + +// System configuration. +$config = new JConfig; + +// Set the error_reporting, and adjust a global Error Handler +switch ($config->error_reporting) +{ + case 'default': + case '-1': + + break; + + case 'none': + case '0': + error_reporting(0); + + break; + + case 'simple': + error_reporting(E_ERROR | E_WARNING | E_PARSE); + ini_set('display_errors', 1); + + break; + + case 'maximum': + case 'development': // <= Stays for backward compatibility, @TODO: can be removed in 5.0 + error_reporting(E_ALL); + ini_set('display_errors', 1); + + break; + + default: + error_reporting($config->error_reporting); + ini_set('display_errors', 1); + + break; +} + +if (!defined('JDEBUG')) +{ + define('JDEBUG', $config->debug); +} + +// Check deprecation logging +if (empty($config->log_deprecated)) +{ + // Reset handler for E_USER_DEPRECATED + set_error_handler(null, E_USER_DEPRECATED); +} +else +{ + // Make sure handler for E_USER_DEPRECATED is registered + set_error_handler(['Joomla\CMS\Exception\ExceptionHandler', 'handleUserDeprecatedErrors'], E_USER_DEPRECATED); +} + +if (JDEBUG || $config->error_reporting === 'maximum') +{ + // Set new Exception handler with debug enabled + $errorHandler->setExceptionHandler( + [ + new \Symfony\Component\ErrorHandler\ErrorHandler(null, true), + 'renderException' + ] + ); +} + +/** + * Correctly set the allowing of IP Overrides if behind a trusted proxy/load balancer. + * + * We need to do this as high up the stack as we can, as the default in \Joomla\Utilities\IpHelper is to + * $allowIpOverride = true which is the wrong default for a generic site NOT behind a trusted proxy/load balancer. + */ +if (property_exists($config, 'behind_loadbalancer') && $config->behind_loadbalancer == 1) +{ + // If Joomla is configured to be behind a trusted proxy/load balancer, allow HTTP Headers to override the REMOTE_ADDR + IpHelper::setAllowIpOverrides(true); +} +else +{ + // We disable the allowing of IP overriding using headers by default. + IpHelper::setAllowIpOverrides(false); +} + +unset($config); diff --git a/week-03/project/includes/index.html b/week-03/project/includes/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/week-03/project/includes/index.html @@ -0,0 +1 @@ + diff --git a/week-03/project/index.php b/week-03/project/index.php new file mode 100644 index 0000000..c2b6893 --- /dev/null +++ b/week-03/project/index.php @@ -0,0 +1,32 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +// NOTE: This file should remain compatible with PHP 5.2 to allow us to run our PHP minimum check and show a friendly error message + +// Define the application's minimum supported PHP version as a constant so it can be referenced within the application. +define('JOOMLA_MINIMUM_PHP', '7.2.5'); + +if (version_compare(PHP_VERSION, JOOMLA_MINIMUM_PHP, '<')) +{ + die( + str_replace( + '{{phpversion}}', + JOOMLA_MINIMUM_PHP, + file_get_contents(dirname(__FILE__) . '/templates/system/incompatible.html') + ) + ); +} + +/** + * Constant that is checked in included files to prevent direct access. + * define() is used rather than "const" to not error for PHP 5.2 and lower + */ +define('_JEXEC', 1); + +// Run the application - All executable code should be triggered through this file +require_once dirname(__FILE__) . '/includes/app.php'; diff --git a/week-03/project/libraries/.htaccess b/week-03/project/libraries/.htaccess new file mode 100644 index 0000000..9afb1a1 --- /dev/null +++ b/week-03/project/libraries/.htaccess @@ -0,0 +1,9 @@ +# Apache 2.4+ + + Require all denied + + +# Apache 2.0-2.2 + + Deny from all + diff --git a/week-03/project/libraries/bootstrap.php b/week-03/project/libraries/bootstrap.php new file mode 100644 index 0000000..54a5976 --- /dev/null +++ b/week-03/project/libraries/bootstrap.php @@ -0,0 +1,90 @@ + + * @license GNU General Public License version 2 or later; see LICENSE + */ + +defined('_JEXEC') or die; + +// Set the platform root path as a constant if necessary. +defined('JPATH_PLATFORM') or define('JPATH_PLATFORM', __DIR__); + +// Detect the native operating system type. +$os = strtoupper(substr(PHP_OS, 0, 3)); + +defined('IS_WIN') or define('IS_WIN', ($os === 'WIN')); +defined('IS_UNIX') or define('IS_UNIX', (($os !== 'MAC') && ($os !== 'WIN'))); + +// Import the library loader if necessary. +if (!class_exists('JLoader')) +{ + require_once JPATH_PLATFORM . '/loader.php'; + + // If JLoader still does not exist panic. + if (!class_exists('JLoader')) + { + throw new RuntimeException('Joomla Platform not loaded.'); + } +} + +// Setup the autoloaders. +JLoader::setup(); + +// Create the Composer autoloader +/** @var \Composer\Autoload\ClassLoader $loader */ +$loader = require JPATH_LIBRARIES . '/vendor/autoload.php'; + +// We need to pull our decorated class loader into memory before unregistering Composer's loader +class_exists('\\Joomla\\CMS\\Autoload\\ClassLoader'); + +$loader->unregister(); + +// Decorate Composer autoloader +spl_autoload_register([new \Joomla\CMS\Autoload\ClassLoader($loader), 'loadClass'], true, true); + +// Register the class aliases for Framework classes that have replaced their Platform equivalents +require_once JPATH_LIBRARIES . '/classmap.php'; + +/** + * Register the global exception handler. And set error level to server default error level. + * The error level may be changed later in boot up process, after application config will be loaded. + * Do not remove the variable, to allow to use it further, after including this file. + */ +$errorHandler = \Symfony\Component\ErrorHandler\ErrorHandler::register(); +\Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer::setTemplate(__DIR__ . '/../templates/system/fatal.php'); + +// Register the error handler which processes E_USER_DEPRECATED errors +if (error_reporting() & E_USER_DEPRECATED) +{ + set_error_handler(['Joomla\CMS\Exception\ExceptionHandler', 'handleUserDeprecatedErrors'], E_USER_DEPRECATED); +} + +// Suppress phar stream wrapper for non .phar files +$behavior = new \TYPO3\PharStreamWrapper\Behavior; +\TYPO3\PharStreamWrapper\Manager::initialize( + $behavior->withAssertion(new \TYPO3\PharStreamWrapper\Interceptor\PharExtensionInterceptor) +); + +if (in_array('phar', stream_get_wrappers())) +{ + stream_wrapper_unregister('phar'); + stream_wrapper_register('phar', 'TYPO3\\PharStreamWrapper\\PharStreamWrapper'); +} + +// Define the Joomla version if not already defined. +defined('JVERSION') or define('JVERSION', (new \Joomla\CMS\Version)->getShortVersion()); + +// Set up the message queue logger for web requests +if (array_key_exists('REQUEST_METHOD', $_SERVER)) +{ + \Joomla\CMS\Log\Log::addLogger(['logger' => 'messagequeue'], \Joomla\CMS\Log\Log::ALL, ['jerror']); +} + +// Register the Crypto lib +JLoader::register('Crypto', JPATH_PLATFORM . '/php-encryption/Crypto.php'); + +// Register the PasswordHash library. +JLoader::register('PasswordHash', JPATH_PLATFORM . '/phpass/PasswordHash.php'); diff --git a/week-03/project/libraries/loader.php b/week-03/project/libraries/loader.php new file mode 100644 index 0000000..2389dca --- /dev/null +++ b/week-03/project/libraries/loader.php @@ -0,0 +1,790 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +defined('JPATH_PLATFORM') or die; + +/** + * Static class to handle loading of libraries. + * + * @since 1.7.0 + */ +abstract class JLoader +{ + /** + * Container for already imported library paths. + * + * @var array + * @since 1.7.0 + */ + protected static $classes = array(); + + /** + * Container for already imported library paths. + * + * @var array + * @since 1.7.0 + */ + protected static $imported = array(); + + /** + * Container for registered library class prefixes and path lookups. + * + * @var array + * @since 3.0.0 + */ + protected static $prefixes = array(); + + /** + * Holds proxy classes and the class names the proxy. + * + * @var array + * @since 3.2 + */ + protected static $classAliases = array(); + + /** + * Holds the inverse lookup for proxy classes and the class names the proxy. + * + * @var array + * @since 3.4 + */ + protected static $classAliasesInverse = array(); + + /** + * Container for namespace => path map. + * + * @var array + * @since 3.1.4 + */ + protected static $namespaces = array(); + + /** + * Holds a reference for all deprecated aliases (mainly for use by a logging platform). + * + * @var array + * @since 3.6.3 + */ + protected static $deprecatedAliases = array(); + + /** + * The root folders where extensions can be found. + * + * @var array + * @since 4.0.0 + */ + protected static $extensionRootFolders = array(); + + /** + * Method to discover classes of a given type in a given path. + * + * @param string $classPrefix The class name prefix to use for discovery. + * @param string $parentPath Full path to the parent folder for the classes to discover. + * @param boolean $force True to overwrite the autoload path value for the class if it already exists. + * @param boolean $recurse Recurse through all child directories as well as the parent path. + * + * @return void + * + * @since 1.7.0 + * @deprecated 5.0 Classes should be autoloaded. Use JLoader::registerPrefix() or JLoader::registerNamespace() to register an autoloader for + * your files. + */ + public static function discover($classPrefix, $parentPath, $force = true, $recurse = false) + { + try + { + if ($recurse) + { + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($parentPath), + RecursiveIteratorIterator::SELF_FIRST + ); + } + else + { + $iterator = new DirectoryIterator($parentPath); + } + + /** @type $file DirectoryIterator */ + foreach ($iterator as $file) + { + $fileName = $file->getFilename(); + + // Only load for php files. + if ($file->isFile() && $file->getExtension() === 'php') + { + // Get the class name and full path for each file. + $class = strtolower($classPrefix . preg_replace('#\.php$#', '', $fileName)); + + // Register the class with the autoloader if not already registered or the force flag is set. + if ($force || empty(self::$classes[$class])) + { + self::register($class, $file->getPath() . '/' . $fileName); + } + } + } + } + catch (UnexpectedValueException $e) + { + // Exception will be thrown if the path is not a directory. Ignore it. + } + } + + /** + * Method to get the list of registered classes and their respective file paths for the autoloader. + * + * @return array The array of class => path values for the autoloader. + * + * @since 1.7.0 + */ + public static function getClassList() + { + return self::$classes; + } + + /** + * Method to get the list of deprecated class aliases. + * + * @return array An associative array with deprecated class alias data. + * + * @since 3.6.3 + */ + public static function getDeprecatedAliases() + { + return self::$deprecatedAliases; + } + + /** + * Method to get the list of registered namespaces. + * + * @return array The array of namespace => path values for the autoloader. + * + * @since 3.1.4 + */ + public static function getNamespaces() + { + return self::$namespaces; + } + + /** + * Loads a class from specified directories. + * + * @param string $key The class name to look for (dot notation). + * @param string $base Search this directory for the class. + * + * @return boolean True on success. + * + * @since 1.7.0 + * @deprecated 5.0 Classes should be autoloaded. Use JLoader::registerPrefix() or JLoader::registerNamespace() to register an autoloader for + * your files. + */ + public static function import($key, $base = null) + { + // Only import the library if not already attempted. + if (!isset(self::$imported[$key])) + { + // Setup some variables. + $success = false; + $parts = explode('.', $key); + $class = array_pop($parts); + $base = (!empty($base)) ? $base : __DIR__; + $path = str_replace('.', DIRECTORY_SEPARATOR, $key); + + // Handle special case for helper classes. + if ($class === 'helper') + { + $class = ucfirst(array_pop($parts)) . ucfirst($class); + } + // Standard class. + else + { + $class = ucfirst($class); + } + + // If we are importing a library from the Joomla namespace set the class to autoload. + if (strpos($path, 'joomla') === 0) + { + // Since we are in the Joomla namespace prepend the classname with J. + $class = 'J' . $class; + + // Only register the class for autoloading if the file exists. + if (is_file($base . '/' . $path . '.php')) + { + self::$classes[strtolower($class)] = $base . '/' . $path . '.php'; + $success = true; + } + } + /* + * If we are not importing a library from the Joomla namespace directly include the + * file since we cannot assert the file/folder naming conventions. + */ + else + { + // If the file exists attempt to include it. + if (is_file($base . '/' . $path . '.php')) + { + $success = (bool) include_once $base . '/' . $path . '.php'; + } + } + + // Add the import key to the memory cache container. + self::$imported[$key] = $success; + } + + return self::$imported[$key]; + } + + /** + * Load the file for a class. + * + * @param string $class The class to be loaded. + * + * @return boolean True on success + * + * @since 1.7.0 + */ + public static function load($class) + { + // Sanitize class name. + $key = strtolower($class); + + // If the class already exists do nothing. + if (class_exists($class, false)) + { + return true; + } + + // If the class is registered include the file. + if (isset(self::$classes[$key])) + { + $found = (bool) include_once self::$classes[$key]; + + if ($found) + { + self::loadAliasFor($class); + } + + // If the class doesn't exists, we probably have a class alias available + if (!class_exists($class, false)) + { + // Search the alias class, first none namespaced and then namespaced + $original = array_search($class, self::$classAliases) ? : array_search('\\' . $class, self::$classAliases); + + // When we have an original and the class exists an alias should be created + if ($original && class_exists($original, false)) + { + class_alias($original, $class); + } + } + + return true; + } + + return false; + } + + /** + * Directly register a class to the autoload list. + * + * @param string $class The class name to register. + * @param string $path Full path to the file that holds the class to register. + * @param boolean $force True to overwrite the autoload path value for the class if it already exists. + * + * @return void + * + * @since 1.7.0 + * @deprecated 5.0 Classes should be autoloaded. Use JLoader::registerPrefix() or JLoader::registerNamespace() to register an autoloader for + * your files. + */ + public static function register($class, $path, $force = true) + { + // When an alias exists, register it as well + if (array_key_exists(strtolower($class), self::$classAliases)) + { + self::register(self::stripFirstBackslash(self::$classAliases[strtolower($class)]), $path, $force); + } + + // Sanitize class name. + $class = strtolower($class); + + // Only attempt to register the class if the name and file exist. + if (!empty($class) && is_file($path)) + { + // Register the class with the autoloader if not already registered or the force flag is set. + if ($force || empty(self::$classes[$class])) + { + self::$classes[$class] = $path; + } + } + } + + /** + * Register a class prefix with lookup path. This will allow developers to register library + * packages with different class prefixes to the system autoloader. More than one lookup path + * may be registered for the same class prefix, but if this method is called with the reset flag + * set to true then any registered lookups for the given prefix will be overwritten with the current + * lookup path. When loaded, prefix paths are searched in a "last in, first out" order. + * + * @param string $prefix The class prefix to register. + * @param string $path Absolute file path to the library root where classes with the given prefix can be found. + * @param boolean $reset True to reset the prefix with only the given lookup path. + * @param boolean $prepend If true, push the path to the beginning of the prefix lookup paths array. + * + * @return void + * + * @throws RuntimeException + * + * @since 3.0.0 + */ + public static function registerPrefix($prefix, $path, $reset = false, $prepend = false) + { + // Verify the library path exists. + if (!is_dir($path)) + { + $path = (str_replace(JPATH_ROOT, '', $path) == $path) ? basename($path) : str_replace(JPATH_ROOT, '', $path); + + throw new RuntimeException('Library path ' . $path . ' cannot be found.', 500); + } + + // If the prefix is not yet registered or we have an explicit reset flag then set set the path. + if ($reset || !isset(self::$prefixes[$prefix])) + { + self::$prefixes[$prefix] = array($path); + } + // Otherwise we want to simply add the path to the prefix. + else + { + if ($prepend) + { + array_unshift(self::$prefixes[$prefix], $path); + } + else + { + self::$prefixes[$prefix][] = $path; + } + } + } + + /** + * Offers the ability for "just in time" usage of `class_alias()`. + * You cannot overwrite an existing alias. + * + * @param string $alias The alias name to register. + * @param string $original The original class to alias. + * @param string|boolean $version The version in which the alias will no longer be present. + * + * @return boolean True if registration was successful. False if the alias already exists. + * + * @since 3.2 + */ + public static function registerAlias($alias, $original, $version = false) + { + // PHP is case insensitive so support all kind of alias combination + $lowercasedAlias = strtolower($alias); + + if (!isset(self::$classAliases[$lowercasedAlias])) + { + self::$classAliases[$lowercasedAlias] = $original; + + $original = self::stripFirstBackslash($original); + + if (!isset(self::$classAliasesInverse[$original])) + { + self::$classAliasesInverse[$original] = array($lowercasedAlias); + } + else + { + self::$classAliasesInverse[$original][] = $lowercasedAlias; + } + + // If given a version, log this alias as deprecated + if ($version) + { + self::$deprecatedAliases[] = array('old' => $alias, 'new' => $original, 'version' => $version); + } + + return true; + } + + return false; + } + + /** + * Register a namespace to the autoloader. When loaded, namespace paths are searched in a "last in, first out" order. + * + * @param string $namespace A case sensitive Namespace to register. + * @param string $path A case sensitive absolute file path to the library root where classes of the given namespace can be found. + * @param boolean $reset True to reset the namespace with only the given lookup path. + * @param boolean $prepend If true, push the path to the beginning of the namespace lookup paths array. + * + * @return void + * + * @throws RuntimeException + * + * @since 3.1.4 + */ + public static function registerNamespace($namespace, $path, $reset = false, $prepend = false) + { + // Verify the library path exists. + if (!is_dir($path)) + { + $path = (str_replace(JPATH_ROOT, '', $path) == $path) ? basename($path) : str_replace(JPATH_ROOT, '', $path); + + throw new RuntimeException('Library path ' . $path . ' cannot be found.', 500); + } + + // Trim leading and trailing backslashes from namespace, allowing "\Parent\Child", "Parent\Child\" and "\Parent\Child\" to be treated the same way. + $namespace = trim($namespace, '\\'); + + // If the namespace is not yet registered or we have an explicit reset flag then set the path. + if ($reset || !isset(self::$namespaces[$namespace])) + { + self::$namespaces[$namespace] = array($path); + } + + // Otherwise we want to simply add the path to the namespace. + else + { + if ($prepend) + { + array_unshift(self::$namespaces[$namespace], $path); + } + else + { + self::$namespaces[$namespace][] = $path; + } + } + } + + /** + * Method to setup the autoloaders for the Joomla Platform. + * Since the SPL autoloaders are called in a queue we will add our explicit + * class-registration based loader first, then fall back on the autoloader based on conventions. + * This will allow people to register a class in a specific location and override platform libraries + * as was previously possible. + * + * @param boolean $enablePsr True to enable autoloading based on PSR-0. + * @param boolean $enablePrefixes True to enable prefix based class loading (needed to auto load the Joomla core). + * @param boolean $enableClasses True to enable class map based class loading (needed to auto load the Joomla core). + * + * @return void + * + * @since 3.1.4 + */ + public static function setup($enablePsr = true, $enablePrefixes = true, $enableClasses = true) + { + if ($enableClasses) + { + // Register the class map based autoloader. + spl_autoload_register(array('JLoader', 'load')); + } + + if ($enablePrefixes) + { + // Register the prefix autoloader. + spl_autoload_register(array('JLoader', '_autoload')); + } + + if ($enablePsr) + { + // Register the PSR based autoloader. + spl_autoload_register(array('JLoader', 'loadByPsr')); + spl_autoload_register(array('JLoader', 'loadByAlias')); + } + } + + /** + * Method to autoload classes that are namespaced to the PSR-4 standard. + * + * @param string $class The fully qualified class name to autoload. + * + * @return boolean True on success, false otherwise. + * + * @since 3.7.0 + * @deprecated 5.0 Use JLoader::loadByPsr instead + */ + public static function loadByPsr4($class) + { + return self::loadByPsr($class); + } + + /** + * Method to autoload classes that are namespaced to the PSR-4 standard. + * + * @param string $class The fully qualified class name to autoload. + * + * @return boolean True on success, false otherwise. + * + * @since 4.0.0 + */ + public static function loadByPsr($class) + { + $class = self::stripFirstBackslash($class); + + // Find the location of the last NS separator. + $pos = strrpos($class, '\\'); + + // If one is found, we're dealing with a NS'd class. + if ($pos !== false) + { + $classPath = str_replace('\\', DIRECTORY_SEPARATOR, substr($class, 0, $pos)) . DIRECTORY_SEPARATOR; + $className = substr($class, $pos + 1); + } + // If not, no need to parse path. + else + { + $classPath = null; + $className = $class; + } + + $classPath .= $className . '.php'; + + // Loop through registered namespaces until we find a match. + foreach (self::$namespaces as $ns => $paths) + { + if (strpos($class, "{$ns}\\") === 0) + { + $nsPath = trim(str_replace('\\', DIRECTORY_SEPARATOR, $ns), DIRECTORY_SEPARATOR); + + // Loop through paths registered to this namespace until we find a match. + foreach ($paths as $path) + { + $classFilePath = realpath($path . DIRECTORY_SEPARATOR . substr_replace($classPath, '', 0, strlen($nsPath) + 1)); + + // We do not allow files outside the namespace root to be loaded + if (strpos($classFilePath, realpath($path)) !== 0) + { + continue; + } + + // We check for class_exists to handle case-sensitive file systems + if (is_file($classFilePath) && !class_exists($class, false)) + { + $found = (bool) include_once $classFilePath; + + if ($found) + { + self::loadAliasFor($class); + } + + return $found; + } + } + } + } + + return false; + } + + /** + * Method to autoload classes that have been aliased using the registerAlias method. + * + * @param string $class The fully qualified class name to autoload. + * + * @return boolean True on success, false otherwise. + * + * @since 3.2 + */ + public static function loadByAlias($class) + { + $class = strtolower(self::stripFirstBackslash($class)); + + if (isset(self::$classAliases[$class])) + { + // Force auto-load of the regular class + class_exists(self::$classAliases[$class], true); + + // Normally this shouldn't execute as the autoloader will execute applyAliasFor when the regular class is + // auto-loaded above. + if (!class_exists($class, false) && !interface_exists($class, false)) + { + class_alias(self::$classAliases[$class], $class); + } + } + } + + /** + * Applies a class alias for an already loaded class, if a class alias was created for it. + * + * @param string $class We'll look for and register aliases for this (real) class name + * + * @return void + * + * @since 3.4 + */ + public static function applyAliasFor($class) + { + $class = self::stripFirstBackslash($class); + + if (isset(self::$classAliasesInverse[$class])) + { + foreach (self::$classAliasesInverse[$class] as $alias) + { + class_alias($class, $alias); + } + } + } + + /** + * Autoload a class based on name. + * + * @param string $class The class to be loaded. + * + * @return boolean True if the class was loaded, false otherwise. + * + * @since 1.7.3 + */ + public static function _autoload($class) + { + foreach (self::$prefixes as $prefix => $lookup) + { + $chr = strlen($prefix) < strlen($class) ? $class[strlen($prefix)] : 0; + + if (strpos($class, $prefix) === 0 && ($chr === strtoupper($chr))) + { + return self::_load(substr($class, strlen($prefix)), $lookup); + } + } + + return false; + } + + /** + * Load a class based on name and lookup array. + * + * @param string $class The class to be loaded (without prefix). + * @param array $lookup The array of base paths to use for finding the class file. + * + * @return boolean True if the class was loaded, false otherwise. + * + * @since 3.0.0 + */ + private static function _load($class, $lookup) + { + // Split the class name into parts separated by camelCase. + $parts = preg_split('/(?<=[a-z0-9])(?=[A-Z])/x', $class); + $partsCount = count($parts); + + foreach ($lookup as $base) + { + // Generate the path based on the class name parts. + $path = realpath($base . '/' . implode('/', array_map('strtolower', $parts)) . '.php'); + + // Load the file if it exists and is in the lookup path. + if (strpos($path, realpath($base)) === 0 && is_file($path)) + { + $found = (bool) include_once $path; + + if ($found) + { + self::loadAliasFor($class); + } + + return $found; + } + + // Backwards compatibility patch + + // If there is only one part we want to duplicate that part for generating the path. + if ($partsCount === 1) + { + // Generate the path based on the class name parts. + $path = realpath($base . '/' . implode('/', array_map('strtolower', array($parts[0], $parts[0]))) . '.php'); + + // Load the file if it exists and is in the lookup path. + if (strpos($path, realpath($base)) === 0 && is_file($path)) + { + $found = (bool) include_once $path; + + if ($found) + { + self::loadAliasFor($class); + } + + return $found; + } + } + } + + return false; + } + + /** + * Loads the aliases for the given class. + * + * @param string $class The class. + * + * @return void + * + * @since 3.8.0 + */ + private static function loadAliasFor($class) + { + if (!array_key_exists($class, self::$classAliasesInverse)) + { + return; + } + + foreach (self::$classAliasesInverse[$class] as $alias) + { + // Force auto-load of the alias class + class_exists($alias, true); + } + } + + /** + * Strips the first backslash from the given class if present. + * + * @param string $class The class to strip the first prefix from. + * + * @return string The striped class name. + * + * @since 3.8.0 + */ + private static function stripFirstBackslash($class) + { + return $class && $class[0] === '\\' ? substr($class, 1) : $class; + } +} + +// Check if jexit is defined first (our unit tests mock this) +if (!function_exists('jexit')) +{ + /** + * Global application exit. + * + * This function provides a single exit point for the platform. + * + * @param mixed $message Exit code or string. Defaults to zero. + * + * @return void + * + * @codeCoverageIgnore + * @since 1.7.0 + */ + function jexit($message = 0) + { + exit($message); + } +} + +/** + * Intelligent file importer. + * + * @param string $path A dot syntax path. + * @param string $base Search this directory for the class. + * + * @return boolean True on success. + * + * @since 1.7.0 + * @deprecated 5.0 Classes should be autoloaded. Use JLoader::registerPrefix() or JLoader::registerNamespace() to register an autoloader for + * your files. + */ +function jimport($path, $base = null) +{ + return JLoader::import($path, $base); +} diff --git a/week-03/project/libraries/src/Application/AdminApplication.php b/week-03/project/libraries/src/Application/AdminApplication.php new file mode 100644 index 0000000..ff6b81c --- /dev/null +++ b/week-03/project/libraries/src/Application/AdminApplication.php @@ -0,0 +1,85 @@ +controllerResolver = $controllerResolver; + $this->router = $router; + + // Call the constructor as late as possible (it runs `initialise`). + parent::__construct($input, $config, $client, $response); + } + + /** + * Method to run the application routines. + * + * @return void + */ + protected function doExecute(): void + { + $route = $this->router->parseRoute($this->get('uri.route'), $this->input->getMethod()); + + // Add variables to the input if not already set + foreach ($route->getRouteVariables() as $key => $value) + { + $this->input->def($key, $value); + } + + \call_user_func($this->controllerResolver->resolve($route)); + } +} diff --git a/week-03/project/libraries/src/Application/IdentityAwareTrait.php b/week-03/project/libraries/src/Application/IdentityAwareTrait.php new file mode 100644 index 0000000..03152ac --- /dev/null +++ b/week-03/project/libraries/src/Application/IdentityAwareTrait.php @@ -0,0 +1,80 @@ + + * @license GNU General Public License version 2 or later; see LICENSE + */ + +namespace Joomla\CMS\Application; + +\defined('JPATH_PLATFORM') or die; + +use Joomla\CMS\User\User; +use Joomla\CMS\User\UserFactoryInterface; + +/** + * Trait for application classes which are identity (user) aware + * + * @since 4.0.0 + */ +trait IdentityAware +{ + /** + * The application identity object. + * + * @var User + * @since 4.0.0 + */ + protected $identity; + + /** + * UserFactoryInterface + * + * @var UserFactoryInterface + * @since 4.0.0 + */ + private $userFactory; + + /** + * Get the application identity. + * + * @return User + * + * @since 4.0.0 + */ + public function getIdentity() + { + return $this->identity; + } + + /** + * Allows the application to load a custom or default identity. + * + * @param User $identity An optional identity object. If omitted, a null user object is created. + * + * @return $this + * + * @since 4.0.0 + */ + public function loadIdentity(User $identity = null) + { + $this->identity = $identity ?: $this->userFactory->loadUserById(0); + + return $this; + } + + /** + * Set the user factory to use. + * + * @param UserFactoryInterface $userFactory The user factory to use + * + * @return void + * + * @since 4.0.0 + */ + public function setUserFactory(UserFactoryInterface $userFactory) + { + $this->userFactory = $userFactory; + } +} diff --git a/week-03/project/libraries/src/Application/SiteApplication.php b/week-03/project/libraries/src/Application/SiteApplication.php new file mode 100644 index 0000000..ff6b81c --- /dev/null +++ b/week-03/project/libraries/src/Application/SiteApplication.php @@ -0,0 +1,85 @@ +controllerResolver = $controllerResolver; + $this->router = $router; + + // Call the constructor as late as possible (it runs `initialise`). + parent::__construct($input, $config, $client, $response); + } + + /** + * Method to run the application routines. + * + * @return void + */ + protected function doExecute(): void + { + $route = $this->router->parseRoute($this->get('uri.route'), $this->input->getMethod()); + + // Add variables to the input if not already set + foreach ($route->getRouteVariables() as $key => $value) + { + $this->input->def($key, $value); + } + + \call_user_func($this->controllerResolver->resolve($route)); + } +} diff --git a/week-03/project/libraries/src/Asset/MixPathPackage.php b/week-03/project/libraries/src/Asset/MixPathPackage.php new file mode 100644 index 0000000..a1626e7 --- /dev/null +++ b/week-03/project/libraries/src/Asset/MixPathPackage.php @@ -0,0 +1,70 @@ +decoratedPackage = $decoratedPackage; + } + + /** + * Returns an absolute or root-relative public path. + * + * @param string $path A path + * + * @return string The public path + */ + public function getUrl($path) + { + if ($this->isAbsoluteUrl($path)) + { + return $path; + } + + $versionedPath = $this->getVersionStrategy()->applyVersion("/$path"); + + if ($versionedPath === $path) + { + return $this->decoratedPackage->getUrl($path); + } + + return $this->getBasePath() . ltrim($versionedPath, '/'); + } +} diff --git a/week-03/project/libraries/src/Autoload/ClassLoader.php b/week-03/project/libraries/src/Autoload/ClassLoader.php new file mode 100644 index 0000000..918028e --- /dev/null +++ b/week-03/project/libraries/src/Autoload/ClassLoader.php @@ -0,0 +1,63 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Autoload; + +\defined('_JEXEC') or die; + +use Composer\Autoload\ClassLoader as ComposerClassLoader; + +/** + * Decorate Composer ClassLoader for Joomla! + * + * For backward compatibility due to class aliasing in the CMS, the loadClass() method was modified to call + * the JLoader::applyAliasFor() method. + * + * @since 3.4 + */ +class ClassLoader +{ + /** + * The Composer class loader + * + * @var ComposerClassLoader + * @since 3.4 + */ + private $loader; + + /** + * Constructor + * + * @param ComposerClassLoader $loader Composer autoloader + * + * @since 3.4 + */ + public function __construct(ComposerClassLoader $loader) + { + $this->loader = $loader; + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * + * @return boolean|null True if loaded, null otherwise + * + * @since 3.4 + */ + public function loadClass($class) + { + if ($result = $this->loader->loadClass($class)) + { + \JLoader::applyAliasFor($class); + } + + return $result; + } +} diff --git a/week-03/project/libraries/src/Controller/DashboardController.php b/week-03/project/libraries/src/Controller/DashboardController.php new file mode 100644 index 0000000..58bcc79 --- /dev/null +++ b/week-03/project/libraries/src/Controller/DashboardController.php @@ -0,0 +1,62 @@ +view = $view; + } + + /** + * Execute the controller. + * + * @return boolean + */ + public function execute(): bool + { + // Enable browser caching + $this->getApplication()->allowCache(true); + + $this->view->setPackage($this->getInput()->getString('package')); + + $this->getApplication()->setResponse(new HtmlResponse($this->view->render())); + + return true; + } +} diff --git a/week-03/project/libraries/src/Controller/HomepageController.php b/week-03/project/libraries/src/Controller/HomepageController.php new file mode 100644 index 0000000..58bcc79 --- /dev/null +++ b/week-03/project/libraries/src/Controller/HomepageController.php @@ -0,0 +1,62 @@ +view = $view; + } + + /** + * Execute the controller. + * + * @return boolean + */ + public function execute(): bool + { + // Enable browser caching + $this->getApplication()->allowCache(true); + + $this->view->setPackage($this->getInput()->getString('package')); + + $this->getApplication()->setResponse(new HtmlResponse($this->view->render())); + + return true; + } +} diff --git a/week-03/project/libraries/src/Controller/LoginController.php b/week-03/project/libraries/src/Controller/LoginController.php new file mode 100644 index 0000000..58bcc79 --- /dev/null +++ b/week-03/project/libraries/src/Controller/LoginController.php @@ -0,0 +1,62 @@ +view = $view; + } + + /** + * Execute the controller. + * + * @return boolean + */ + public function execute(): bool + { + // Enable browser caching + $this->getApplication()->allowCache(true); + + $this->view->setPackage($this->getInput()->getString('package')); + + $this->getApplication()->setResponse(new HtmlResponse($this->view->render())); + + return true; + } +} diff --git a/week-03/project/libraries/src/Controller/PageController.php b/week-03/project/libraries/src/Controller/PageController.php new file mode 100644 index 0000000..58bcc79 --- /dev/null +++ b/week-03/project/libraries/src/Controller/PageController.php @@ -0,0 +1,62 @@ +view = $view; + } + + /** + * Execute the controller. + * + * @return boolean + */ + public function execute(): bool + { + // Enable browser caching + $this->getApplication()->allowCache(true); + + $this->view->setPackage($this->getInput()->getString('package')); + + $this->getApplication()->setResponse(new HtmlResponse($this->view->render())); + + return true; + } +} diff --git a/week-03/project/libraries/src/Controller/WrongCmsController.php b/week-03/project/libraries/src/Controller/WrongCmsController.php new file mode 100644 index 0000000..8ab023e --- /dev/null +++ b/week-03/project/libraries/src/Controller/WrongCmsController.php @@ -0,0 +1,38 @@ +getApplication()->allowCache(true); + + $response = new TextResponse("This isn't the CMS you're looking for.", 404); + + $this->getApplication()->setResponse($response); + + return true; + } +} diff --git a/week-03/project/libraries/src/Date/Date.php b/week-03/project/libraries/src/Date/Date.php new file mode 100644 index 0000000..c26cc0f --- /dev/null +++ b/week-03/project/libraries/src/Date/Date.php @@ -0,0 +1,485 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Date; + +\defined('JPATH_PLATFORM') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\Language\Text; +use Joomla\Database\DatabaseDriver; + +/** + * Date is a class that stores a date and provides logic to manipulate + * and render that date in a variety of formats. + * + * @method Date|bool add(\DateInterval $interval) Adds an amount of days, months, years, hours, minutes and seconds to a Date object. + * @method Date|bool sub(\DateInterval $interval) Subtracts an amount of days, months, years, hours, minutes and seconds from a Date object. + * @method Date|bool modify(string $modify) Alter the timestamp of this object by incre/decre-menting in a format accepted by strtotime(). + * + * @property-read string $daysinmonth t - Number of days in the given month. + * @property-read string $dayofweek N - ISO-8601 numeric representation of the day of the week. + * @property-read string $dayofyear z - The day of the year (starting from 0). + * @property-read boolean $isleapyear L - Whether it's a leap year. + * @property-read string $day d - Day of the month, 2 digits with leading zeros. + * @property-read string $hour H - 24-hour format of an hour with leading zeros. + * @property-read string $minute i - Minutes with leading zeros. + * @property-read string $second s - Seconds with leading zeros. + * @property-read string $microsecond u - Microseconds with leading zeros. + * @property-read string $month m - Numeric representation of a month, with leading zeros. + * @property-read string $ordinal S - English ordinal suffix for the day of the month, 2 characters. + * @property-read string $week W - ISO-8601 week number of year, weeks starting on Monday. + * @property-read string $year Y - A full numeric representation of a year, 4 digits. + * + * @since 1.7.0 + */ +class Date extends \DateTime +{ + const DAY_ABBR = "\x021\x03"; + const DAY_NAME = "\x022\x03"; + const MONTH_ABBR = "\x023\x03"; + const MONTH_NAME = "\x024\x03"; + + /** + * The format string to be applied when using the __toString() magic method. + * + * @var string + * @since 1.7.0 + */ + public static $format = 'Y-m-d H:i:s'; + + /** + * Placeholder for a \DateTimeZone object with GMT as the time zone. + * + * @var object + * @since 1.7.0 + * + * @deprecated 5.0 Without replacement + */ + protected static $gmt; + + /** + * Placeholder for a \DateTimeZone object with the default server + * time zone as the time zone. + * + * @var object + * @since 1.7.0 + * + * @deprecated 5.0 Without replacement + */ + protected static $stz; + + /** + * The \DateTimeZone object for usage in rending dates as strings. + * + * @var \DateTimeZone + * @since 3.0.0 + */ + protected $tz; + + /** + * Constructor. + * + * @param string $date String in a format accepted by strtotime(), defaults to "now". + * @param mixed $tz Time zone to be used for the date. Might be a string or a DateTimeZone object. + * + * @since 1.7.0 + */ + public function __construct($date = 'now', $tz = null) + { + // Create the base GMT and server time zone objects. + if (empty(self::$gmt) || empty(self::$stz)) + { + // @TODO: This code block stays here only for B/C, can be removed in 5.0 + self::$gmt = new \DateTimeZone('GMT'); + self::$stz = new \DateTimeZone(@date_default_timezone_get()); + } + + // If the time zone object is not set, attempt to build it. + if (!($tz instanceof \DateTimeZone)) + { + if (\is_string($tz)) + { + $tz = new \DateTimeZone($tz); + } + else + { + $tz = new \DateTimeZone('UTC'); + } + } + + // Backup active time zone + $activeTZ = date_default_timezone_get(); + + // Force UTC timezone for correct time handling + date_default_timezone_set('UTC'); + + // If the date is numeric assume a unix timestamp and convert it. + $date = is_numeric($date) ? date('c', $date) : $date; + + // Call the DateTime constructor. + parent::__construct($date, $tz); + + // Restore previously active timezone + date_default_timezone_set($activeTZ); + + // Set the timezone object for access later. + $this->tz = $tz; + } + + /** + * Magic method to access properties of the date given by class to the format method. + * + * @param string $name The name of the property. + * + * @return mixed A value if the property name is valid, null otherwise. + * + * @since 1.7.0 + */ + public function __get($name) + { + $value = null; + + switch ($name) + { + case 'daysinmonth': + $value = $this->format('t', true); + break; + + case 'dayofweek': + $value = $this->format('N', true); + break; + + case 'dayofyear': + $value = $this->format('z', true); + break; + + case 'isleapyear': + $value = (boolean) $this->format('L', true); + break; + + case 'day': + $value = $this->format('d', true); + break; + + case 'hour': + $value = $this->format('H', true); + break; + + case 'minute': + $value = $this->format('i', true); + break; + + case 'second': + $value = $this->format('s', true); + break; + + case 'month': + $value = $this->format('m', true); + break; + + case 'ordinal': + $value = $this->format('S', true); + break; + + case 'week': + $value = $this->format('W', true); + break; + + case 'year': + $value = $this->format('Y', true); + break; + + default: + $trace = debug_backtrace(); + trigger_error( + 'Undefined property via __get(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], + E_USER_NOTICE + ); + } + + return $value; + } + + /** + * Magic method to render the date object in the format specified in the public + * static member Date::$format. + * + * @return string The date as a formatted string. + * + * @since 1.7.0 + */ + public function __toString() + { + return (string) parent::format(self::$format); + } + + /** + * Proxy for new Date(). + * + * @param string $date String in a format accepted by strtotime(), defaults to "now". + * @param mixed $tz Time zone to be used for the date. + * + * @return Date + * + * @since 1.7.3 + */ + public static function getInstance($date = 'now', $tz = null) + { + return new static($date, $tz); + } + + /** + * Translates day of week number to a string. + * + * @param integer $day The numeric day of the week. + * @param boolean $abbr Return the abbreviated day string? + * + * @return string The day of the week. + * + * @since 1.7.0 + */ + public function dayToString($day, $abbr = false) + { + switch ($day) + { + case 0: + return $abbr ? Text::_('SUN') : Text::_('SUNDAY'); + case 1: + return $abbr ? Text::_('MON') : Text::_('MONDAY'); + case 2: + return $abbr ? Text::_('TUE') : Text::_('TUESDAY'); + case 3: + return $abbr ? Text::_('WED') : Text::_('WEDNESDAY'); + case 4: + return $abbr ? Text::_('THU') : Text::_('THURSDAY'); + case 5: + return $abbr ? Text::_('FRI') : Text::_('FRIDAY'); + case 6: + return $abbr ? Text::_('SAT') : Text::_('SATURDAY'); + } + } + + /** + * Gets the date as a formatted string in a local calendar. + * + * @param string $format The date format specification string (see {@link PHP_MANUAL#date}) + * @param boolean $local True to return the date string in the local time zone, false to return it in GMT. + * @param boolean $translate True to translate localised strings + * + * @return string The date string in the specified format format. + * + * @since 1.7.0 + */ + public function calendar($format, $local = false, $translate = true) + { + return $this->format($format, $local, $translate); + } + + /** + * Gets the date as a formatted string. + * + * @param string $format The date format specification string (see {@link PHP_MANUAL#date}) + * @param boolean $local True to return the date string in the local time zone, false to return it in GMT. + * @param boolean $translate True to translate localised strings + * + * @return string The date string in the specified format format. + * + * @since 1.7.0 + */ + #[\ReturnTypeWillChange] + public function format($format, $local = false, $translate = true) + { + if ($translate) + { + // Do string replacements for date format options that can be translated. + $format = preg_replace('/(^|[^\\\])D/', "\\1" . self::DAY_ABBR, $format); + $format = preg_replace('/(^|[^\\\])l/', "\\1" . self::DAY_NAME, $format); + $format = preg_replace('/(^|[^\\\])M/', "\\1" . self::MONTH_ABBR, $format); + $format = preg_replace('/(^|[^\\\])F/', "\\1" . self::MONTH_NAME, $format); + } + + // If the returned time should not be local use UTC. + if ($local == false) + { + parent::setTimezone(new \DateTimeZone('UTC')); + } + + // Format the date. + $return = parent::format($format); + + if ($translate) + { + // Manually modify the month and day strings in the formatted time. + if (strpos($return, self::DAY_ABBR) !== false) + { + $return = str_replace(self::DAY_ABBR, $this->dayToString(parent::format('w'), true), $return); + } + + if (strpos($return, self::DAY_NAME) !== false) + { + $return = str_replace(self::DAY_NAME, $this->dayToString(parent::format('w')), $return); + } + + if (strpos($return, self::MONTH_ABBR) !== false) + { + $return = str_replace(self::MONTH_ABBR, $this->monthToString(parent::format('n'), true), $return); + } + + if (strpos($return, self::MONTH_NAME) !== false) + { + $return = str_replace(self::MONTH_NAME, $this->monthToString(parent::format('n')), $return); + } + } + + if ($local == false && $this->tz !== null) + { + parent::setTimezone($this->tz); + } + + return $return; + } + + /** + * Get the time offset from GMT in hours or seconds. + * + * @param boolean $hours True to return the value in hours. + * + * @return float The time offset from GMT either in hours or in seconds. + * + * @since 1.7.0 + */ + public function getOffsetFromGmt($hours = false) + { + return (float) $hours ? ($this->tz->getOffset($this) / 3600) : $this->tz->getOffset($this); + } + + /** + * Translates month number to a string. + * + * @param integer $month The numeric month of the year. + * @param boolean $abbr If true, return the abbreviated month string + * + * @return string The month of the year. + * + * @since 1.7.0 + */ + public function monthToString($month, $abbr = false) + { + switch ($month) + { + case 1: + return $abbr ? Text::_('JANUARY_SHORT') : Text::_('JANUARY'); + case 2: + return $abbr ? Text::_('FEBRUARY_SHORT') : Text::_('FEBRUARY'); + case 3: + return $abbr ? Text::_('MARCH_SHORT') : Text::_('MARCH'); + case 4: + return $abbr ? Text::_('APRIL_SHORT') : Text::_('APRIL'); + case 5: + return $abbr ? Text::_('MAY_SHORT') : Text::_('MAY'); + case 6: + return $abbr ? Text::_('JUNE_SHORT') : Text::_('JUNE'); + case 7: + return $abbr ? Text::_('JULY_SHORT') : Text::_('JULY'); + case 8: + return $abbr ? Text::_('AUGUST_SHORT') : Text::_('AUGUST'); + case 9: + return $abbr ? Text::_('SEPTEMBER_SHORT') : Text::_('SEPTEMBER'); + case 10: + return $abbr ? Text::_('OCTOBER_SHORT') : Text::_('OCTOBER'); + case 11: + return $abbr ? Text::_('NOVEMBER_SHORT') : Text::_('NOVEMBER'); + case 12: + return $abbr ? Text::_('DECEMBER_SHORT') : Text::_('DECEMBER'); + } + } + + /** + * Method to wrap the setTimezone() function and set the internal time zone object. + * + * @param \DateTimeZone $tz The new \DateTimeZone object. + * + * @return Date + * + * @since 1.7.0 + * @note This method can't be type hinted due to a PHP bug: https://bugs.php.net/bug.php?id=61483 + */ + #[\ReturnTypeWillChange] + public function setTimezone($tz) + { + $this->tz = $tz; + + return parent::setTimezone($tz); + } + + /** + * Gets the date as an ISO 8601 string. IETF RFC 3339 defines the ISO 8601 format + * and it can be found at the IETF Web site. + * + * @param boolean $local True to return the date string in the local time zone, false to return it in GMT. + * + * @return string The date string in ISO 8601 format. + * + * @link http://www.ietf.org/rfc/rfc3339.txt + * @since 1.7.0 + */ + public function toISO8601($local = false) + { + return $this->format(\DateTimeInterface::RFC3339, $local, false); + } + + /** + * Gets the date as an SQL datetime string. + * + * @param boolean $local True to return the date string in the local time zone, false to return it in GMT. + * @param DatabaseDriver $db The database driver or null to use Factory::getDbo() + * + * @return string The date string in SQL datetime format. + * + * @link http://dev.mysql.com/doc/refman/5.0/en/datetime.html + * @since 2.5.0 + */ + public function toSql($local = false, DatabaseDriver $db = null) + { + if ($db === null) + { + $db = Factory::getDbo(); + } + + return $this->format($db->getDateFormat(), $local, false); + } + + /** + * Gets the date as an RFC 822 string. IETF RFC 2822 supercedes RFC 822 and its definition + * can be found at the IETF Web site. + * + * @param boolean $local True to return the date string in the local time zone, false to return it in GMT. + * + * @return string The date string in RFC 822 format. + * + * @link http://www.ietf.org/rfc/rfc2822.txt + * @since 1.7.0 + */ + public function toRFC822($local = false) + { + return $this->format(\DateTimeInterface::RFC2822, $local, false); + } + + /** + * Gets the date as UNIX time stamp. + * + * @return integer The date as a UNIX timestamp. + * + * @since 1.7.0 + */ + public function toUnix() + { + return (int) parent::format('U'); + } +} diff --git a/week-03/project/libraries/src/EventListener/ErrorSubscriber.php b/week-03/project/libraries/src/EventListener/ErrorSubscriber.php new file mode 100644 index 0000000..a233439 --- /dev/null +++ b/week-03/project/libraries/src/EventListener/ErrorSubscriber.php @@ -0,0 +1,194 @@ +renderer = $renderer; + } + + /** + * Returns an array of events this subscriber will listen to. + * + * @return array + */ + public static function getSubscribedEvents(): array + { + return [ + ApplicationEvents::ERROR => 'handleWebError', + ConsoleEvents::APPLICATION_ERROR => 'handleConsoleError', + ]; + } + + /** + * Handle console application errors. + * + * @param ConsoleApplicationErrorEvent $event Event object + * + * @return void + */ + public function handleConsoleError(ConsoleApplicationErrorEvent $event): void + { + $this->logError($event->getError()); + } + + /** + * Handle web application errors. + * + * @param ApplicationErrorEvent $event Event object + * + * @return void + */ + public function handleWebError(ApplicationErrorEvent $event): void + { + /** @var WebApplication $app */ + $app = $event->getApplication(); + + switch (true) + { + case $event->getError() instanceof MethodNotAllowedException : + // Log the error for reference + $this->logger->error( + sprintf('Route `%s` not supported by method `%s`', $app->get('uri.route'), $app->input->getMethod()), + ['exception' => $event->getError()] + ); + + $this->prepareResponse($event); + + $app->setHeader('Allow', implode(', ', $event->getError()->getAllowedMethods())); + + break; + + case $event->getError() instanceof RouteNotFoundException : + // Log the error for reference + $this->logger->error( + sprintf('Route `%s` not found', $app->get('uri.route')), + ['exception' => $event->getError()] + ); + + $this->prepareResponse($event); + + break; + + default: + $this->logError($event->getError()); + + $this->prepareResponse($event); + + break; + } + } + + /** + * Log the error. + * + * @param \Throwable $throwable The error being processed + * + * @return void + */ + private function logError(\Throwable $throwable): void + { + $this->logger->error( + sprintf('Uncaught Throwable of type %s caught.', \get_class($throwable)), + ['exception' => $throwable] + ); + } + + /** + * Prepare the response for the event + * + * @param ApplicationErrorEvent $event Event object + * + * @return void + */ + private function prepareResponse(ApplicationErrorEvent $event): void + { + /** @var WebApplication $app */ + $app = $event->getApplication(); + + $app->allowCache(false); + + switch (true) + { + case $app->input->getString('_format', 'html') === 'json' : + case $app->mimeType === 'application/json' : + case $app->getResponse() instanceof JsonResponse : + $data = [ + 'code' => $event->getError()->getCode(), + 'message' => $event->getError()->getMessage(), + 'error' => true, + ]; + + $response = new JsonResponse($data); + + break; + + default : + $response = new HtmlResponse( + $this->renderer->render('exception.twig', ['exception' => $event->getError()]) + ); + + break; + } + + switch ($event->getError()->getCode()) + { + case 404 : + $response = $response->withStatus(404); + + break; + + case 405 : + $response = $response->withStatus(405); + + break; + + case 500 : + default : + $response = $response->withStatus(500); + + break; + } + + $app->setResponse($response); + } +} diff --git a/week-03/project/libraries/src/Factory.php b/week-03/project/libraries/src/Factory.php new file mode 100644 index 0000000..dbc52d5 --- /dev/null +++ b/week-03/project/libraries/src/Factory.php @@ -0,0 +1,850 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS; + +\defined('JPATH_PLATFORM') or die; + +use Joomla\CMS\Application\CMSApplicationInterface; +use Joomla\CMS\Cache\Cache; +use Joomla\CMS\Cache\CacheControllerFactoryInterface; +use Joomla\CMS\Client\ClientHelper; +use Joomla\CMS\Date\Date; +use Joomla\CMS\Document\Document; +use Joomla\CMS\Document\FactoryInterface; +use Joomla\CMS\Filesystem\Stream; +use Joomla\CMS\Language\Language; +use Joomla\CMS\Language\LanguageFactoryInterface; +use Joomla\CMS\Log\Log; +use Joomla\CMS\Mail\Mail; +use Joomla\CMS\Mail\MailHelper; +use Joomla\CMS\Session\Session; +use Joomla\CMS\User\User; +use Joomla\Database\DatabaseDriver; +use Joomla\Database\DatabaseInterface; +use Joomla\DI\Container; +use Joomla\Registry\Registry; +use PHPMailer\PHPMailer\Exception as phpmailerException; + +/** + * Joomla Platform Factory class. + * + * @since 1.7.0 + */ +abstract class Factory +{ + /** + * Global application object + * + * @var CMSApplicationInterface + * @since 1.7.0 + */ + public static $application = null; + + /** + * Global cache object + * + * @var Cache + * @since 1.7.0 + */ + public static $cache = null; + + /** + * Global configuration object + * + * @var \JConfig + * @since 1.7.0 + * @deprecated 5.0 Use the configuration object within the application + */ + public static $config = null; + + /** + * Global container object + * + * @var Container + * @since 4.0.0 + */ + public static $container = null; + + /** + * Container for Date instances + * + * @var array + * @since 1.7.3 + */ + public static $dates = array(); + + /** + * Global session object + * + * @var Session + * @since 1.7.0 + * @deprecated 5.0 Use the session service in the DI container + */ + public static $session = null; + + /** + * Global language object + * + * @var Language + * @since 1.7.0 + * @deprecated 5.0 Use the language service in the DI container + */ + public static $language = null; + + /** + * Global document object + * + * @var Document + * @since 1.7.0 + * @deprecated 5.0 Use the document service in the DI container + */ + public static $document = null; + + /** + * Global database object + * + * @var DatabaseDriver + * @since 1.7.0 + * @deprecated 5.0 Use the database service in the DI container + */ + public static $database = null; + + /** + * Global mailer object + * + * @var Mail + * @since 1.7.0 + */ + public static $mailer = null; + + /** + * Get the global application object. When the global application doesn't exist, an exception is thrown. + * + * @return CMSApplicationInterface object + * + * @since 1.7.0 + * @throws \Exception + */ + public static function getApplication() + { + if (!self::$application) + { + throw new \Exception('Failed to start application', 500); + } + + return self::$application; + } + + /** + * Get a configuration object + * + * Returns the global {@link \JConfig} object, only creating it if it doesn't already exist. + * + * @param string $file The path to the configuration file + * @param string $type The type of the configuration file + * @param string $namespace The namespace of the configuration file + * + * @return Registry + * + * @see Registry + * @since 1.7.0 + * @deprecated 5.0 Use the configuration object within the application. + */ + public static function getConfig($file = null, $type = 'PHP', $namespace = '') + { + @trigger_error( + sprintf( + '%s() is deprecated. The configuration object should be read from the application.', + __METHOD__ + ), + E_USER_DEPRECATED + ); + + /** + * If there is an application object, fetch the configuration from there. + * Check it's not null because LanguagesModel can make it null and if it's null + * we would want to re-init it from configuration.php. + */ + if (self::$application && self::$application->getConfig() !== null) + { + return self::$application->getConfig(); + } + + if (!self::$config) + { + if ($file === null) + { + $file = JPATH_CONFIGURATION . '/configuration.php'; + } + + self::$config = self::createConfig($file, $type, $namespace); + } + + return self::$config; + } + + /** + * Get a container object + * + * Returns the global service container object, only creating it if it doesn't already exist. + * + * This method is only suggested for use in code whose responsibility is to create new services + * and needs to be able to resolve the dependencies, and should therefore only be used when the + * container is not accessible by other means. Valid uses of this method include: + * + * - A static `getInstance()` method calling a factory service from the container, + * see `Joomla\CMS\Toolbar\Toolbar::getInstance()` as an example + * - An application front controller loading and executing the Joomla application class, + * see the `cli/joomla.php` file as an example + * - Retrieving optional constructor dependencies when not injected into a class during a transitional + * period to retain backward compatibility, in this case a deprecation notice should also be emitted to + * notify developers of changes needed in their code + * + * This method is not suggested for use as a one-for-one replacement of static calls, such as + * replacing calls to `Factory::getDbo()` with calls to `Factory::getContainer()->get('db')`, code + * should be refactored to support dependency injection instead of making this change. + * + * @return Container + * + * @since 4.0.0 + */ + public static function getContainer(): Container + { + if (!self::$container) + { + self::$container = self::createContainer(); + } + + return self::$container; + } + + /** + * Get a session object. + * + * Returns the global {@link Session} object, only creating it if it doesn't already exist. + * + * @param array $options An array containing session options + * + * @return Session object + * + * @see Session + * @since 1.7.0 + * @deprecated 5.0 Load the session service from the dependency injection container or via $app->getSession() + */ + public static function getSession(array $options = array()) + { + @trigger_error( + sprintf( + '%1$s() is deprecated. Load the session from the dependency injection container or via %2$s::getApplication()->getSession().', + __METHOD__, + __CLASS__ + ), + E_USER_DEPRECATED + ); + + return self::getApplication()->getSession(); + } + + /** + * Get a language object. + * + * Returns the global {@link Language} object, only creating it if it doesn't already exist. + * + * @return Language object + * + * @see Language + * @since 1.7.0 + * @deprecated 5.0 Load the language service from the dependency injection container or via $app->getLanguage() + */ + public static function getLanguage() + { + @trigger_error( + sprintf( + '%1$s() is deprecated. Load the language from the dependency injection container or via %2$s::getApplication()->getLanguage().', + __METHOD__, + __CLASS__ + ), + E_USER_DEPRECATED + ); + + if (!self::$language) + { + self::$language = self::createLanguage(); + } + + return self::$language; + } + + /** + * Get a document object. + * + * Returns the global {@link \Joomla\CMS\Document\Document} object, only creating it if it doesn't already exist. + * + * @return Document object + * + * @see Document + * @since 1.7.0 + * @deprecated 5.0 Load the document service from the dependency injection container or via $app->getDocument() + */ + public static function getDocument() + { + @trigger_error( + sprintf( + '%1$s() is deprecated. Load the document from the dependency injection container or via %2$s::getApplication()->getDocument().', + __METHOD__, + __CLASS__ + ), + E_USER_DEPRECATED + ); + + if (!self::$document) + { + self::$document = self::createDocument(); + } + + return self::$document; + } + + /** + * Get a user object. + * + * Returns the global {@link User} object, only creating it if it doesn't already exist. + * + * @param integer $id The user to load - Can be an integer or string - If string, it is converted to ID automatically. + * + * @return User object + * + * @see User + * @since 1.7.0 + * @deprecated 5.0 Load the user service from the dependency injection container or via $app->getIdentity() + */ + public static function getUser($id = null) + { + @trigger_error( + sprintf( + '%1$s() is deprecated. Load the user from the dependency injection container or via %2$s::getApplication()->getIdentity().', + __METHOD__, + __CLASS__ + ), + E_USER_DEPRECATED + ); + + $instance = self::getApplication()->getSession()->get('user'); + + if (\is_null($id)) + { + if (!($instance instanceof User)) + { + $instance = User::getInstance(); + } + } + // Check if we have a string as the id or if the numeric id is the current instance + elseif (!($instance instanceof User) || \is_string($id) || $instance->id !== $id) + { + $instance = User::getInstance($id); + } + + return $instance; + } + + /** + * Get a cache object + * + * Returns the global {@link CacheController} object + * + * @param string $group The cache group name + * @param string $handler The handler to use + * @param string $storage The storage method + * + * @return \Joomla\CMS\Cache\CacheController object + * + * @see Cache + * @since 1.7.0 + * @deprecated 5.0 Use the cache controller factory instead + */ + public static function getCache($group = '', $handler = 'callback', $storage = null) + { + @trigger_error( + sprintf( + '%s() is deprecated. The cache controller should be fetched from the factory.', + __METHOD__ + ), + E_USER_DEPRECATED + ); + + $hash = md5($group . $handler . $storage); + + if (isset(self::$cache[$hash])) + { + return self::$cache[$hash]; + } + + $handler = ($handler === 'function') ? 'callback' : $handler; + + $options = array('defaultgroup' => $group); + + if (isset($storage)) + { + $options['storage'] = $storage; + } + + $cache = self::getContainer()->get(CacheControllerFactoryInterface::class)->createCacheController($handler, $options); + + self::$cache[$hash] = $cache; + + return self::$cache[$hash]; + } + + /** + * Get a database object. + * + * Returns the global {@link DatabaseDriver} object, only creating it if it doesn't already exist. + * + * @return DatabaseDriver + * + * @see DatabaseDriver + * @since 1.7.0 + * @deprecated 5.0 Load the database service from the dependency injection container + */ + public static function getDbo() + { + @trigger_error( + sprintf( + '%1$s() is deprecated. Load the database from the dependency injection container.', + __METHOD__ + ), + E_USER_DEPRECATED + ); + + if (!self::$database) + { + if (self::getContainer()->has('DatabaseDriver')) + { + self::$database = self::getContainer()->get('DatabaseDriver'); + } + else + { + self::$database = self::createDbo(); + } + } + + return self::$database; + } + + /** + * Get a mailer object. + * + * Returns the global {@link Mail} object, only creating it if it doesn't already exist. + * + * @return Mail object + * + * @see Mail + * @since 1.7.0 + */ + public static function getMailer() + { + if (!self::$mailer) + { + self::$mailer = self::createMailer(); + } + + $copy = clone self::$mailer; + + return $copy; + } + + /** + * Return the {@link Date} object + * + * @param mixed $time The initial time for the Date object + * @param mixed $tzOffset The timezone offset. + * + * @return Date object + * + * @see Date + * @since 1.7.0 + */ + public static function getDate($time = 'now', $tzOffset = null) + { + static $classname; + static $mainLocale; + + $language = self::getLanguage(); + $locale = $language->getTag(); + + if (!isset($classname) || $locale != $mainLocale) + { + // Store the locale for future reference + $mainLocale = $locale; + + if ($mainLocale !== false) + { + $classname = str_replace('-', '_', $mainLocale) . 'Date'; + + if (!class_exists($classname)) + { + // The class does not exist, default to Date + $classname = 'Joomla\\CMS\\Date\\Date'; + } + } + else + { + // No tag, so default to Date + $classname = 'Joomla\\CMS\\Date\\Date'; + } + } + + $key = $time . '-' . ($tzOffset instanceof \DateTimeZone ? $tzOffset->getName() : (string) $tzOffset); + + if (!isset(self::$dates[$classname][$key])) + { + self::$dates[$classname][$key] = new $classname($time, $tzOffset); + } + + $date = clone self::$dates[$classname][$key]; + + return $date; + } + + /** + * Create a configuration object + * + * @param string $file The path to the configuration file. + * @param string $type The type of the configuration file. + * @param string $namespace The namespace of the configuration file. + * + * @return Registry + * + * @see Registry + * @since 1.7.0 + * @deprecated 5.0 Use the configuration object within the application. + */ + protected static function createConfig($file, $type = 'PHP', $namespace = '') + { + @trigger_error( + sprintf( + '%s() is deprecated. The configuration object should be read from the application.', + __METHOD__ + ), + E_USER_DEPRECATED + ); + + if (is_file($file)) + { + include_once $file; + } + + // Create the registry with a default namespace of config + $registry = new Registry; + + // Sanitize the namespace. + $namespace = ucfirst((string) preg_replace('/[^A-Z_]/i', '', $namespace)); + + // Build the config name. + $name = 'JConfig' . $namespace; + + // Handle the PHP configuration type. + if ($type === 'PHP' && class_exists($name)) + { + // Create the JConfig object + $config = new $name; + + // Load the configuration values into the registry + $registry->loadObject($config); + } + + return $registry; + } + + /** + * Create a container object + * + * @return Container + * + * @since 4.0.0 + */ + protected static function createContainer(): Container + { + $container = (new Container) + ->registerServiceProvider(new \Joomla\CMS\Service\Provider\Application) + ->registerServiceProvider(new \Joomla\CMS\Service\Provider\Authentication) + ->registerServiceProvider(new \Joomla\CMS\Service\Provider\CacheController) + ->registerServiceProvider(new \Joomla\CMS\Service\Provider\Config) + ->registerServiceProvider(new \Joomla\CMS\Service\Provider\Console) + ->registerServiceProvider(new \Joomla\CMS\Service\Provider\Database) + ->registerServiceProvider(new \Joomla\CMS\Service\Provider\Dispatcher) + ->registerServiceProvider(new \Joomla\CMS\Service\Provider\Document) + ->registerServiceProvider(new \Joomla\CMS\Service\Provider\Form) + ->registerServiceProvider(new \Joomla\CMS\Service\Provider\Logger) + ->registerServiceProvider(new \Joomla\CMS\Service\Provider\Language) + ->registerServiceProvider(new \Joomla\CMS\Service\Provider\Menu) + ->registerServiceProvider(new \Joomla\CMS\Service\Provider\Pathway) + ->registerServiceProvider(new \Joomla\CMS\Service\Provider\HTMLRegistry) + ->registerServiceProvider(new \Joomla\CMS\Service\Provider\Session) + ->registerServiceProvider(new \Joomla\CMS\Service\Provider\Toolbar) + ->registerServiceProvider(new \Joomla\CMS\Service\Provider\WebAssetRegistry) + ->registerServiceProvider(new \Joomla\CMS\Service\Provider\ApiRouter) + ->registerServiceProvider(new \Joomla\CMS\Service\Provider\User); + + return $container; + } + + /** + * Create a database object + * + * @return DatabaseDriver + * + * @see DatabaseDriver + * @since 1.7.0 + * @deprecated 5.0 Use the database service in the DI container + */ + protected static function createDbo() + { + @trigger_error( + sprintf( + '%1$s() is deprecated, register a service provider to create a %2$s instance instead.', + __METHOD__, + DatabaseInterface::class + ), + E_USER_DEPRECATED + ); + + $conf = self::getConfig(); + + $host = $conf->get('host'); + $user = $conf->get('user'); + $password = $conf->get('password'); + $database = $conf->get('db'); + $prefix = $conf->get('dbprefix'); + $driver = $conf->get('dbtype'); + + $options = array('driver' => $driver, 'host' => $host, 'user' => $user, 'password' => $password, 'database' => $database, 'prefix' => $prefix); + + if ((int) $conf->get('dbencryption') !== 0) + { + $options['ssl'] = [ + 'enable' => true, + 'verify_server_cert' => (bool) $conf->get('dbsslverifyservercert'), + ]; + + foreach (['cipher', 'ca', 'key', 'cert'] as $value) + { + $confVal = trim($conf->get('dbssl' . $value, '')); + + if ($confVal !== '') + { + $options['ssl'][$value] = $confVal; + } + } + } + + try + { + $db = DatabaseDriver::getInstance($options); + } + catch (\RuntimeException $e) + { + if (!headers_sent()) + { + header('HTTP/1.1 500 Internal Server Error'); + } + + jexit('Database Error: ' . $e->getMessage()); + } + + return $db; + } + + /** + * Create a mailer object + * + * @return Mail object + * + * @see Mail + * @since 1.7.0 + */ + protected static function createMailer() + { + $conf = self::getConfig(); + + $smtpauth = ($conf->get('smtpauth') == 0) ? null : 1; + $smtpuser = $conf->get('smtpuser'); + $smtppass = $conf->get('smtppass'); + $smtphost = $conf->get('smtphost'); + $smtpsecure = $conf->get('smtpsecure'); + $smtpport = $conf->get('smtpport'); + $mailfrom = $conf->get('mailfrom'); + $fromname = $conf->get('fromname'); + $mailer = $conf->get('mailer'); + + // Create a Mail object + $mail = Mail::getInstance(); + + // Clean the email address + $mailfrom = MailHelper::cleanLine($mailfrom); + + // Set default sender without Reply-to if the mailfrom is a valid address + if (MailHelper::isEmailAddress($mailfrom)) + { + // Wrap in try/catch to catch phpmailerExceptions if it is throwing them + try + { + // Check for a false return value if exception throwing is disabled + if ($mail->setFrom($mailfrom, MailHelper::cleanLine($fromname), false) === false) + { + Log::add(__METHOD__ . '() could not set the sender data.', Log::WARNING, 'mail'); + } + } + catch (phpmailerException $e) + { + Log::add(__METHOD__ . '() could not set the sender data.', Log::WARNING, 'mail'); + } + } + + // Default mailer is to use PHP's mail function + switch ($mailer) + { + case 'smtp': + $mail->useSmtp($smtpauth, $smtphost, $smtpuser, $smtppass, $smtpsecure, $smtpport); + break; + + case 'sendmail': + $mail->isSendmail(); + break; + + default: + $mail->isMail(); + break; + } + + return $mail; + } + + /** + * Create a language object + * + * @return Language object + * + * @see Language + * @since 1.7.0 + * @deprecated 5.0 Load the language service from the dependency injection container or via $app->getLanguage() + */ + protected static function createLanguage() + { + @trigger_error( + sprintf( + '%1$s() is deprecated. Load the language from the dependency injection container or via %2$s::getApplication()->getLanguage().', + __METHOD__, + __CLASS__ + ), + E_USER_DEPRECATED + ); + + $conf = self::getConfig(); + $locale = $conf->get('language'); + $debug = $conf->get('debug_lang'); + $lang = self::getContainer()->get(LanguageFactoryInterface::class)->createLanguage($locale, $debug); + + return $lang; + } + + /** + * Create a document object + * + * @return Document object + * + * @see Document + * @since 1.7.0 + * @deprecated 5.0 Load the document service from the dependency injection container or via $app->getDocument() + */ + protected static function createDocument() + { + @trigger_error( + sprintf( + '%1$s() is deprecated. Load the document from the dependency injection container or via %2$s::getApplication()->getDocument().', + __METHOD__, + __CLASS__ + ), + E_USER_DEPRECATED + ); + + $lang = self::getLanguage(); + + $input = self::getApplication()->input; + $type = $input->get('format', 'html', 'cmd'); + + $version = new Version; + + $attributes = array( + 'charset' => 'utf-8', + 'lineend' => 'unix', + 'tab' => "\t", + 'language' => $lang->getTag(), + 'direction' => $lang->isRtl() ? 'rtl' : 'ltr', + 'mediaversion' => $version->getMediaVersion(), + ); + + return self::getContainer()->get(FactoryInterface::class)->createDocument($type, $attributes); + } + + /** + * Creates a new stream object with appropriate prefix + * + * @param boolean $usePrefix Prefix the connections for writing + * @param boolean $useNetwork Use network if available for writing; use false to disable (e.g. FTP, SCP) + * @param string $userAgentSuffix String to append to user agent + * @param boolean $maskUserAgent User agent masking (prefix Mozilla) + * + * @return Stream + * + * @see Stream + * @since 1.7.0 + */ + public static function getStream($usePrefix = true, $useNetwork = true, $userAgentSuffix = 'Joomla', $maskUserAgent = false) + { + // Setup the context; Joomla! UA and overwrite + $context = array(); + $version = new Version; + + // Set the UA for HTTP and overwrite for FTP + $context['http']['user_agent'] = $version->getUserAgent($userAgentSuffix, $maskUserAgent); + $context['ftp']['overwrite'] = true; + + if ($usePrefix) + { + $FTPOptions = ClientHelper::getCredentials('ftp'); + $SCPOptions = ClientHelper::getCredentials('scp'); + + if ($FTPOptions['enabled'] == 1 && $useNetwork) + { + $prefix = 'ftp://' . $FTPOptions['user'] . ':' . $FTPOptions['pass'] . '@' . $FTPOptions['host']; + $prefix .= $FTPOptions['port'] ? ':' . $FTPOptions['port'] : ''; + $prefix .= $FTPOptions['root']; + } + elseif ($SCPOptions['enabled'] == 1 && $useNetwork) + { + $prefix = 'ssh2.sftp://' . $SCPOptions['user'] . ':' . $SCPOptions['pass'] . '@' . $SCPOptions['host']; + $prefix .= $SCPOptions['port'] ? ':' . $SCPOptions['port'] : ''; + $prefix .= $SCPOptions['root']; + } + else + { + $prefix = JPATH_ROOT . '/'; + } + + $retval = new Stream($prefix, JPATH_ROOT, $context); + } + else + { + $retval = new Stream('', '', $context); + } + + return $retval; + } +} diff --git a/week-03/project/libraries/src/Filter/InputFilter.php b/week-03/project/libraries/src/Filter/InputFilter.php new file mode 100644 index 0000000..b654049 --- /dev/null +++ b/week-03/project/libraries/src/Filter/InputFilter.php @@ -0,0 +1,529 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Filter; + +\defined('JPATH_PLATFORM') or die; + +use Joomla\CMS\String\PunycodeHelper; +use Joomla\Filter\InputFilter as BaseInputFilter; + +/** + * InputFilter is a class for filtering input from any data source + * + * Forked from the php input filter library by: Daniel Morris + * Original Contributors: Gianpaolo Racca, Ghislain Picard, Marco Wandschneider, Chris Tobin and Andrew Eddie. + * + * @since 1.7.0 + */ +class InputFilter extends BaseInputFilter +{ + /** + * An array containing a list of extensions for files that are typically + * executable directly in the webserver context, potentially resulting in code executions + * + * @since 4.0.0 + */ + public const FORBIDDEN_FILE_EXTENSIONS = [ + 'php', 'phps', 'pht', 'phtml', 'php3', 'php4', 'php5', 'php6', 'php7', 'asp', + 'php8', 'phar', 'inc', 'pl', 'cgi', 'fcgi', 'java', 'jar', 'py', 'aspx' + ]; + + /** + * A flag for Unicode Supplementary Characters (4-byte Unicode character) stripping. + * + * @var integer + * @since 3.5 + */ + private $stripUSC = 0; + + /** + * A container for InputFilter instances. + * + * @var InputFilter[] + * @since 4.0.0 + */ + protected static $instances = array(); + /** + * Constructor for inputFilter class. Only first parameter is required. + * + * @param array $tagsArray List of user-defined tags + * @param array $attrArray List of user-defined attributes + * @param integer $tagsMethod The constant static::ONLY_ALLOW_DEFINED_TAGS or static::BLOCK_DEFINED_TAGS + * @param integer $attrMethod The constant static::ONLY_ALLOW_DEFINED_ATTRIBUTES or static::BLOCK_DEFINED_ATTRIBUTES + * @param integer $xssAuto Only auto clean essentials = 0, Allow clean blocked tags/attributes = 1 + * @param integer $stripUSC Strip 4-byte unicode characters = 1, no strip = 0 + * + * @since 1.7.0 + */ + public function __construct($tagsArray = array(), $attrArray = array(), $tagsMethod = 0, $attrMethod = 0, $xssAuto = 1, $stripUSC = 0) + { + parent::__construct($tagsArray, $attrArray, $tagsMethod, $attrMethod, $xssAuto); + + // Assign member variables + $this->stripUSC = $stripUSC; + } + + /** + * Returns an input filter object, only creating it if it doesn't already exist. + * + * @param array $tagsArray List of user-defined tags + * @param array $attrArray List of user-defined attributes + * @param integer $tagsMethod The constant static::ONLY_ALLOW_DEFINED_TAGS or static::BLOCK_DEFINED_TAGS + * @param integer $attrMethod The constant static::ONLY_ALLOW_DEFINED_ATTRIBUTES or static::BLOCK_DEFINED_ATTRIBUTES + * @param integer $xssAuto Only auto clean essentials = 0, Allow clean blocked tags/attributes = 1 + * @param integer $stripUSC Strip 4-byte unicode characters = 1, no strip = 0 + * + * @return InputFilter The InputFilter object. + * + * @since 1.7.0 + */ + public static function getInstance($tagsArray = array(), $attrArray = array(), $tagsMethod = 0, $attrMethod = 0, $xssAuto = 1, $stripUSC = 0) + { + $sig = md5(serialize(array($tagsArray, $attrArray, $tagsMethod, $attrMethod, $xssAuto))); + + if (empty(self::$instances[$sig])) + { + self::$instances[$sig] = new InputFilter($tagsArray, $attrArray, $tagsMethod, $attrMethod, $xssAuto, $stripUSC); + } + + return self::$instances[$sig]; + } + + /** + * Method to be called by another php script. Processes for XSS and + * specified bad code. + * + * @param mixed $source Input string/array-of-string to be 'cleaned' + * @param string $type The return type for the variable: + * INT: An integer, or an array of integers, + * UINT: An unsigned integer, or an array of unsigned integers, + * FLOAT: A floating point number, or an array of floating point numbers, + * BOOLEAN: A boolean value, + * WORD: A string containing A-Z or underscores only (not case sensitive), + * ALNUM: A string containing A-Z or 0-9 only (not case sensitive), + * CMD: A string containing A-Z, 0-9, underscores, periods or hyphens (not case sensitive), + * BASE64: A string containing A-Z, 0-9, forward slashes, plus or equals (not case sensitive), + * STRING: A fully decoded and sanitised string (default), + * HTML: A sanitised string, + * ARRAY: An array, + * PATH: A sanitised file path, or an array of sanitised file paths, + * TRIM: A string trimmed from normal, non-breaking and multibyte spaces + * USERNAME: Do not use (use an application specific filter), + * RAW: The raw string is returned with no filtering, + * unknown: An unknown filter will act like STRING. If the input is an array it will return an + * array of fully decoded and sanitised strings. + * + * @return mixed 'Cleaned' version of input parameter + * + * @since 1.7.0 + */ + public function clean($source, $type = 'string') + { + // Strip Unicode Supplementary Characters when requested to do so + if ($this->stripUSC) + { + // Alternatively: preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xE2\xAF\x91", $source) but it'd be slower. + $source = $this->stripUSC($source); + } + + return parent::clean($source, $type); + } + + /** + * Function to punyencode utf8 mail when saving content + * + * @param string $text The strings to encode + * + * @return string The punyencoded mail + * + * @since 3.5 + */ + public function emailToPunycode($text) + { + $pattern = '/(("mailto:)+[\w\.\-\+]+\@[^"?]+\.+[^."?]+("|\?))/'; + + if (preg_match_all($pattern, $text, $matches)) + { + foreach ($matches[0] as $match) + { + $match = (string) str_replace(array('?', '"'), '', $match); + $text = (string) str_replace($match, PunycodeHelper::emailToPunycode($match), $text); + } + } + + return $text; + } + + /** + * Checks an uploaded for suspicious naming and potential PHP contents which could indicate a hacking attempt. + * + * The options you can define are: + * null_byte Prevent files with a null byte in their name (buffer overflow attack) + * forbidden_extensions Do not allow these strings anywhere in the file's extension + * php_tag_in_content Do not allow ` true, + + // Forbidden string in extension (e.g. php matched .php, .xxx.php, .php.xxx and so on) + 'forbidden_extensions' => self::FORBIDDEN_FILE_EXTENSIONS, + + // true, + + // true, + + // __HALT_COMPILER() + 'phar_stub_in_content' => true, + + // Which file extensions to scan for short tags + 'shorttag_extensions' => array( + 'inc', 'phps', 'class', 'php3', 'php4', 'php5', 'php6', 'php7', 'php8', 'txt', 'dat', 'tpl', 'tmpl', + ), + + // Forbidden extensions anywhere in the content + 'fobidden_ext_in_content' => true, + + // Which file extensions to scan for .php in the content + 'php_ext_content_extensions' => array('zip', 'rar', 'tar', 'gz', 'tgz', 'bz2', 'tbz', 'jpa'), + ); + + $options = array_merge($defaultOptions, $options); + + // Make sure we can scan nested file descriptors + $descriptors = $file; + + if (isset($file['name']) && isset($file['tmp_name'])) + { + $descriptors = static::decodeFileData( + array( + $file['name'], + $file['type'], + $file['tmp_name'], + $file['error'], + $file['size'], + ) + ); + } + + // Handle non-nested descriptors (single files) + if (isset($descriptors['name'])) + { + $descriptors = array($descriptors); + } + + // Scan all descriptors detected + foreach ($descriptors as $fileDescriptor) + { + if (!isset($fileDescriptor['name'])) + { + // This is a nested descriptor. We have to recurse. + if (!static::isSafeFile($fileDescriptor, $options)) + { + return false; + } + + continue; + } + + $tempNames = $fileDescriptor['tmp_name']; + $intendedNames = $fileDescriptor['name']; + + if (!\is_array($tempNames)) + { + $tempNames = array($tempNames); + } + + if (!\is_array($intendedNames)) + { + $intendedNames = array($intendedNames); + } + + $len = \count($tempNames); + + for ($i = 0; $i < $len; $i++) + { + $tempName = array_shift($tempNames); + $intendedName = array_shift($intendedNames); + + // 1. Null byte check + if ($options['null_byte']) + { + if (strstr($intendedName, "\x00")) + { + return false; + } + } + + // 2. PHP-in-extension check (.php, .php.xxx[.yyy[.zzz[...]]], .xxx[.yyy[.zzz[...]]].php) + if (!empty($options['forbidden_extensions'])) + { + $explodedName = explode('.', $intendedName); + $explodedName = array_reverse($explodedName); + array_pop($explodedName); + $explodedName = array_map('strtolower', $explodedName); + + /* + * DO NOT USE array_intersect HERE! array_intersect expects the two arrays to + * be set, i.e. they should have unique values. + */ + foreach ($options['forbidden_extensions'] as $ext) + { + if (\in_array($ext, $explodedName)) + { + return false; + } + } + } + + // 3. File contents scanner (PHP tag in file contents) + if ($options['php_tag_in_content'] + || $options['shorttag_in_content'] || $options['phar_stub_in_content'] + || ($options['fobidden_ext_in_content'] && !empty($options['forbidden_extensions']))) + { + $fp = strlen($tempName) ? @fopen($tempName, 'r') : false; + + if ($fp !== false) + { + $data = ''; + + while (!feof($fp)) + { + $data .= @fread($fp, 131072); + + if ($options['php_tag_in_content'] && stripos($data, ' $v) + { + $result[$k] = static::decodeFileData(array($data[0][$k], $data[1][$k], $data[2][$k], $data[3][$k], $data[4][$k])); + } + + return $result; + } + + return array('name' => $data[0], 'type' => $data[1], 'tmp_name' => $data[2], 'error' => $data[3], 'size' => $data[4]); + } + + /** + * Try to convert to plaintext + * + * @param string $source The source string. + * + * @return string Plaintext string + * + * @since 3.5 + */ + protected function decode($source) + { + static $ttr; + + if (!\is_array($ttr)) + { + // Entity decode + $trans_tbl = get_html_translation_table(HTML_ENTITIES, ENT_COMPAT, 'ISO-8859-1'); + + foreach ($trans_tbl as $k => $v) + { + $ttr[$v] = utf8_encode($k); + } + } + + $source = strtr($source, $ttr); + + // Convert decimal + $source = preg_replace_callback( + '/&#(\d+);/m', + function ($m) { + return utf8_encode(\chr($m[1])); + }, + $source + ); + + // Convert hex + $source = preg_replace_callback( + '/&#x([a-f0-9]+);/mi', + function ($m) { + return utf8_encode(\chr('0x' . $m[1])); + }, + $source + ); + + return $source; + } + + /** + * Recursively strip Unicode Supplementary Characters from the source. Not: objects cannot be filtered. + * + * @param mixed $source The data to filter + * + * @return mixed The filtered result + * + * @since 3.5 + */ + protected function stripUSC($source) + { + if (\is_object($source)) + { + return $source; + } + + if (\is_array($source)) + { + $filteredArray = array(); + + foreach ($source as $k => $v) + { + $filteredArray[$k] = $this->stripUSC($v); + } + + return $filteredArray; + } + + return preg_replace('/[\xF0-\xF7].../s', "\xE2\xAF\x91", $source); + } +} diff --git a/week-03/project/libraries/src/Model/DashboardModel.php b/week-03/project/libraries/src/Model/DashboardModel.php new file mode 100644 index 0000000..f2213c9 --- /dev/null +++ b/week-03/project/libraries/src/Model/DashboardModel.php @@ -0,0 +1,172 @@ +setDb($db); + } + + /** + * Add a package + * + * @param string $packageName The package name as registered with Packagist + * @param string $displayName The package's display name + * @param string $repoName The package's repo name + * @param boolean $isStable Flag indicating the package is stable + * @param boolean $isDeprecated Flag indicating the package is deprecated + * @param boolean $isAbandoned Flag indicating the package is abandoned + * + * @return void + */ + public function addPackage(string $packageName, string $displayName, string $repoName, bool $isStable, bool $isDeprecated, bool $isAbandoned): void + { + $db = $this->getDb(); + + $data = (object) [ + 'package' => $packageName, + 'display' => $displayName, + 'repo' => $repoName, + 'stable' => (int) $isStable, + 'deprecated' => (int) $isDeprecated, + 'abandoned' => (int) $isAbandoned, + ]; + + $db->insertObject('#__packages', $data); + } + + /** + * Get the active package data + * + * @return array + */ + public function getActivePackages(): array + { + $abandoned = false; + + $db = $this->getDb(); + + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__packages')) + ->where($db->quoteName('abandoned') . ' = :abandoned') + ->bind('abandoned', $abandoned, ParameterType::INTEGER); + + return $db->setQuery($query)->loadObjectList('id'); + } + + /** + * Get a package's data + * + * @param string $packageName The package to lookup + * + * @return \stdClass + * + * @throws \RuntimeException + */ + public function getPackage(string $packageName): \stdClass + { + $db = $this->getDb(); + + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__packages')) + ->where($db->quoteName('package') . ' = :package'); + + $query->bind('package', $packageName, ParameterType::STRING); + + $package = $db->setQuery($query)->loadObject(); + + if (!$package) + { + throw new \RuntimeException(sprintf('Unable to find release data for the `%s` package', $package->display), 404); + } + + return $package; + } + + /** + * Get the known package names + * + * @return array + */ + public function getPackageNames(): array + { + $db = $this->getDb(); + + $query = $db->getQuery(true) + ->select(['id', 'package']) + ->from($db->quoteName('#__packages')); + + return $db->setQuery($query)->loadAssocList('id', 'package'); + } + + /** + * Get the known package data + * + * @return array + */ + public function getPackages(): array + { + $db = $this->getDb(); + + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__packages')); + + return $db->setQuery($query)->loadObjectList('id'); + } + + /** + * Update a package + * + * @param integer $packageId The local package ID + * @param string $packageName The package name as registered with Packagist + * @param string $displayName The package's display name + * @param string $repoName The package's repo name + * @param boolean $isStable Flag indicating the package is stable + * @param boolean $isDeprecated Flag indicating the package is deprecated + * @param boolean $isAbandoned Flag indicating the package is abandoned + * + * @return void + */ + public function updatePackage(int $packageId, string $packageName, string $displayName, string $repoName, bool $isStable, bool $isDeprecated, bool $isAbandoned): void + { + $db = $this->getDb(); + + $data = (object) [ + 'id' => $packageId, + 'package' => $packageName, + 'display' => $displayName, + 'repo' => $repoName, + 'stable' => (int) $isStable, + 'deprecated' => (int) $isDeprecated, + 'abandoned' => (int) $isAbandoned, + ]; + + $db->updateObject('#__packages', $data, 'id'); + } +} diff --git a/week-03/project/libraries/src/Model/PageModel.php b/week-03/project/libraries/src/Model/PageModel.php new file mode 100644 index 0000000..f2213c9 --- /dev/null +++ b/week-03/project/libraries/src/Model/PageModel.php @@ -0,0 +1,172 @@ +setDb($db); + } + + /** + * Add a package + * + * @param string $packageName The package name as registered with Packagist + * @param string $displayName The package's display name + * @param string $repoName The package's repo name + * @param boolean $isStable Flag indicating the package is stable + * @param boolean $isDeprecated Flag indicating the package is deprecated + * @param boolean $isAbandoned Flag indicating the package is abandoned + * + * @return void + */ + public function addPackage(string $packageName, string $displayName, string $repoName, bool $isStable, bool $isDeprecated, bool $isAbandoned): void + { + $db = $this->getDb(); + + $data = (object) [ + 'package' => $packageName, + 'display' => $displayName, + 'repo' => $repoName, + 'stable' => (int) $isStable, + 'deprecated' => (int) $isDeprecated, + 'abandoned' => (int) $isAbandoned, + ]; + + $db->insertObject('#__packages', $data); + } + + /** + * Get the active package data + * + * @return array + */ + public function getActivePackages(): array + { + $abandoned = false; + + $db = $this->getDb(); + + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__packages')) + ->where($db->quoteName('abandoned') . ' = :abandoned') + ->bind('abandoned', $abandoned, ParameterType::INTEGER); + + return $db->setQuery($query)->loadObjectList('id'); + } + + /** + * Get a package's data + * + * @param string $packageName The package to lookup + * + * @return \stdClass + * + * @throws \RuntimeException + */ + public function getPackage(string $packageName): \stdClass + { + $db = $this->getDb(); + + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__packages')) + ->where($db->quoteName('package') . ' = :package'); + + $query->bind('package', $packageName, ParameterType::STRING); + + $package = $db->setQuery($query)->loadObject(); + + if (!$package) + { + throw new \RuntimeException(sprintf('Unable to find release data for the `%s` package', $package->display), 404); + } + + return $package; + } + + /** + * Get the known package names + * + * @return array + */ + public function getPackageNames(): array + { + $db = $this->getDb(); + + $query = $db->getQuery(true) + ->select(['id', 'package']) + ->from($db->quoteName('#__packages')); + + return $db->setQuery($query)->loadAssocList('id', 'package'); + } + + /** + * Get the known package data + * + * @return array + */ + public function getPackages(): array + { + $db = $this->getDb(); + + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__packages')); + + return $db->setQuery($query)->loadObjectList('id'); + } + + /** + * Update a package + * + * @param integer $packageId The local package ID + * @param string $packageName The package name as registered with Packagist + * @param string $displayName The package's display name + * @param string $repoName The package's repo name + * @param boolean $isStable Flag indicating the package is stable + * @param boolean $isDeprecated Flag indicating the package is deprecated + * @param boolean $isAbandoned Flag indicating the package is abandoned + * + * @return void + */ + public function updatePackage(int $packageId, string $packageName, string $displayName, string $repoName, bool $isStable, bool $isDeprecated, bool $isAbandoned): void + { + $db = $this->getDb(); + + $data = (object) [ + 'id' => $packageId, + 'package' => $packageName, + 'display' => $displayName, + 'repo' => $repoName, + 'stable' => (int) $isStable, + 'deprecated' => (int) $isDeprecated, + 'abandoned' => (int) $isAbandoned, + ]; + + $db->updateObject('#__packages', $data, 'id'); + } +} diff --git a/week-03/project/libraries/src/Renderer/ApplicationContext.php b/week-03/project/libraries/src/Renderer/ApplicationContext.php new file mode 100644 index 0000000..9347d04 --- /dev/null +++ b/week-03/project/libraries/src/Renderer/ApplicationContext.php @@ -0,0 +1,61 @@ +app = $app; + } + + /** + * Gets the base path. + * + * @return string The base path + */ + public function getBasePath() + { + return rtrim($this->app->get('uri.base.path'), '/'); + } + + /** + * Checks whether the request is secure or not. + * + * @return boolean + */ + public function isSecure() + { + if ($this->app instanceof AbstractWebApplication) + { + return $this->app->isSslConnection(); + } + + return false; + } +} diff --git a/week-03/project/libraries/src/Renderer/FrameworkExtension.php b/week-03/project/libraries/src/Renderer/FrameworkExtension.php new file mode 100644 index 0000000..a3535b0 --- /dev/null +++ b/week-03/project/libraries/src/Renderer/FrameworkExtension.php @@ -0,0 +1,62 @@ + ['html']]), + new TwigFunction('url', [FrameworkTwigRuntime::class, 'getRouteUrl']), + ]; + } + + /** + * Removes the application root path defined by the constant "JPATH_ROOT" + * + * @param string $string The string to process + * + * @return string + */ + public function stripRootPath(string $string): string + { + return str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR, '', $string); + } +} diff --git a/week-03/project/libraries/src/Renderer/FrameworkTwigRuntime.php b/week-03/project/libraries/src/Renderer/FrameworkTwigRuntime.php new file mode 100644 index 0000000..41a8733 --- /dev/null +++ b/week-03/project/libraries/src/Renderer/FrameworkTwigRuntime.php @@ -0,0 +1,160 @@ +app = $app; + $this->preloadManager = $preloadManager; + $this->sriManifestPath = $sriManifestPath; + } + + /** + * Retrieves the current URI + * + * @return string + */ + public function getRequestUri(): string + { + return $this->app->get('uri.request'); + } + + /** + * Get the URI for a route + * + * @param string $route Route to get the path for + * + * @return string + */ + public function getRouteUri(string $route = ''): string + { + return $this->app->get('uri.base.path') . $route; + } + + /** + * Get the full URL for a route + * + * @param string $route Route to get the URL for + * + * @return string + */ + public function getRouteUrl(string $route = ''): string + { + return $this->app->get('uri.base.host') . $this->getRouteUri($route); + } + + /** + * Get the SRI attributes for an asset + * + * @param string $path A public path + * + * @return string + */ + public function getSriAttributes(string $path): string + { + if ($this->sriManifestData === null) + { + if (!file_exists($this->sriManifestPath)) + { + throw new \RuntimeException(sprintf('SRI manifest file "%s" does not exist.', $this->sriManifestPath)); + } + + $sriManifestContents = file_get_contents($this->sriManifestPath); + + if ($sriManifestContents === false) + { + throw new \RuntimeException(sprintf('Could not read SRI manifest file "%s".', $this->sriManifestPath)); + } + + $this->sriManifestData = json_decode($sriManifestContents, true); + + if (0 < json_last_error()) + { + throw new \RuntimeException(sprintf('Error parsing JSON from SRI manifest file "%s" - %s', $this->sriManifestPath, json_last_error_msg())); + } + } + + $assetKey = "/$path"; + + if (!isset($this->sriManifestData[$assetKey])) + { + return ''; + } + + $attributes = ''; + + foreach ($this->sriManifestData[$assetKey] as $key => $value) + { + $attributes .= ' ' . $key . '="' . $value . '"'; + } + + return $attributes; + } + + /** + * Preload a resource + * + * @param string $uri The URI for the resource to preload + * @param string $linkType The preload method to apply + * @param array $attributes The attributes of this link (e.g. "array('as' => true)", "array('pr' => 0.5)") + * + * @return string + * + * @throws \InvalidArgumentException + */ + public function preloadAsset(string $uri, string $linkType = 'preload', array $attributes = []): string + { + $this->preloadManager->link($uri, $linkType, $attributes); + + return $uri; + } +} diff --git a/week-03/project/libraries/src/Service/AdminApplicationProvider.php b/week-03/project/libraries/src/Service/AdminApplicationProvider.php new file mode 100644 index 0000000..648b1bf --- /dev/null +++ b/week-03/project/libraries/src/Service/AdminApplicationProvider.php @@ -0,0 +1,748 @@ +share(ConsoleApplication::class, [$this, 'getConsoleApplicationService'], true); + + // This service cannot be protected as it is decorated when the debug bar is available + $container->alias(WebApplication::class, AbstractWebApplication::class) + ->share(AbstractWebApplication::class, [$this, 'getWebApplicationClassService']); + + /* + * Application Helpers and Dependencies + */ + + $container->alias(Analytics::class, 'analytics') + ->share('analytics', [$this, 'getAnalyticsService'], true); + + $container->alias(ContainerLoader::class, LoaderInterface::class) + ->share(LoaderInterface::class, [$this, 'getCommandLoaderService'], true); + + // This service cannot be protected as it is decorated when the debug bar is available + $container->alias(ContainerControllerResolver::class, ControllerResolverInterface::class) + ->share(ControllerResolverInterface::class, [$this, 'getControllerResolverService']); + + $container->alias(Helper::class, 'application.helper') + ->share('application.helper', [$this, 'getApplicationHelperService'], true); + + $container->alias(PackagistHelper::class, 'application.helper.packagist') + ->share('application.helper.packagist', [$this, 'getApplicationHelperPackagistService'], true); + + $container->share('application.packages', [$this, 'getApplicationPackagesService'], true); + + $container->share(WebClient::class, [$this, 'getWebClientService'], true); + + // This service cannot be protected as it is decorated when the debug bar is available + $container->alias(RouterInterface::class, 'application.router') + ->alias(Router::class, 'application.router') + ->share('application.router', [$this, 'getApplicationRouterService']); + + $container->share(Input::class, [$this, 'getInputClassService'], true); + + /* + * Console Commands + */ + + $container->share(DebugEventDispatcherCommand::class, [$this, 'getDebugEventDispatcherCommandService'], true); + $container->share(DebugRouterCommand::class, [$this, 'getDebugRouterCommandService'], true); + $container->share(DownloadsCommand::class, [$this, 'getDownloadsCommandService'], true); + $container->share(GenerateSriCommand::class, [$this, 'getGenerateSriCommandService'], true); + $container->share(PackageSyncCommand::class, [$this, 'getPackageSyncCommandService'], true); + $container->share(PackagistSyncCommand::class, [$this, 'getPackagistSyncCommandService'], true); + $container->share(ResetCacheCommand::class, [$this, 'getResetCacheCommandService'], true); + $container->share(UpdateCommand::class, [$this, 'getUpdateCommandService'], true); + + /* + * MVC Layer + */ + + // Controllers + $container->alias(PackageControllerGet::class, 'controller.api.package') + ->share('controller.api.package', [$this, 'getControllerApiPackageService'], true); + + $container->alias(StatusControllerGet::class, 'controller.api.status') + ->share('controller.api.status', [$this, 'getControllerApiStatusService'], true); + + $container->alias(HomepageController::class, 'controller.homepage') + ->share('controller.homepage', [$this, 'getControllerHomepageService'], true); + + $container->alias(PackageController::class, 'controller.package') + ->share('controller.package', [$this, 'getControllerPackageService'], true); + + $container->alias(PageController::class, 'controller.page') + ->share('controller.page', [$this, 'getControllerPageService'], true); + + $container->alias(StatusController::class, 'controller.status') + ->share('controller.status', [$this, 'getControllerStatusService'], true); + + $container->alias(WrongCmsController::class, 'controller.wrong.cms') + ->share('controller.wrong.cms', [$this, 'getControllerWrongCmsService'], true); + + // Models + $container->alias(PackageModel::class, 'model.package') + ->share('model.package', [$this, 'getModelPackageService'], true); + + $container->alias(ReleaseModel::class, 'model.release') + ->share('model.release', [$this, 'getModelReleaseService'], true); + + // Views + $container->alias(PackageHtmlView::class, 'view.package.html') + ->share('view.package.html', [$this, 'getViewPackageHtmlService'], true); + + $container->alias(PackageJsonView::class, 'view.package.json') + ->share('view.package.json', [$this, 'getViewPackageJsonService'], true); + + $container->alias(StatusHtmlView::class, 'view.status.html') + ->share('view.status.html', [$this, 'getViewStatusHtmlService'], true); + + $container->alias(StatusJsonView::class, 'view.status.json') + ->share('view.status.json', [$this, 'getViewStatusJsonService'], true); + } + + /** + * Get the Analytics class service + * + * @param Container $container The DI container. + * + * @return Analytics + */ + public function getAnalyticsService(Container $container) + { + return new Analytics(true); + } + + /** + * Get the `application.helper` service + * + * @param Container $container The DI container. + * + * @return Helper + */ + public function getApplicationHelperService(Container $container): Helper + { + $helper = new Helper; + $helper->setPackages($container->get('application.packages')); + + return $helper; + } + + /** + * Get the `application.helper.packagist` service + * + * @param Container $container The DI container. + * + * @return PackagistHelper + */ + public function getApplicationHelperPackagistService(Container $container): PackagistHelper + { + $helper = new PackagistHelper($container->get(Http::class), $container->get(DatabaseInterface::class)); + $helper->setPackages($container->get('application.packages')); + + return $helper; + } + + /** + * Get the `application.packages` service + * + * @param Container $container The DI container. + * + * @return Registry + */ + public function getApplicationPackagesService(Container $container): Registry + { + return (new Registry)->loadFile(JPATH_ROOT . '/packages.yml', 'YAML'); + } + + /** + * Get the `application.router` service + * + * @param Container $container The DI container. + * + * @return RouterInterface + */ + public function getApplicationRouterService(Container $container): RouterInterface + { + $router = new Router; + + /* + * CMS Admin Panels + */ + $router->get( + '/administrator', + WrongCmsController::class + ); + + $router->get( + '/administrator/*', + WrongCmsController::class + ); + + $router->get( + '/wp-admin', + WrongCmsController::class + ); + + $router->get( + '/wp-admin/*', + WrongCmsController::class + ); + + $router->get( + 'wp-login.php', + WrongCmsController::class + ); + + /* + * Web routes + */ + $router->addRoute(new Route(['GET', 'HEAD'], '/', HomepageController::class)); + + $router->get( + '/status', + StatusController::class + ); + + $router->get( + '/:view', + PageController::class + ); + + $router->get( + '/status/:package', + PackageController::class + ); + + /* + * API routes + */ + $router->get( + '/api/v1/packages', + StatusControllerGet::class, + [], + [ + '_format' => 'json', + ] + ); + + $router->get( + '/api/v1/packages/:package', + PackageControllerGet::class, + [], + [ + '_format' => 'json', + ] + ); + + return $router; + } + + /** + * Get the LoaderInterface service + * + * @param Container $container The DI container. + * + * @return LoaderInterface + */ + public function getCommandLoaderService(Container $container): LoaderInterface + { + $mapping = [ + DebugEventDispatcherCommand::getDefaultName() => DebugEventDispatcherCommand::class, + DebugRouterCommand::getDefaultName() => DebugRouterCommand::class, + DownloadsCommand::getDefaultName() => DownloadsCommand::class, + PackageSyncCommand::getDefaultName() => PackageSyncCommand::class, + PackagistSyncCommand::getDefaultName() => PackagistSyncCommand::class, + GenerateSriCommand::getDefaultName() => GenerateSriCommand::class, + ResetCacheCommand::getDefaultName() => ResetCacheCommand::class, + UpdateCommand::getDefaultName() => UpdateCommand::class, + ]; + + return new ContainerLoader($container, $mapping); + } + + /** + * Get the ConsoleApplication service + * + * @param Container $container The DI container. + * + * @return ConsoleApplication + */ + public function getConsoleApplicationService(Container $container): ConsoleApplication + { + $application = new ConsoleApplication(new ArgvInput, new ConsoleOutput, $container->get('config')); + + $application->setCommandLoader($container->get(LoaderInterface::class)); + $application->setDispatcher($container->get(DispatcherInterface::class)); + $application->setLogger($container->get(LoggerInterface::class)); + $application->setName('Joomla! Framework Website'); + + return $application; + } + + /** + * Get the `controller.api.package` service + * + * @param Container $container The DI container. + * + * @return PackageControllerGet + */ + public function getControllerApiPackageService(Container $container): PackageControllerGet + { + $controller = new PackageControllerGet( + $container->get(PackageJsonView::class), + $container->get(Analytics::class), + $container->get(Input::class), + $container->get(WebApplication::class) + ); + + $controller->setLogger($container->get(LoggerInterface::class)); + + return $controller; + } + + /** + * Get the `controller.api.status` service + * + * @param Container $container The DI container. + * + * @return StatusControllerGet + */ + public function getControllerApiStatusService(Container $container): StatusControllerGet + { + $controller = new StatusControllerGet( + $container->get(StatusJsonView::class), + $container->get(Analytics::class), + $container->get(Input::class), + $container->get(WebApplication::class) + ); + + $controller->setLogger($container->get(LoggerInterface::class)); + + return $controller; + } + + /** + * Get the `controller.homepage` service + * + * @param Container $container The DI container. + * + * @return HomepageController + */ + public function getControllerHomepageService(Container $container): HomepageController + { + return new HomepageController( + $container->get(RendererInterface::class), + $container->get(Input::class), + $container->get(WebApplication::class) + ); + } + + /** + * Get the `controller.package` service + * + * @param Container $container The DI container. + * + * @return PackageController + */ + public function getControllerPackageService(Container $container): PackageController + { + return new PackageController( + $container->get(PackageHtmlView::class), + $container->get(Input::class), + $container->get(WebApplication::class) + ); + } + + /** + * Get the `controller.page` service + * + * @param Container $container The DI container. + * + * @return PageController + */ + public function getControllerPageService(Container $container): PageController + { + return new PageController( + $container->get(RendererInterface::class), + $container->get(Input::class), + $container->get(WebApplication::class) + ); + } + + /** + * Get the controller resolver service + * + * @param Container $container The DI container. + * + * @return ControllerResolverInterface + */ + public function getControllerResolverService(Container $container): ControllerResolverInterface + { + return new ContainerControllerResolver($container); + } + + /** + * Get the `controller.status` service + * + * @param Container $container The DI container. + * + * @return StatusController + */ + public function getControllerStatusService(Container $container): StatusController + { + return new StatusController( + $container->get(StatusHtmlView::class), + $container->get(Input::class), + $container->get(WebApplication::class) + ); + } + + /** + * Get the `controller.wrong.cms` service + * + * @param Container $container The DI container. + * + * @return WrongCmsController + */ + public function getControllerWrongCmsService(Container $container): WrongCmsController + { + return new WrongCmsController( + $container->get(Input::class), + $container->get(WebApplication::class) + ); + } + + /** + * Get the DebugEventDispatcherCommand service + * + * @param Container $container The DI container. + * + * @return DebugEventDispatcherCommand + */ + public function getDebugEventDispatcherCommandService(Container $container): DebugEventDispatcherCommand + { + return new DebugEventDispatcherCommand( + $container->get(DispatcherInterface::class) + ); + } + + /** + * Get the DebugRouterCommand service + * + * @param Container $container The DI container. + * + * @return DebugRouterCommand + */ + public function getDebugRouterCommandService(Container $container): DebugRouterCommand + { + return new DebugRouterCommand( + $container->get(Router::class) + ); + } + + /** + * Get the DownloadsCommand service + * + * @param Container $container The DI container. + * + * @return DownloadsCommand + */ + public function getDownloadsCommandService(Container $container): DownloadsCommand + { + return new DownloadsCommand( + $container->get(PackagistHelper::class) + ); + } + + /** + * Get the GenerateSriCommand service + * + * @param Container $container The DI container. + * + * @return GenerateSriCommand + */ + public function getGenerateSriCommandService(Container $container): GenerateSriCommand + { + return new GenerateSriCommand; + } + + /** + * Get the Input class service + * + * @param Container $container The DI container. + * + * @return Input + */ + public function getInputClassService(Container $container): Input + { + return new Input($_REQUEST); + } + + /** + * Get the `model.package` service + * + * @param Container $container The DI container. + * + * @return PackageModel + */ + public function getModelPackageService(Container $container): PackageModel + { + return new PackageModel($container->get(DatabaseInterface::class)); + } + + /** + * Get the `model.release` service + * + * @param Container $container The DI container. + * + * @return ReleaseModel + */ + public function getModelReleaseService(Container $container): ReleaseModel + { + return new ReleaseModel($container->get(DatabaseInterface::class)); + } + + /** + * Get the PackageSyncCommand service + * + * @param Container $container The DI container. + * + * @return PackageSyncCommand + */ + public function getPackageSyncCommandService(Container $container): PackageSyncCommand + { + return new PackageSyncCommand( + $container->get(Helper::class), + $container->get(PackageModel::class) + ); + } + + /** + * Get the PackagistSyncCommand service + * + * @param Container $container The DI container. + * + * @return PackagistSyncCommand + */ + public function getPackagistSyncCommandService(Container $container): PackagistSyncCommand + { + return new PackagistSyncCommand( + $container->get(Http::class), + $container->get(PackageModel::class), + $container->get(ReleaseModel::class) + ); + } + + /** + * Get the ResetCacheCommand service + * + * @param Container $container The DI container. + * + * @return ResetCacheCommand + */ + public function getResetCacheCommandService(Container $container): ResetCacheCommand + { + return new ResetCacheCommand( + $container->get(TwigRenderer::class), + $container->get('config') + ); + } + + /** + * Get the UpdateCommand service + * + * @param Container $container The DI container. + * + * @return UpdateCommand + */ + public function getUpdateCommandService(Container $container): UpdateCommand + { + return new UpdateCommand; + } + + /** + * Get the `view.package.html` service + * + * @param Container $container The DI container. + * + * @return PackageHtmlView + */ + public function getViewPackageHtmlService(Container $container): PackageHtmlView + { + $view = new PackageHtmlView( + $container->get('model.package'), + $container->get('model.release'), + $container->get(Helper::class), + $container->get('renderer') + ); + + $view->setLayout('package.twig'); + + return $view; + } + + /** + * Get the `view.package.json` service + * + * @param Container $container The DI container. + * + * @return PackageJsonView + */ + public function getViewPackageJsonService(Container $container): PackageJsonView + { + return new PackageJsonView( + $container->get('model.package'), + $container->get('model.release') + ); + } + + /** + * Get the `view.status.html` service + * + * @param Container $container The DI container. + * + * @return StatusHtmlView + */ + public function getViewStatusHtmlService(Container $container): StatusHtmlView + { + $view = new StatusHtmlView( + $container->get('model.package'), + $container->get('model.release'), + $container->get('renderer') + ); + + $view->setLayout('status.twig'); + + return $view; + } + + /** + * Get the `view.status.json` service + * + * @param Container $container The DI container. + * + * @return StatusJsonView + */ + public function getViewStatusJsonService(Container $container): StatusJsonView + { + return new StatusJsonView( + $container->get('model.package'), + $container->get('model.release') + ); + } + + /** + * Get the WebApplication class service + * + * @param Container $container The DI container. + * + * @return WebApplication + */ + public function getWebApplicationClassService(Container $container): WebApplication + { + $application = new WebApplication( + $container->get(ControllerResolverInterface::class), + $container->get(RouterInterface::class), + $container->get(Input::class), + $container->get('config'), + $container->get(WebClient::class) + ); + + $application->httpVersion = '2'; + + // Inject extra services + $application->setDispatcher($container->get(DispatcherInterface::class)); + $application->setLogger($container->get(LoggerInterface::class)); + + return $application; + } + + /** + * Get the web client service + * + * @param Container $container The DI container. + * + * @return WebClient + */ + public function getWebClientService(Container $container): WebClient + { + /** @var Input $input */ + $input = $container->get(Input::class); + $userAgent = $input->server->getString('HTTP_USER_AGENT', ''); + $acceptEncoding = $input->server->getString('HTTP_ACCEPT_ENCODING', ''); + $acceptLanguage = $input->server->getString('HTTP_ACCEPT_LANGUAGE', ''); + + return new WebClient($userAgent, $acceptEncoding, $acceptLanguage); + } +} diff --git a/week-03/project/libraries/src/Service/ConfigurationProvider.php b/week-03/project/libraries/src/Service/ConfigurationProvider.php new file mode 100644 index 0000000..455c294 --- /dev/null +++ b/week-03/project/libraries/src/Service/ConfigurationProvider.php @@ -0,0 +1,66 @@ +config = (new Registry)->loadFile($file); + + // Hardcode database driver option + $this->config->set('database.driver', 'mysql'); + } + + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + */ + public function register(Container $container): void + { + $container->share( + 'config', + function (): Registry + { + return $this->config; + }, + true + ); + } +} diff --git a/week-03/project/libraries/src/Service/EventProvider.php b/week-03/project/libraries/src/Service/EventProvider.php new file mode 100644 index 0000000..4fdfa8d --- /dev/null +++ b/week-03/project/libraries/src/Service/EventProvider.php @@ -0,0 +1,74 @@ +alias(Dispatcher::class, DispatcherInterface::class) + ->share(DispatcherInterface::class, [$this, 'getDispatcherService']); + + $container->share(ErrorSubscriber::class, [$this, 'getErrorSubscriber'], true) + ->tag('event.subscriber', [ErrorSubscriber::class]); + } + + /** + * Get the DispatcherInterface service + * + * @param Container $container The DI container. + * + * @return DispatcherInterface + */ + public function getDispatcherService(Container $container): DispatcherInterface + { + $dispatcher = new Dispatcher; + + foreach ($container->getTagged('event.subscriber') as $subscriber) + { + $dispatcher->addSubscriber($subscriber); + } + + return $dispatcher; + } + + /** + * Get the ErrorSubscriber service + * + * @param Container $container The DI container. + * + * @return ErrorSubscriber + */ + public function getErrorSubscriber(Container $container): ErrorSubscriber + { + $subscriber = new ErrorSubscriber($container->get(RendererInterface::class)); + $subscriber->setLogger($container->get(LoggerInterface::class)); + + return $subscriber; + } +} diff --git a/week-03/project/libraries/src/Service/HttpProvider.php b/week-03/project/libraries/src/Service/HttpProvider.php new file mode 100644 index 0000000..4758caf --- /dev/null +++ b/week-03/project/libraries/src/Service/HttpProvider.php @@ -0,0 +1,63 @@ +alias(Http::class, 'http') + ->share('http', [$this, 'getHttpService'], true); + + $container->alias(HttpFactory::class, 'http.factory') + ->share('http.factory', [$this, 'getHttpFactoryService'], true); + } + + /** + * Get the `http` service + * + * @param Container $container The DI container. + * + * @return Http + */ + public function getHttpService(Container $container): Http + { + /** @var HttpFactory $factory */ + $factory = $container->get('http.factory'); + + return $factory->getHttp(); + } + + /** + * Get the `http.factory` service + * + * @param Container $container The DI container. + * + * @return HttpFactory + */ + public function getHttpFactoryService(Container $container): HttpFactory + { + return new HttpFactory; + } +} diff --git a/week-03/project/libraries/src/Service/LoggingProvider.php b/week-03/project/libraries/src/Service/LoggingProvider.php new file mode 100644 index 0000000..e479906 --- /dev/null +++ b/week-03/project/libraries/src/Service/LoggingProvider.php @@ -0,0 +1,131 @@ +share('monolog.handler.application', [$this, 'getMonologHandlerApplicationService'], true); + + /* + * Monolog Processors + */ + $container->share('monolog.processor.psr3', [$this, 'getMonologProcessorPsr3Service'], true); + $container->share('monolog.processor.web', [$this, 'getMonologProcessorWebService'], true); + + /* + * Application Loggers + */ + $container->share('monolog.logger.application.cli', [$this, 'getMonologLoggerApplicationCliService'], true); + $container->share('monolog.logger.application.web', [$this, 'getMonologLoggerApplicationWebService'], true); + } + + /** + * Get the `monolog.handler.application` service + * + * @param Container $container The DI container. + * + * @return StreamHandler + */ + public function getMonologHandlerApplicationService(Container $container): StreamHandler + { + /** @var \Joomla\Registry\Registry $config */ + $config = $container->get('config'); + + $level = strtoupper($config->get('log.application', $config->get('log.level', 'error'))); + + return new StreamHandler(JPATH_ROOT . '/logs/framework.log', \constant('\\Monolog\\Logger::' . $level)); + } + + /** + * Get the `monolog.logger.application.cli` service + * + * @param Container $container The DI container. + * + * @return Logger + */ + public function getMonologLoggerApplicationCliService(Container $container): Logger + { + return new Logger( + 'Framework', + [ + $container->get('monolog.handler.application'), + ], + [ + $container->get('monolog.processor.psr3'), + ] + ); + } + + /** + * Get the `monolog.logger.application.web` service + * + * @param Container $container The DI container. + * + * @return Logger + */ + public function getMonologLoggerApplicationWebService(Container $container): Logger + { + return new Logger( + 'Framework', + [ + $container->get('monolog.handler.application'), + ], + [ + $container->get('monolog.processor.psr3'), + $container->get('monolog.processor.web'), + ] + ); + } + + /** + * Get the `monolog.processor.psr3` service + * + * @param Container $container The DI container. + * + * @return PsrLogMessageProcessor + */ + public function getMonologProcessorPsr3Service(Container $container): PsrLogMessageProcessor + { + return new PsrLogMessageProcessor; + } + + /** + * Get the `monolog.processor.web` service + * + * @param Container $container The DI container. + * + * @return WebProcessor + */ + public function getMonologProcessorWebService(Container $container): WebProcessor + { + return new WebProcessor; + } +} diff --git a/week-03/project/libraries/src/Service/SessionProvider.php b/week-03/project/libraries/src/Service/SessionProvider.php new file mode 100644 index 0000000..82b06a6 --- /dev/null +++ b/week-03/project/libraries/src/Service/SessionProvider.php @@ -0,0 +1,360 @@ + + * @license GNU General Public License version 2 or later; see LICENSE + */ + +namespace Joomla\CMS\Service\Provider; + +\defined('JPATH_PLATFORM') or die; + +use Joomla\CMS\Application\AdministratorApplication; +use Joomla\CMS\Application\ApplicationHelper; +use Joomla\CMS\Application\CMSApplicationInterface; +use Joomla\CMS\Application\ConsoleApplication; +use Joomla\CMS\Application\SiteApplication; +use Joomla\CMS\Factory; +use Joomla\CMS\Installation\Application\InstallationApplication; +use Joomla\CMS\Session\EventListener\MetadataManagerListener; +use Joomla\CMS\Session\MetadataManager; +use Joomla\CMS\Session\SessionFactory; +use Joomla\CMS\Session\SessionManager; +use Joomla\CMS\Session\Storage\JoomlaStorage; +use Joomla\Database\DatabaseInterface; +use Joomla\DI\Container; +use Joomla\DI\Exception\DependencyResolutionException; +use Joomla\DI\ServiceProviderInterface; +use Joomla\Event\DispatcherInterface; +use Joomla\Event\LazyServiceEventListener; +use Joomla\Event\Priority; +use Joomla\Registry\Registry; +use Joomla\Session\HandlerInterface; +use Joomla\Session\SessionEvents; +use Joomla\Session\SessionInterface; +use Joomla\Session\Storage\RuntimeStorage; +use Joomla\Session\StorageInterface; + +/** + * Service provider for the application's session dependency + * + * @since 4.0.0 + */ +class Session implements ServiceProviderInterface +{ + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->share( + 'session.web.administrator', + function (Container $container) + { + /** @var Registry $config */ + $config = $container->get('config'); + $app = Factory::getApplication(); + + // Generate a session name. + $name = ApplicationHelper::getHash($config->get('session_name', AdministratorApplication::class)); + + // Calculate the session lifetime. + $lifetime = $config->get('lifetime') ? $config->get('lifetime') * 60 : 900; + + // Initialize the options for the Session object. + $options = [ + 'name' => $name, + 'expire' => $lifetime, + ]; + + if ($config->get('force_ssl') >= 1) + { + $options['force_ssl'] = true; + } + + $handler = $container->get('session.factory')->createSessionHandler($options); + + if (!$container->has('session.handler')) + { + $this->registerSessionHandlerAsService($container, $handler); + } + + return $this->buildSession( + new JoomlaStorage($app->input, $handler), + $app, + $container->get(DispatcherInterface::class), + $options + ); + }, + true + ); + + $container->share( + 'session.web.installation', + function (Container $container) + { + /** @var Registry $config */ + $config = $container->get('config'); + $app = Factory::getApplication(); + + /** + * Session handler for the session is always filesystem so it doesn't flip to the database after + * configuration.php has been written to + */ + $config->set('session_handler', 'filesystem'); + + /** + * Generate a session name - unlike all the other apps we don't have either a secret or a session name + * (that's not the app name) until we complete installation which then leads to us dropping things like + * language preferences after installation as the app refreshes. + */ + $name = md5(serialize(JPATH_ROOT . InstallationApplication::class)); + + // Calculate the session lifetime. + $lifetime = $config->get('lifetime') ? $config->get('lifetime') * 60 : 900; + + // Initialize the options for the Session object. + $options = [ + 'name' => $name, + 'expire' => $lifetime, + ]; + + $handler = $container->get('session.factory')->createSessionHandler($options); + + if (!$container->has('session.handler')) + { + $this->registerSessionHandlerAsService($container, $handler); + } + + return $this->buildSession( + new JoomlaStorage($app->input, $handler), + $app, + $container->get(DispatcherInterface::class), + $options + ); + }, + true + ); + + $container->share( + 'session.web.site', + function (Container $container) + { + /** @var Registry $config */ + $config = $container->get('config'); + $app = Factory::getApplication(); + + // Generate a session name. + $name = ApplicationHelper::getHash($config->get('session_name', SiteApplication::class)); + + // Calculate the session lifetime. + $lifetime = $config->get('lifetime') ? $config->get('lifetime') * 60 : 900; + + // Initialize the options for the Session object. + $options = [ + 'name' => $name, + 'expire' => $lifetime, + ]; + + if ($config->get('force_ssl') == 2) + { + $options['force_ssl'] = true; + } + + $handler = $container->get('session.factory')->createSessionHandler($options); + + if (!$container->has('session.handler')) + { + $this->registerSessionHandlerAsService($container, $handler); + } + + return $this->buildSession( + new JoomlaStorage($app->input, $handler), + $app, + $container->get(DispatcherInterface::class), + $options + ); + }, + true + ); + + $container->share( + 'session.cli', + function (Container $container) + { + /** @var Registry $config */ + $config = $container->get('config'); + $app = Factory::getApplication(); + + // Generate a session name. + $name = ApplicationHelper::getHash($config->get('session_name', ConsoleApplication::class)); + + // Calculate the session lifetime. + $lifetime = $config->get('lifetime') ? $config->get('lifetime') * 60 : 900; + + // Initialize the options for the Session object. + $options = [ + 'name' => $name, + 'expire' => $lifetime, + ]; + + // Unlike the web apps, we will only toggle the force SSL setting based on it being enabled and not based on client + if ($config->get('force_ssl') >= 1) + { + $options['force_ssl'] = true; + } + + $handler = $container->get('session.factory')->createSessionHandler($options); + + if (!$container->has('session.handler')) + { + $this->registerSessionHandlerAsService($container, $handler); + } + + return $this->buildSession( + new RuntimeStorage, + $app, + $container->get(DispatcherInterface::class), + $options + ); + }, + true + ); + + $container->alias(SessionFactory::class, 'session.factory') + ->share( + 'session.factory', + function (Container $container) + { + $factory = new SessionFactory; + $factory->setContainer($container); + + return $factory; + }, + true + ); + + $container->alias(SessionManager::class, 'session.manager') + ->share( + 'session.manager', + function (Container $container) + { + if (!$container->has('session.handler')) + { + throw new DependencyResolutionException( + 'The "session.handler" service has not been created, make sure you have created the "session" service first.' + ); + } + + return new SessionManager($container->get('session.handler')); + }, + true + ); + + $container->alias(MetadataManager::class, 'session.metadata_manager') + ->share( + 'session.metadata_manager', + function (Container $container) + { + /* + * Normally we should inject the application as a dependency via $container->get() however there is not + * a 'app' or CMSApplicationInterface::class key for the primary application of the request so we need to + * rely on the application having been injected to the global Factory otherwise we cannot build the service + */ + if (!Factory::$application) + { + throw new DependencyResolutionException( + sprintf( + 'Creating the "session.metadata_manager" service requires %s::$application be initialised.', + Factory::class + ) + ); + } + + return new MetadataManager(Factory::$application, $container->get(DatabaseInterface::class)); + }, + true + ); + + $container->alias(MetadataManagerListener::class, 'session.event_listener.metadata_manager') + ->share( + 'session.event_listener.metadata_manager', + function (Container $container) + { + return new MetadataManagerListener($container->get(MetadataManager::class), $container->get('config')); + }, + true + ); + + $listener = new LazyServiceEventListener($container, 'session.event_listener.metadata_manager', 'onAfterSessionStart'); + + /** @var DispatcherInterface $dispatcher */ + $dispatcher = $container->get(DispatcherInterface::class); + $dispatcher->addListener(SessionEvents::START, $listener); + } + + /** + * Build the root session service + * + * @param StorageInterface $storage The session storage engine. + * @param CMSApplicationInterface $app The application instance. + * @param DispatcherInterface $dispatcher The event dispatcher. + * @param array $options The configured session options. + * + * @return SessionInterface + * + * @since 4.0.0 + */ + private function buildSession( + StorageInterface $storage, + CMSApplicationInterface $app, + DispatcherInterface $dispatcher, + array $options + ): SessionInterface + { + $input = $app->input; + + if (method_exists($app, 'afterSessionStart')) + { + $dispatcher->addListener(SessionEvents::START, [$app, 'afterSessionStart'], Priority::HIGH); + } + + $session = new \Joomla\CMS\Session\Session($storage, $dispatcher, $options); + + return $session; + } + + /** + * Registers the session handler as a service + * + * @param Container $container The container to register the service to. + * @param \SessionHandlerInterface $sessionHandler The session handler. + * + * @return void + * + * @since 4.0.0 + */ + private function registerSessionHandlerAsService(Container $container, \SessionHandlerInterface $sessionHandler): void + { + // Alias the session handler to the core SessionHandlerInterface for improved autowiring and discoverability + $container->alias(\SessionHandlerInterface::class, 'session.handler') + ->share( + 'session.handler', + $sessionHandler, + true + ); + + // If the session handler implements the extended interface, register an alias for that as well + if ($sessionHandler instanceof HandlerInterface) + { + $container->alias(HandlerInterface::class, 'session.handler'); + } + } +} diff --git a/week-03/project/libraries/src/Service/SiteApplicationProvider.php b/week-03/project/libraries/src/Service/SiteApplicationProvider.php new file mode 100644 index 0000000..648b1bf --- /dev/null +++ b/week-03/project/libraries/src/Service/SiteApplicationProvider.php @@ -0,0 +1,748 @@ +share(ConsoleApplication::class, [$this, 'getConsoleApplicationService'], true); + + // This service cannot be protected as it is decorated when the debug bar is available + $container->alias(WebApplication::class, AbstractWebApplication::class) + ->share(AbstractWebApplication::class, [$this, 'getWebApplicationClassService']); + + /* + * Application Helpers and Dependencies + */ + + $container->alias(Analytics::class, 'analytics') + ->share('analytics', [$this, 'getAnalyticsService'], true); + + $container->alias(ContainerLoader::class, LoaderInterface::class) + ->share(LoaderInterface::class, [$this, 'getCommandLoaderService'], true); + + // This service cannot be protected as it is decorated when the debug bar is available + $container->alias(ContainerControllerResolver::class, ControllerResolverInterface::class) + ->share(ControllerResolverInterface::class, [$this, 'getControllerResolverService']); + + $container->alias(Helper::class, 'application.helper') + ->share('application.helper', [$this, 'getApplicationHelperService'], true); + + $container->alias(PackagistHelper::class, 'application.helper.packagist') + ->share('application.helper.packagist', [$this, 'getApplicationHelperPackagistService'], true); + + $container->share('application.packages', [$this, 'getApplicationPackagesService'], true); + + $container->share(WebClient::class, [$this, 'getWebClientService'], true); + + // This service cannot be protected as it is decorated when the debug bar is available + $container->alias(RouterInterface::class, 'application.router') + ->alias(Router::class, 'application.router') + ->share('application.router', [$this, 'getApplicationRouterService']); + + $container->share(Input::class, [$this, 'getInputClassService'], true); + + /* + * Console Commands + */ + + $container->share(DebugEventDispatcherCommand::class, [$this, 'getDebugEventDispatcherCommandService'], true); + $container->share(DebugRouterCommand::class, [$this, 'getDebugRouterCommandService'], true); + $container->share(DownloadsCommand::class, [$this, 'getDownloadsCommandService'], true); + $container->share(GenerateSriCommand::class, [$this, 'getGenerateSriCommandService'], true); + $container->share(PackageSyncCommand::class, [$this, 'getPackageSyncCommandService'], true); + $container->share(PackagistSyncCommand::class, [$this, 'getPackagistSyncCommandService'], true); + $container->share(ResetCacheCommand::class, [$this, 'getResetCacheCommandService'], true); + $container->share(UpdateCommand::class, [$this, 'getUpdateCommandService'], true); + + /* + * MVC Layer + */ + + // Controllers + $container->alias(PackageControllerGet::class, 'controller.api.package') + ->share('controller.api.package', [$this, 'getControllerApiPackageService'], true); + + $container->alias(StatusControllerGet::class, 'controller.api.status') + ->share('controller.api.status', [$this, 'getControllerApiStatusService'], true); + + $container->alias(HomepageController::class, 'controller.homepage') + ->share('controller.homepage', [$this, 'getControllerHomepageService'], true); + + $container->alias(PackageController::class, 'controller.package') + ->share('controller.package', [$this, 'getControllerPackageService'], true); + + $container->alias(PageController::class, 'controller.page') + ->share('controller.page', [$this, 'getControllerPageService'], true); + + $container->alias(StatusController::class, 'controller.status') + ->share('controller.status', [$this, 'getControllerStatusService'], true); + + $container->alias(WrongCmsController::class, 'controller.wrong.cms') + ->share('controller.wrong.cms', [$this, 'getControllerWrongCmsService'], true); + + // Models + $container->alias(PackageModel::class, 'model.package') + ->share('model.package', [$this, 'getModelPackageService'], true); + + $container->alias(ReleaseModel::class, 'model.release') + ->share('model.release', [$this, 'getModelReleaseService'], true); + + // Views + $container->alias(PackageHtmlView::class, 'view.package.html') + ->share('view.package.html', [$this, 'getViewPackageHtmlService'], true); + + $container->alias(PackageJsonView::class, 'view.package.json') + ->share('view.package.json', [$this, 'getViewPackageJsonService'], true); + + $container->alias(StatusHtmlView::class, 'view.status.html') + ->share('view.status.html', [$this, 'getViewStatusHtmlService'], true); + + $container->alias(StatusJsonView::class, 'view.status.json') + ->share('view.status.json', [$this, 'getViewStatusJsonService'], true); + } + + /** + * Get the Analytics class service + * + * @param Container $container The DI container. + * + * @return Analytics + */ + public function getAnalyticsService(Container $container) + { + return new Analytics(true); + } + + /** + * Get the `application.helper` service + * + * @param Container $container The DI container. + * + * @return Helper + */ + public function getApplicationHelperService(Container $container): Helper + { + $helper = new Helper; + $helper->setPackages($container->get('application.packages')); + + return $helper; + } + + /** + * Get the `application.helper.packagist` service + * + * @param Container $container The DI container. + * + * @return PackagistHelper + */ + public function getApplicationHelperPackagistService(Container $container): PackagistHelper + { + $helper = new PackagistHelper($container->get(Http::class), $container->get(DatabaseInterface::class)); + $helper->setPackages($container->get('application.packages')); + + return $helper; + } + + /** + * Get the `application.packages` service + * + * @param Container $container The DI container. + * + * @return Registry + */ + public function getApplicationPackagesService(Container $container): Registry + { + return (new Registry)->loadFile(JPATH_ROOT . '/packages.yml', 'YAML'); + } + + /** + * Get the `application.router` service + * + * @param Container $container The DI container. + * + * @return RouterInterface + */ + public function getApplicationRouterService(Container $container): RouterInterface + { + $router = new Router; + + /* + * CMS Admin Panels + */ + $router->get( + '/administrator', + WrongCmsController::class + ); + + $router->get( + '/administrator/*', + WrongCmsController::class + ); + + $router->get( + '/wp-admin', + WrongCmsController::class + ); + + $router->get( + '/wp-admin/*', + WrongCmsController::class + ); + + $router->get( + 'wp-login.php', + WrongCmsController::class + ); + + /* + * Web routes + */ + $router->addRoute(new Route(['GET', 'HEAD'], '/', HomepageController::class)); + + $router->get( + '/status', + StatusController::class + ); + + $router->get( + '/:view', + PageController::class + ); + + $router->get( + '/status/:package', + PackageController::class + ); + + /* + * API routes + */ + $router->get( + '/api/v1/packages', + StatusControllerGet::class, + [], + [ + '_format' => 'json', + ] + ); + + $router->get( + '/api/v1/packages/:package', + PackageControllerGet::class, + [], + [ + '_format' => 'json', + ] + ); + + return $router; + } + + /** + * Get the LoaderInterface service + * + * @param Container $container The DI container. + * + * @return LoaderInterface + */ + public function getCommandLoaderService(Container $container): LoaderInterface + { + $mapping = [ + DebugEventDispatcherCommand::getDefaultName() => DebugEventDispatcherCommand::class, + DebugRouterCommand::getDefaultName() => DebugRouterCommand::class, + DownloadsCommand::getDefaultName() => DownloadsCommand::class, + PackageSyncCommand::getDefaultName() => PackageSyncCommand::class, + PackagistSyncCommand::getDefaultName() => PackagistSyncCommand::class, + GenerateSriCommand::getDefaultName() => GenerateSriCommand::class, + ResetCacheCommand::getDefaultName() => ResetCacheCommand::class, + UpdateCommand::getDefaultName() => UpdateCommand::class, + ]; + + return new ContainerLoader($container, $mapping); + } + + /** + * Get the ConsoleApplication service + * + * @param Container $container The DI container. + * + * @return ConsoleApplication + */ + public function getConsoleApplicationService(Container $container): ConsoleApplication + { + $application = new ConsoleApplication(new ArgvInput, new ConsoleOutput, $container->get('config')); + + $application->setCommandLoader($container->get(LoaderInterface::class)); + $application->setDispatcher($container->get(DispatcherInterface::class)); + $application->setLogger($container->get(LoggerInterface::class)); + $application->setName('Joomla! Framework Website'); + + return $application; + } + + /** + * Get the `controller.api.package` service + * + * @param Container $container The DI container. + * + * @return PackageControllerGet + */ + public function getControllerApiPackageService(Container $container): PackageControllerGet + { + $controller = new PackageControllerGet( + $container->get(PackageJsonView::class), + $container->get(Analytics::class), + $container->get(Input::class), + $container->get(WebApplication::class) + ); + + $controller->setLogger($container->get(LoggerInterface::class)); + + return $controller; + } + + /** + * Get the `controller.api.status` service + * + * @param Container $container The DI container. + * + * @return StatusControllerGet + */ + public function getControllerApiStatusService(Container $container): StatusControllerGet + { + $controller = new StatusControllerGet( + $container->get(StatusJsonView::class), + $container->get(Analytics::class), + $container->get(Input::class), + $container->get(WebApplication::class) + ); + + $controller->setLogger($container->get(LoggerInterface::class)); + + return $controller; + } + + /** + * Get the `controller.homepage` service + * + * @param Container $container The DI container. + * + * @return HomepageController + */ + public function getControllerHomepageService(Container $container): HomepageController + { + return new HomepageController( + $container->get(RendererInterface::class), + $container->get(Input::class), + $container->get(WebApplication::class) + ); + } + + /** + * Get the `controller.package` service + * + * @param Container $container The DI container. + * + * @return PackageController + */ + public function getControllerPackageService(Container $container): PackageController + { + return new PackageController( + $container->get(PackageHtmlView::class), + $container->get(Input::class), + $container->get(WebApplication::class) + ); + } + + /** + * Get the `controller.page` service + * + * @param Container $container The DI container. + * + * @return PageController + */ + public function getControllerPageService(Container $container): PageController + { + return new PageController( + $container->get(RendererInterface::class), + $container->get(Input::class), + $container->get(WebApplication::class) + ); + } + + /** + * Get the controller resolver service + * + * @param Container $container The DI container. + * + * @return ControllerResolverInterface + */ + public function getControllerResolverService(Container $container): ControllerResolverInterface + { + return new ContainerControllerResolver($container); + } + + /** + * Get the `controller.status` service + * + * @param Container $container The DI container. + * + * @return StatusController + */ + public function getControllerStatusService(Container $container): StatusController + { + return new StatusController( + $container->get(StatusHtmlView::class), + $container->get(Input::class), + $container->get(WebApplication::class) + ); + } + + /** + * Get the `controller.wrong.cms` service + * + * @param Container $container The DI container. + * + * @return WrongCmsController + */ + public function getControllerWrongCmsService(Container $container): WrongCmsController + { + return new WrongCmsController( + $container->get(Input::class), + $container->get(WebApplication::class) + ); + } + + /** + * Get the DebugEventDispatcherCommand service + * + * @param Container $container The DI container. + * + * @return DebugEventDispatcherCommand + */ + public function getDebugEventDispatcherCommandService(Container $container): DebugEventDispatcherCommand + { + return new DebugEventDispatcherCommand( + $container->get(DispatcherInterface::class) + ); + } + + /** + * Get the DebugRouterCommand service + * + * @param Container $container The DI container. + * + * @return DebugRouterCommand + */ + public function getDebugRouterCommandService(Container $container): DebugRouterCommand + { + return new DebugRouterCommand( + $container->get(Router::class) + ); + } + + /** + * Get the DownloadsCommand service + * + * @param Container $container The DI container. + * + * @return DownloadsCommand + */ + public function getDownloadsCommandService(Container $container): DownloadsCommand + { + return new DownloadsCommand( + $container->get(PackagistHelper::class) + ); + } + + /** + * Get the GenerateSriCommand service + * + * @param Container $container The DI container. + * + * @return GenerateSriCommand + */ + public function getGenerateSriCommandService(Container $container): GenerateSriCommand + { + return new GenerateSriCommand; + } + + /** + * Get the Input class service + * + * @param Container $container The DI container. + * + * @return Input + */ + public function getInputClassService(Container $container): Input + { + return new Input($_REQUEST); + } + + /** + * Get the `model.package` service + * + * @param Container $container The DI container. + * + * @return PackageModel + */ + public function getModelPackageService(Container $container): PackageModel + { + return new PackageModel($container->get(DatabaseInterface::class)); + } + + /** + * Get the `model.release` service + * + * @param Container $container The DI container. + * + * @return ReleaseModel + */ + public function getModelReleaseService(Container $container): ReleaseModel + { + return new ReleaseModel($container->get(DatabaseInterface::class)); + } + + /** + * Get the PackageSyncCommand service + * + * @param Container $container The DI container. + * + * @return PackageSyncCommand + */ + public function getPackageSyncCommandService(Container $container): PackageSyncCommand + { + return new PackageSyncCommand( + $container->get(Helper::class), + $container->get(PackageModel::class) + ); + } + + /** + * Get the PackagistSyncCommand service + * + * @param Container $container The DI container. + * + * @return PackagistSyncCommand + */ + public function getPackagistSyncCommandService(Container $container): PackagistSyncCommand + { + return new PackagistSyncCommand( + $container->get(Http::class), + $container->get(PackageModel::class), + $container->get(ReleaseModel::class) + ); + } + + /** + * Get the ResetCacheCommand service + * + * @param Container $container The DI container. + * + * @return ResetCacheCommand + */ + public function getResetCacheCommandService(Container $container): ResetCacheCommand + { + return new ResetCacheCommand( + $container->get(TwigRenderer::class), + $container->get('config') + ); + } + + /** + * Get the UpdateCommand service + * + * @param Container $container The DI container. + * + * @return UpdateCommand + */ + public function getUpdateCommandService(Container $container): UpdateCommand + { + return new UpdateCommand; + } + + /** + * Get the `view.package.html` service + * + * @param Container $container The DI container. + * + * @return PackageHtmlView + */ + public function getViewPackageHtmlService(Container $container): PackageHtmlView + { + $view = new PackageHtmlView( + $container->get('model.package'), + $container->get('model.release'), + $container->get(Helper::class), + $container->get('renderer') + ); + + $view->setLayout('package.twig'); + + return $view; + } + + /** + * Get the `view.package.json` service + * + * @param Container $container The DI container. + * + * @return PackageJsonView + */ + public function getViewPackageJsonService(Container $container): PackageJsonView + { + return new PackageJsonView( + $container->get('model.package'), + $container->get('model.release') + ); + } + + /** + * Get the `view.status.html` service + * + * @param Container $container The DI container. + * + * @return StatusHtmlView + */ + public function getViewStatusHtmlService(Container $container): StatusHtmlView + { + $view = new StatusHtmlView( + $container->get('model.package'), + $container->get('model.release'), + $container->get('renderer') + ); + + $view->setLayout('status.twig'); + + return $view; + } + + /** + * Get the `view.status.json` service + * + * @param Container $container The DI container. + * + * @return StatusJsonView + */ + public function getViewStatusJsonService(Container $container): StatusJsonView + { + return new StatusJsonView( + $container->get('model.package'), + $container->get('model.release') + ); + } + + /** + * Get the WebApplication class service + * + * @param Container $container The DI container. + * + * @return WebApplication + */ + public function getWebApplicationClassService(Container $container): WebApplication + { + $application = new WebApplication( + $container->get(ControllerResolverInterface::class), + $container->get(RouterInterface::class), + $container->get(Input::class), + $container->get('config'), + $container->get(WebClient::class) + ); + + $application->httpVersion = '2'; + + // Inject extra services + $application->setDispatcher($container->get(DispatcherInterface::class)); + $application->setLogger($container->get(LoggerInterface::class)); + + return $application; + } + + /** + * Get the web client service + * + * @param Container $container The DI container. + * + * @return WebClient + */ + public function getWebClientService(Container $container): WebClient + { + /** @var Input $input */ + $input = $container->get(Input::class); + $userAgent = $input->server->getString('HTTP_USER_AGENT', ''); + $acceptEncoding = $input->server->getString('HTTP_ACCEPT_ENCODING', ''); + $acceptLanguage = $input->server->getString('HTTP_ACCEPT_LANGUAGE', ''); + + return new WebClient($userAgent, $acceptEncoding, $acceptLanguage); + } +} diff --git a/week-03/project/libraries/src/Service/TemplatingProvider.php b/week-03/project/libraries/src/Service/TemplatingProvider.php new file mode 100644 index 0000000..a474465 --- /dev/null +++ b/week-03/project/libraries/src/Service/TemplatingProvider.php @@ -0,0 +1,311 @@ +alias(Packages::class, 'asset.packages') + ->share('asset.packages', [$this, 'getAssetPackagesService'], true); + + $container->alias(RendererInterface::class, 'renderer') + ->alias(TwigRenderer::class, 'renderer') + ->share('renderer', [$this, 'getRendererService'], true); + + $container->alias(CacheInterface::class, 'twig.cache') + ->alias(\Twig_CacheInterface::class, 'twig.cache') + ->share('twig.cache', [$this, 'getTwigCacheService'], true); + + $container->alias(Environment::class, 'twig.environment') + ->alias(\Twig_Environment::class, 'twig.environment') + ->share('twig.environment', [$this, 'getTwigEnvironmentService'], true); + + $container->alias(DebugExtension::class, 'twig.extension.debug') + ->alias(\Twig_Extension_Debug::class, 'twig.extension.debug') + ->share('twig.extension.debug', [$this, 'getTwigExtensionDebugService'], true); + + $container->alias(FrameworkExtension::class, 'twig.extension.framework') + ->share('twig.extension.framework', [$this, 'getTwigExtensionFrameworkService'], true); + + // This service cannot be protected as it is decorated when the debug bar is available + $container->alias(ProfilerExtension::class, 'twig.extension.profiler') + ->alias(\Twig_Extension_Profiler::class, 'twig.extension.profiler') + ->share('twig.extension.profiler', [$this, 'getTwigExtensionProfilerService']); + + $container->alias(LoaderInterface::class, 'twig.loader') + ->alias(\Twig_LoaderInterface::class, 'twig.loader') + ->share('twig.loader', [$this, 'getTwigLoaderService'], true); + + $container->alias(Profile::class, 'twig.profiler.profile') + ->alias(\Twig_Profiler_Profile::class, 'twig.profiler.profile') + ->share('twig.profiler.profile', [$this, 'getTwigProfilerProfileService'], true); + + $container->alias(FrameworkTwigRuntime::class, 'twig.runtime.framework') + ->share('twig.runtime.framework', [$this, 'getTwigRuntimeFrameworkService'], true); + + $container->alias(ContainerRuntimeLoader::class, 'twig.runtime.loader') + ->alias(\Twig_ContainerRuntimeLoader::class, 'twig.runtime.loader') + ->share('twig.runtime.loader', [$this, 'getTwigRuntimeLoaderService'], true); + + $this->tagTwigExtensions($container); + } + + /** + * Get the `asset.packages` service + * + * @param Container $container The DI container. + * + * @return Packages + */ + public function getAssetPackagesService(Container $container): Packages + { + /** @var AbstractApplication $app */ + $app = $container->get(AbstractApplication::class); + + $context = new ApplicationContext($app); + + $mediaPath = $app->get('uri.media.path', '/media/'); + + $defaultPackage = new PathPackage($mediaPath, new EmptyVersionStrategy, $context); + + $mixStrategy = new MixPathPackage( + $defaultPackage, + $mediaPath, + new JsonManifestVersionStrategy(JPATH_ROOT . '/www/media/mix-manifest.json'), + $context + ); + + return new Packages( + $defaultPackage, + [ + 'mix' => $mixStrategy, + ] + ); + } + + /** + * Get the `renderer` service + * + * @param Container $container The DI container. + * + * @return RendererInterface + */ + public function getRendererService(Container $container): RendererInterface + { + return new TwigRenderer($container->get('twig.environment')); + } + + /** + * Get the `twig.cache` service + * + * @param Container $container The DI container. + * + * @return \Twig_CacheInterface + */ + public function getTwigCacheService(Container $container): \Twig_CacheInterface + { + /** @var \Joomla\Registry\Registry $config */ + $config = $container->get('config'); + + // Pull down the renderer config + $cacheEnabled = $config->get('template.cache.enabled', false); + $cachePath = $config->get('template.cache.path', 'cache/twig'); + $debug = $config->get('template.debug', false); + + if ($debug === false && $cacheEnabled !== false) + { + return new FilesystemCache(JPATH_ROOT . '/' . $cachePath); + } + + return new NullCache; + } + + /** + * Get the `twig.environment` service + * + * @param Container $container The DI container. + * + * @return Environment + */ + public function getTwigEnvironmentService(Container $container): Environment + { + /** @var \Joomla\Registry\Registry $config */ + $config = $container->get('config'); + + $debug = $config->get('template.debug', false); + + $environment = new Environment( + $container->get('twig.loader'), + ['debug' => $debug] + ); + + // Add the runtime loader + $environment->addRuntimeLoader($container->get('twig.runtime.loader')); + + // Set up the environment's caching service + $environment->setCache($container->get('twig.cache')); + + // Add the Twig extensions + $environment->setExtensions($container->getTagged('twig.extension')); + + // Add a global tracking the debug states + $environment->addGlobal('appDebug', $config->get('debug', false)); + $environment->addGlobal('fwDebug', $debug); + + return $environment; + } + + /** + * Get the `twig.extension.debug` service + * + * @param Container $container The DI container. + * + * @return DebugExtension + */ + public function getTwigExtensionDebugService(Container $container): DebugExtension + { + return new DebugExtension; + } + + /** + * Get the `twig.extension.framework` service + * + * @param Container $container The DI container. + * + * @return FrameworkExtension + */ + public function getTwigExtensionFrameworkService(Container $container): FrameworkExtension + { + return new FrameworkExtension; + } + + /** + * Get the `twig.extension.profiler` service + * + * @param Container $container The DI container. + * + * @return ProfilerExtension + */ + public function getTwigExtensionProfilerService(Container $container): ProfilerExtension + { + return new ProfilerExtension($container->get('twig.profiler.profile')); + } + + /** + * Get the `twig.loader` service + * + * @param Container $container The DI container. + * + * @return \Twig_LoaderInterface + */ + public function getTwigLoaderService(Container $container): \Twig_LoaderInterface + { + return new FilesystemLoader([JPATH_TEMPLATES]); + } + + /** + * Get the `twig.profiler.profile` service + * + * @param Container $container The DI container. + * + * @return Profile + */ + public function getTwigProfilerProfileService(Container $container): Profile + { + return new Profile; + } + + /** + * Get the `twig.runtime.framework` service + * + * @param Container $container The DI container. + * + * @return FrameworkTwigRuntime + */ + public function getTwigRuntimeFrameworkService(Container $container): FrameworkTwigRuntime + { + return new FrameworkTwigRuntime( + $container->get(AbstractApplication::class), + $container->get(PreloadManager::class), + JPATH_ROOT . '/www/media/sri-manifest.json' + ); + } + + /** + * Get the `twig.runtime.loader` service + * + * @param Container $container The DI container. + * + * @return ContainerRuntimeLoader + */ + public function getTwigRuntimeLoaderService(Container $container): ContainerRuntimeLoader + { + return new ContainerRuntimeLoader($container); + } + + /** + * Tag services which are Twig extensions + * + * @param Container $container The DI container. + * + * @return void + */ + private function tagTwigExtensions(Container $container): void + { + /** @var \Joomla\Registry\Registry $config */ + $config = $container->get('config'); + + $debug = $config->get('template.debug', false); + + $twigExtensions = ['twig.extension.framework']; + + if ($debug) + { + $twigExtensions[] = 'twig.extension.debug'; + } + + $container->tag('twig.extension', $twigExtensions); + } +} diff --git a/week-03/project/libraries/src/Service/UserProvider.php b/week-03/project/libraries/src/Service/UserProvider.php new file mode 100644 index 0000000..a05b4ef --- /dev/null +++ b/week-03/project/libraries/src/Service/UserProvider.php @@ -0,0 +1,48 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Service\Provider; + +\defined('JPATH_PLATFORM') or die; + +use Joomla\CMS\User\UserFactory; +use Joomla\CMS\User\UserFactoryInterface; +use Joomla\Database\DatabaseInterface; +use Joomla\DI\Container; +use Joomla\DI\ServiceProviderInterface; + +/** + * Service provider for the user dependency + * + * @since 4.0.0 + */ +class User implements ServiceProviderInterface +{ + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->alias('user.factory', UserFactoryInterface::class) + ->alias(UserFactory::class, UserFactoryInterface::class) + ->share( + UserFactoryInterface::class, + function (Container $container) + { + return new UserFactory($container->get(DatabaseInterface::class)); + }, + true + ); + } +} diff --git a/week-03/project/libraries/src/Session/MetadataManager.php b/week-03/project/libraries/src/Session/MetadataManager.php new file mode 100644 index 0000000..4c7b620 --- /dev/null +++ b/week-03/project/libraries/src/Session/MetadataManager.php @@ -0,0 +1,336 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\Session; + +\defined('JPATH_PLATFORM') or die; + +use Joomla\Application\AbstractApplication; +use Joomla\CMS\Application\CMSApplication; +use Joomla\CMS\User\User; +use Joomla\Database\DatabaseInterface; +use Joomla\Database\Exception\ExecutionFailureException; +use Joomla\Database\ParameterType; +use Joomla\Session\SessionInterface; + +/** + * Manager for optional session metadata. + * + * @since 3.8.6 + * @internal + */ +final class MetadataManager +{ + /** + * Internal variable indicating a session record exists. + * + * @var integer + * @since 4.0.0 + * @note Once PHP 7.1 is the minimum supported version this should become a private constant + */ + private static $sessionRecordExists = 1; + + /** + * Internal variable indicating a session record does not exist. + * + * @var integer + * @since 4.0.0 + * @note Once PHP 7.1 is the minimum supported version this should become a private constant + */ + private static $sessionRecordDoesNotExist = 0; + + /** + * Internal variable indicating an unknown session record statue. + * + * @var integer + * @since 4.0.0 + * @note Once PHP 7.1 is the minimum supported version this should become a private constant + */ + private static $sessionRecordUnknown = -1; + + /** + * Application object. + * + * @var AbstractApplication + * @since 3.8.6 + */ + private $app; + + /** + * Database driver. + * + * @var DatabaseInterface + * @since 3.8.6 + */ + private $db; + + /** + * MetadataManager constructor. + * + * @param AbstractApplication $app Application object. + * @param DatabaseInterface $db Database driver. + * + * @since 3.8.6 + */ + public function __construct(AbstractApplication $app, DatabaseInterface $db) + { + $this->app = $app; + $this->db = $db; + } + + /** + * Create the metadata record if it does not exist. + * + * @param SessionInterface $session The session to create the metadata record for. + * @param User $user The user to associate with the record. + * + * @return void + * + * @since 3.8.6 + * @throws \RuntimeException + */ + public function createRecordIfNonExisting(SessionInterface $session, User $user) + { + $exists = $this->checkSessionRecordExists($session->getId()); + + // Only touch the database if the record does not already exist + if ($exists !== self::$sessionRecordExists) + { + return; + } + + $this->createSessionRecord($session, $user); + } + + /** + * Create the metadata record if it does not exist. + * + * @param SessionInterface $session The session to create or update the metadata record for. + * @param User $user The user to associate with the record. + * + * @return void + * + * @since 4.0.0 + * @throws \RuntimeException + */ + public function createOrUpdateRecord(SessionInterface $session, User $user) + { + $exists = $this->checkSessionRecordExists($session->getId()); + + // Do not try to touch the database if we can't determine the record state + if ($exists === self::$sessionRecordUnknown) + { + return; + } + + if ($exists === self::$sessionRecordDoesNotExist) + { + $this->createSessionRecord($session, $user); + + return; + } + + $this->updateSessionRecord($session, $user); + } + + /** + * Delete records with a timestamp prior to the given time. + * + * @param integer $time The time records should be deleted if expired before. + * + * @return void + * + * @since 3.8.6 + */ + public function deletePriorTo($time) + { + $query = $this->db->getQuery(true) + ->delete($this->db->quoteName('#__session')) + ->where($this->db->quoteName('time') . ' < :time') + ->bind(':time', $time, ParameterType::INTEGER); + + $this->db->setQuery($query); + + try + { + $this->db->execute(); + } + catch (ExecutionFailureException $exception) + { + // Since garbage collection does not result in a fatal error when run in the session API, we don't allow it here either. + } + } + + /** + * Check if the session record exists + * + * @param string $sessionId The session ID to check + * + * @return integer Status value for record presence + * + * @since 4.0.0 + */ + private function checkSessionRecordExists(string $sessionId): int + { + $query = $this->db->getQuery(true) + ->select($this->db->quoteName('session_id')) + ->from($this->db->quoteName('#__session')) + ->where($this->db->quoteName('session_id') . ' = :session_id') + ->bind(':session_id', $sessionId) + ->setLimit(1); + + $this->db->setQuery($query); + + try + { + $exists = $this->db->loadResult(); + } + catch (ExecutionFailureException $e) + { + return self::$sessionRecordUnknown; + } + + if ($exists) + { + return self::$sessionRecordExists; + } + + return self::$sessionRecordDoesNotExist; + } + + /** + * Create the session record + * + * @param SessionInterface $session The session to create the metadata record for. + * @param User $user The user to associate with the record. + * + * @return void + * + * @since 4.0.0 + */ + private function createSessionRecord(SessionInterface $session, User $user) + { + $query = $this->db->getQuery(true); + + $time = $session->isNew() ? time() : $session->get('session.timer.start'); + + $columns = [ + $this->db->quoteName('session_id'), + $this->db->quoteName('guest'), + $this->db->quoteName('time'), + $this->db->quoteName('userid'), + $this->db->quoteName('username'), + ]; + + // Add query placeholders + $values = [ + ':session_id', + ':guest', + ':time', + ':user_id', + ':username', + ]; + + // Bind query values + $sessionId = $session->getId(); + $userIsGuest = $user->guest; + $userId = $user->id; + $username = $user->username === null ? '' : $user->username; + + $query->bind(':session_id', $sessionId) + ->bind(':guest', $userIsGuest, ParameterType::INTEGER) + ->bind(':time', $time) + ->bind(':user_id', $userId, ParameterType::INTEGER) + ->bind(':username', $username); + + if ($this->app instanceof CMSApplication && !$this->app->get('shared_session', false)) + { + $clientId = $this->app->getClientId(); + + $columns[] = $this->db->quoteName('client_id'); + $values[] = ':client_id'; + + $query->bind(':client_id', $clientId, ParameterType::INTEGER); + } + + $query->insert($this->db->quoteName('#__session')) + ->columns($columns) + ->values(implode(', ', $values)); + + $this->db->setQuery($query); + + try + { + $this->db->execute(); + } + catch (ExecutionFailureException $e) + { + // This failure isn't critical, we can go on without the metadata + } + } + + /** + * Update the session record + * + * @param SessionInterface $session The session to update the metadata record for. + * @param User $user The user to associate with the record. + * + * @return void + * + * @since 4.0.0 + */ + private function updateSessionRecord(SessionInterface $session, User $user) + { + $query = $this->db->getQuery(true); + + $time = time(); + + $setValues = [ + $this->db->quoteName('guest') . ' = :guest', + $this->db->quoteName('time') . ' = :time', + $this->db->quoteName('userid') . ' = :user_id', + $this->db->quoteName('username') . ' = :username', + ]; + + // Bind query values + $sessionId = $session->getId(); + $userIsGuest = $user->guest; + $userId = $user->id; + $username = $user->username === null ? '' : $user->username; + + $query->bind(':session_id', $sessionId) + ->bind(':guest', $userIsGuest, ParameterType::INTEGER) + ->bind(':time', $time) + ->bind(':user_id', $userId, ParameterType::INTEGER) + ->bind(':username', $username); + + if ($this->app instanceof CMSApplication && !$this->app->get('shared_session', false)) + { + $clientId = $this->app->getClientId(); + + $setValues[] = $this->db->quoteName('client_id') . ' = :client_id'; + + $query->bind(':client_id', $clientId, ParameterType::INTEGER); + } + + $query->update($this->db->quoteName('#__session')) + ->set($setValues) + ->where($this->db->quoteName('session_id') . ' = :session_id'); + + $this->db->setQuery($query); + + try + { + $this->db->execute(); + } + catch (ExecutionFailureException $e) + { + // This failure isn't critical, we can go on without the metadata + } + } +} diff --git a/week-03/project/libraries/src/String/PunycodeHelper.php b/week-03/project/libraries/src/String/PunycodeHelper.php new file mode 100644 index 0000000..f839afe --- /dev/null +++ b/week-03/project/libraries/src/String/PunycodeHelper.php @@ -0,0 +1,262 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\String; + +\defined('JPATH_PLATFORM') or die; + +use Algo26\IdnaConvert\ToIdn; +use Algo26\IdnaConvert\ToUnicode; +use Joomla\Uri\UriHelper; + +/** + * Joomla Platform String Punycode Class + * + * Class for handling UTF-8 URLs + * Wraps the Punycode library + * All functions assume the validity of utf-8 URLs. + * + * @since 3.1.2 + */ +abstract class PunycodeHelper +{ + /** + * Transforms a UTF-8 string to a Punycode string + * + * @param string $utfString The UTF-8 string to transform + * + * @return string The punycode string + * + * @since 3.1.2 + */ + public static function toPunycode($utfString) + { + return (new ToIdn)->convert($utfString); + } + + /** + * Transforms a Punycode string to a UTF-8 string + * + * @param string $punycodeString The Punycode string to transform + * + * @return string The UF-8 URL + * + * @since 3.1.2 + */ + public static function fromPunycode($punycodeString) + { + return (new ToUnicode)->convert($punycodeString); + } + + /** + * Transforms a UTF-8 URL to a Punycode URL + * + * @param string $uri The UTF-8 URL to transform + * + * @return string The punycode URL + * + * @since 3.1.2 + */ + public static function urlToPunycode($uri) + { + $parsed = UriHelper::parse_url($uri); + + if (!isset($parsed['host']) || $parsed['host'] == '') + { + // If there is no host we do not need to convert it. + return $uri; + } + + $host = $parsed['host']; + $hostExploded = explode('.', $host); + $newhost = ''; + + foreach ($hostExploded as $hostex) + { + $hostex = static::toPunycode($hostex); + $newhost .= $hostex . '.'; + } + + $newhost = substr($newhost, 0, -1); + $newuri = ''; + + if (!empty($parsed['scheme'])) + { + // Assume :// is required although it is not always. + $newuri .= $parsed['scheme'] . '://'; + } + + if (!empty($newhost)) + { + $newuri .= $newhost; + } + + if (!empty($parsed['port'])) + { + $newuri .= ':' . $parsed['port']; + } + + if (!empty($parsed['path'])) + { + $newuri .= $parsed['path']; + } + + if (!empty($parsed['query'])) + { + $newuri .= '?' . $parsed['query']; + } + + if (!empty($parsed['fragment'])) + { + $newuri .= '#' . $parsed['fragment']; + } + + return $newuri; + } + + /** + * Transforms a Punycode URL to a UTF-8 URL + * + * @param string $uri The Punycode URL to transform + * + * @return string The UTF-8 URL + * + * @since 3.1.2 + */ + public static function urlToUTF8($uri) + { + if (empty($uri)) + { + return ''; + } + + $parsed = UriHelper::parse_url($uri); + + if (!isset($parsed['host']) || $parsed['host'] == '') + { + // If there is no host we do not need to convert it. + return $uri; + } + + $host = $parsed['host']; + $hostExploded = explode('.', $host); + $newhost = ''; + + foreach ($hostExploded as $hostex) + { + $hostex = self::fromPunycode($hostex); + $newhost .= $hostex . '.'; + } + + $newhost = substr($newhost, 0, -1); + $newuri = ''; + + if (!empty($parsed['scheme'])) + { + // Assume :// is required although it is not always. + $newuri .= $parsed['scheme'] . '://'; + } + + if (!empty($newhost)) + { + $newuri .= $newhost; + } + + if (!empty($parsed['port'])) + { + $newuri .= ':' . $parsed['port']; + } + + if (!empty($parsed['path'])) + { + $newuri .= $parsed['path']; + } + + if (!empty($parsed['query'])) + { + $newuri .= '?' . $parsed['query']; + } + + if (!empty($parsed['fragment'])) + { + $newuri .= '#' . $parsed['fragment']; + } + + return $newuri; + } + + /** + * Transforms a UTF-8 email to a Punycode email + * This assumes a valid email address + * + * @param string $email The UTF-8 email to transform + * + * @return string The punycode email + * + * @since 3.1.2 + */ + public static function emailToPunycode($email) + { + $explodedAddress = explode('@', $email); + + // Not addressing UTF-8 user names + $newEmail = $explodedAddress[0]; + + if (!empty($explodedAddress[1])) + { + $domainExploded = explode('.', $explodedAddress[1]); + $newdomain = ''; + + foreach ($domainExploded as $domainex) + { + $domainex = static::toPunycode($domainex); + $newdomain .= $domainex . '.'; + } + + $newdomain = substr($newdomain, 0, -1); + $newEmail = $newEmail . '@' . $newdomain; + } + + return $newEmail; + } + + /** + * Transforms a Punycode email to a UTF-8 email + * This assumes a valid email address + * + * @param string $email The punycode email to transform + * + * @return string The punycode email + * + * @since 3.1.2 + */ + public static function emailToUTF8($email) + { + $explodedAddress = explode('@', $email); + + // Not addressing UTF-8 user names + $newEmail = $explodedAddress[0]; + + if (!empty($explodedAddress[1])) + { + $domainExploded = explode('.', $explodedAddress[1]); + $newdomain = ''; + + foreach ($domainExploded as $domainex) + { + $domainex = static::fromPunycode($domainex); + $newdomain .= $domainex . '.'; + } + + $newdomain = substr($newdomain, 0, -1); + $newEmail = $newEmail . '@' . $newdomain; + } + + return $newEmail; + } +} diff --git a/week-03/project/libraries/src/User/User.php b/week-03/project/libraries/src/User/User.php new file mode 100644 index 0000000..85e0e83 --- /dev/null +++ b/week-03/project/libraries/src/User/User.php @@ -0,0 +1,934 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\User; + +\defined('JPATH_PLATFORM') or die; + +use Joomla\CMS\Access\Access; +use Joomla\CMS\Factory; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Log\Log; +use Joomla\CMS\Object\CMSObject; +use Joomla\CMS\Plugin\PluginHelper; +use Joomla\CMS\Table\Table; +use Joomla\Registry\Registry; +use Joomla\Utilities\ArrayHelper; + +/** + * User class. Handles all application interaction with a user + * + * @since 1.7.0 + */ +class User extends CMSObject +{ + /** + * A cached switch for if this user has root access rights. + * + * @var boolean + * @since 1.7.0 + */ + protected $isRoot = null; + + /** + * Unique id + * + * @var integer + * @since 1.7.0 + */ + public $id = null; + + /** + * The user's real name (or nickname) + * + * @var string + * @since 1.7.0 + */ + public $name = null; + + /** + * The login name + * + * @var string + * @since 1.7.0 + */ + public $username = null; + + /** + * The email + * + * @var string + * @since 1.7.0 + */ + public $email = null; + + /** + * MD5 encrypted password + * + * @var string + * @since 1.7.0 + */ + public $password = null; + + /** + * Clear password, only available when a new password is set for a user + * + * @var string + * @since 1.7.0 + */ + public $password_clear = ''; + + /** + * Block status + * + * @var integer + * @since 1.7.0 + */ + public $block = null; + + /** + * Should this user receive system email + * + * @var integer + * @since 1.7.0 + */ + public $sendEmail = null; + + /** + * Date the user was registered + * + * @var string + * @since 1.7.0 + */ + public $registerDate = null; + + /** + * Date of last visit + * + * @var string + * @since 1.7.0 + */ + public $lastvisitDate = null; + + /** + * Activation hash + * + * @var string + * @since 1.7.0 + */ + public $activation = null; + + /** + * User parameters + * + * @var Registry + * @since 1.7.0 + */ + public $params = null; + + /** + * Associative array of user names => group ids + * + * @var array + * @since 1.7.0 + */ + public $groups = array(); + + /** + * Guest status + * + * @var integer + * @since 1.7.0 + */ + public $guest = null; + + /** + * Last Reset Time + * + * @var string + * @since 3.0.1 + */ + public $lastResetTime = null; + + /** + * Count since last Reset Time + * + * @var integer + * @since 3.0.1 + */ + public $resetCount = null; + + /** + * Flag to require the user's password be reset + * + * @var integer + * @since 3.2 + */ + public $requireReset = null; + + /** + * User parameters + * + * @var Registry + * @since 1.7.0 + */ + protected $_params = null; + + /** + * Authorised access groups + * + * @var array + * @since 1.7.0 + */ + protected $_authGroups = null; + + /** + * Authorised access levels + * + * @var array + * @since 1.7.0 + */ + protected $_authLevels = null; + + /** + * Authorised access actions + * + * @var array + * @since 1.7.0 + */ + protected $_authActions = null; + + /** + * Error message + * + * @var string + * @since 1.7.0 + */ + protected $_errorMsg = null; + + /** + * @var array User instances container. + * @since 1.7.3 + */ + protected static $instances = array(); + + /** + * Constructor activating the default information of the language + * + * @param integer $identifier The primary key of the user to load (optional). + * + * @since 1.7.0 + */ + public function __construct($identifier = 0) + { + // Create the user parameters object + $this->_params = new Registry; + + // Load the user if it exists + if (!empty($identifier)) + { + $this->load($identifier); + } + else + { + // Initialise + $this->id = 0; + $this->sendEmail = 0; + $this->aid = 0; + $this->guest = 1; + } + } + + /** + * Returns the global User object, only creating it if it doesn't already exist. + * + * @param integer $identifier The primary key of the user to load (optional). + * + * @return User The User object. + * + * @since 1.7.0 + * @deprecated 5.0 Load the user service from the dependency injection container or via $app->getIdentity() + */ + public static function getInstance($identifier = 0) + { + @trigger_error( + sprintf( + '%1$s() is deprecated. Load the user from the dependency injection container or via %2$s::getApplication()->getIdentity().', + __METHOD__, + __CLASS__ + ), + E_USER_DEPRECATED + ); + + // Find the user id + if (!is_numeric($identifier)) + { + return Factory::getContainer()->get(UserFactoryInterface::class)->loadUserByUsername($identifier); + } + else + { + $id = $identifier; + } + + // If the $id is zero, just return an empty User. + // Note: don't cache this user because it'll have a new ID on save! + if ($id === 0) + { + return Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($id); + } + + // Check if the user ID is already cached. + if (empty(self::$instances[$id])) + { + self::$instances[$id] = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($id); + } + + return self::$instances[$id]; + } + + /** + * Method to get a parameter value + * + * @param string $key Parameter key + * @param mixed $default Parameter default value + * + * @return mixed The value or the default if it did not exist + * + * @since 1.7.0 + */ + public function getParam($key, $default = null) + { + return $this->_params->get($key, $default); + } + + /** + * Method to set a parameter + * + * @param string $key Parameter key + * @param mixed $value Parameter value + * + * @return mixed Set parameter value + * + * @since 1.7.0 + */ + public function setParam($key, $value) + { + return $this->_params->set($key, $value); + } + + /** + * Method to set a default parameter if it does not exist + * + * @param string $key Parameter key + * @param mixed $value Parameter value + * + * @return mixed Set parameter value + * + * @since 1.7.0 + */ + public function defParam($key, $value) + { + return $this->_params->def($key, $value); + } + + /** + * Method to check User object authorisation against an access control + * object and optionally an access extension object + * + * @param string $action The name of the action to check for permission. + * @param string $assetname The name of the asset on which to perform the action. + * + * @return boolean True if authorised + * + * @since 1.7.0 + */ + public function authorise($action, $assetname = null) + { + // Make sure we only check for core.admin once during the run. + if ($this->isRoot === null) + { + $this->isRoot = false; + + // Check for the configuration file failsafe. + $rootUser = Factory::getApplication()->get('root_user'); + + // The root_user variable can be a numeric user ID or a username. + if (is_numeric($rootUser) && $this->id > 0 && $this->id == $rootUser) + { + $this->isRoot = true; + } + elseif ($this->username && $this->username == $rootUser) + { + $this->isRoot = true; + } + elseif ($this->id > 0) + { + // Get all groups against which the user is mapped. + $identities = $this->getAuthorisedGroups(); + array_unshift($identities, $this->id * -1); + + if (Access::getAssetRules(1)->allow('core.admin', $identities)) + { + $this->isRoot = true; + + return true; + } + } + } + + return $this->isRoot ? true : (bool) Access::check($this->id, $action, $assetname); + } + + /** + * Method to return a list of all categories that a user has permission for a given action + * + * @param string $component The component from which to retrieve the categories + * @param string $action The name of the section within the component from which to retrieve the actions. + * + * @return array List of categories that this group can do this action to (empty array if none). Categories must be published. + * + * @since 1.7.0 + */ + public function getAuthorisedCategories($component, $action) + { + // Brute force method: get all published category rows for the component and check each one + // @todo: Modify the way permissions are stored in the db to allow for faster implementation and better scaling + $db = Factory::getDbo(); + + $subQuery = $db->getQuery(true) + ->select($db->quoteName(['id', 'asset_id'])) + ->from($db->quoteName('#__categories')) + ->where( + [ + $db->quoteName('extension') . ' = :component', + $db->quoteName('published') . ' = 1', + ] + ); + + $query = $db->getQuery(true) + ->select($db->quoteName(['c.id', 'a.name'])) + ->from('(' . $subQuery . ') AS ' . $db->quoteName('c')) + ->join('INNER', $db->quoteName('#__assets', 'a'), $db->quoteName('c.asset_id') . ' = ' . $db->quoteName('a.id')) + ->bind(':component', $component); + $db->setQuery($query); + $allCategories = $db->loadObjectList('id'); + $allowedCategories = array(); + + foreach ($allCategories as $category) + { + if ($this->authorise($action, $category->name)) + { + $allowedCategories[] = (int) $category->id; + } + } + + return $allowedCategories; + } + + /** + * Gets an array of the authorised access levels for the user + * + * @return array + * + * @since 1.7.0 + */ + public function getAuthorisedViewLevels() + { + if ($this->_authLevels === null) + { + $this->_authLevels = array(); + } + + if (empty($this->_authLevels)) + { + $this->_authLevels = Access::getAuthorisedViewLevels($this->id); + } + + return $this->_authLevels; + } + + /** + * Gets an array of the authorised user groups + * + * @return array + * + * @since 1.7.0 + */ + public function getAuthorisedGroups() + { + if ($this->_authGroups === null) + { + $this->_authGroups = array(); + } + + if (empty($this->_authGroups)) + { + $this->_authGroups = Access::getGroupsByUser($this->id); + } + + return $this->_authGroups; + } + + /** + * Clears the access rights cache of this user + * + * @return void + * + * @since 3.4.0 + */ + public function clearAccessRights() + { + $this->_authLevels = null; + $this->_authGroups = null; + $this->isRoot = null; + Access::clearStatics(); + } + + /** + * Pass through method to the table for setting the last visit date + * + * @param integer $timestamp The timestamp, defaults to 'now'. + * + * @return boolean True on success. + * + * @since 1.7.0 + */ + public function setLastVisit($timestamp = null) + { + // Create the user table object + /** @var \Joomla\CMS\Table\User $table */ + $table = $this->getTable(); + $table->load($this->id); + + return $table->setLastVisit($timestamp); + } + + /** + * Method to get the user timezone. + * + * If the user didn't set a timezone, it will return the server timezone + * + * @return \DateTimeZone + * + * @since 3.7.0 + */ + public function getTimezone() + { + $timezone = $this->getParam('timezone', Factory::getApplication()->get('offset', 'GMT')); + + return new \DateTimeZone($timezone); + } + + /** + * Method to get the user parameters + * + * @param object $params The user parameters object + * + * @return void + * + * @since 1.7.0 + */ + public function setParameters($params) + { + $this->_params = $params; + } + + /** + * Method to get the user table object + * + * This function uses a static variable to store the table name of the user table to + * instantiate. You can call this function statically to set the table name if + * needed. + * + * @param string $type The user table name to be used + * @param string $prefix The user table prefix to be used + * + * @return Table The user table object + * + * @note At 4.0 this method will no longer be static + * @since 1.7.0 + */ + public static function getTable($type = null, $prefix = 'JTable') + { + static $tabletype; + + // Set the default tabletype; + if (!isset($tabletype)) + { + $tabletype['name'] = 'user'; + $tabletype['prefix'] = 'JTable'; + } + + // Set a custom table type is defined + if (isset($type)) + { + $tabletype['name'] = $type; + $tabletype['prefix'] = $prefix; + } + + // Create the user table object + return Table::getInstance($tabletype['name'], $tabletype['prefix']); + } + + /** + * Method to bind an associative array of data to a user object + * + * @param array &$array The associative array to bind to the object + * + * @return boolean True on success + * + * @since 1.7.0 + */ + public function bind(&$array) + { + // Let's check to see if the user is new or not + if (empty($this->id)) + { + // Check the password and create the crypted password + if (empty($array['password'])) + { + $array['password'] = UserHelper::genRandomPassword(32); + $array['password2'] = $array['password']; + } + + // Not all controllers check the password, although they should. + // Hence this code is required: + if (isset($array['password2']) && $array['password'] != $array['password2']) + { + Factory::getApplication()->enqueueMessage(Text::_('JLIB_USER_ERROR_PASSWORD_NOT_MATCH'), 'error'); + + return false; + } + + $this->password_clear = ArrayHelper::getValue($array, 'password', '', 'string'); + + $array['password'] = UserHelper::hashPassword($array['password']); + + // Set the registration timestamp + $this->set('registerDate', Factory::getDate()->toSql()); + } + else + { + // Updating an existing user + if (!empty($array['password'])) + { + if ($array['password'] != $array['password2']) + { + $this->setError(Text::_('JLIB_USER_ERROR_PASSWORD_NOT_MATCH')); + + return false; + } + + $this->password_clear = ArrayHelper::getValue($array, 'password', '', 'string'); + + // Check if the user is reusing the current password if required to reset their password + if ($this->requireReset == 1 && UserHelper::verifyPassword($this->password_clear, $this->password)) + { + $this->setError(Text::_('JLIB_USER_ERROR_CANNOT_REUSE_PASSWORD')); + + return false; + } + + $array['password'] = UserHelper::hashPassword($array['password']); + + // Reset the change password flag + $array['requireReset'] = 0; + } + else + { + $array['password'] = $this->password; + } + + // Prevent updating internal fields + unset($array['registerDate']); + unset($array['lastvisitDate']); + unset($array['lastResetTime']); + unset($array['resetCount']); + } + + if (\array_key_exists('params', $array)) + { + $this->_params->loadArray($array['params']); + + if (\is_array($array['params'])) + { + $params = (string) $this->_params; + } + else + { + $params = $array['params']; + } + + $this->params = $params; + } + + // Bind the array + if (!$this->setProperties($array)) + { + $this->setError(Text::_('JLIB_USER_ERROR_BIND_ARRAY')); + + return false; + } + + // Make sure its an integer + $this->id = (int) $this->id; + + return true; + } + + /** + * Method to save the User object to the database + * + * @param boolean $updateOnly Save the object only if not a new user + * Currently only used in the user reset password method. + * + * @return boolean True on success + * + * @since 1.7.0 + * @throws \RuntimeException + */ + public function save($updateOnly = false) + { + // Create the user table object + $table = $this->getTable(); + $this->params = (string) $this->_params; + $table->bind($this->getProperties()); + + // Allow an exception to be thrown. + try + { + // Check and store the object. + if (!$table->check()) + { + $this->setError($table->getError()); + + return false; + } + + // If user is made a Super Admin group and user is NOT a Super Admin + + // @todo ACL - this needs to be acl checked + + $my = Factory::getUser(); + + // Are we creating a new user + $isNew = empty($this->id); + + // If we aren't allowed to create new users return + if ($isNew && $updateOnly) + { + return true; + } + + // Get the old user + $oldUser = new User($this->id); + + // Access Checks + + // The only mandatory check is that only Super Admins can operate on other Super Admin accounts. + // To add additional business rules, use a user plugin and throw an Exception with onUserBeforeSave. + + // Check if I am a Super Admin + $iAmSuperAdmin = $my->authorise('core.admin'); + + $iAmRehashingSuperadmin = false; + + if (($my->id == 0 && !$isNew) && $this->id == $oldUser->id && $oldUser->authorise('core.admin') && $oldUser->password != $this->password) + { + $iAmRehashingSuperadmin = true; + } + + // Check if we are using a CLI application + $isCli = false; + + if (Factory::getApplication()->isCli()) + { + $isCli = true; + } + + // We are only worried about edits to this account if I am not a Super Admin. + if ($iAmSuperAdmin != true && $iAmRehashingSuperadmin != true && $isCli != true) + { + // I am not a Super Admin, and this one is, so fail. + if (!$isNew && Access::check($this->id, 'core.admin')) + { + throw new \RuntimeException('User not Super Administrator'); + } + + if ($this->groups != null) + { + // I am not a Super Admin and I'm trying to make one. + foreach ($this->groups as $groupId) + { + if (Access::checkGroup($groupId, 'core.admin')) + { + throw new \RuntimeException('User not Super Administrator'); + } + } + } + } + + // Fire the onUserBeforeSave event. + PluginHelper::importPlugin('user'); + + $result = Factory::getApplication()->triggerEvent('onUserBeforeSave', array($oldUser->getProperties(), $isNew, $this->getProperties())); + + if (\in_array(false, $result, true)) + { + // Plugin will have to raise its own error or throw an exception. + return false; + } + + // Store the user data in the database + $result = $table->store(); + + // Set the id for the User object in case we created a new user. + if (empty($this->id)) + { + $this->id = $table->get('id'); + } + + if ($my->id == $table->id) + { + $registry = new Registry($table->params); + $my->setParameters($registry); + } + + // Fire the onUserAfterSave event + Factory::getApplication()->triggerEvent('onUserAfterSave', array($this->getProperties(), $isNew, $result, $this->getError())); + } + catch (\Exception $e) + { + $this->setError($e->getMessage()); + + return false; + } + + return $result; + } + + /** + * Method to delete the User object from the database + * + * @return boolean True on success + * + * @since 1.7.0 + */ + public function delete() + { + PluginHelper::importPlugin('user'); + + // Trigger the onUserBeforeDelete event + Factory::getApplication()->triggerEvent('onUserBeforeDelete', array($this->getProperties())); + + // Create the user table object + $table = $this->getTable(); + + if (!$result = $table->delete($this->id)) + { + $this->setError($table->getError()); + } + + // Trigger the onUserAfterDelete event + Factory::getApplication()->triggerEvent('onUserAfterDelete', array($this->getProperties(), $result, $this->getError())); + + return $result; + } + + /** + * Method to load a User object by user id number + * + * @param mixed $id The user id of the user to load + * + * @return boolean True on success + * + * @since 1.7.0 + */ + public function load($id) + { + // Create the user table object + $table = $this->getTable(); + + // Load the UserModel object based on the user id or throw a warning. + if (!$table->load($id)) + { + // Reset to guest user + $this->guest = 1; + + Log::add(Text::sprintf('JLIB_USER_ERROR_UNABLE_TO_LOAD_USER', $id), Log::WARNING, 'jerror'); + + return false; + } + + /* + * Set the user parameters using the default XML file. We might want to + * extend this in the future to allow for the ability to have custom + * user parameters, but for right now we'll leave it how it is. + */ + + if ($table->params) + { + $this->_params->loadString($table->params); + } + + // Assuming all is well at this point let's bind the data + $this->setProperties($table->getProperties()); + + // The user is no longer a guest + if ($this->id != 0) + { + $this->guest = 0; + } + else + { + $this->guest = 1; + } + + return true; + } + + /** + * Method to allow serialize the object with minimal properties. + * + * @return array The names of the properties to include in serialization. + * + * @since 3.6.0 + */ + public function __sleep() + { + return array('id'); + } + + /** + * Method to recover the full object on unserialize. + * + * @return void + * + * @since 3.6.0 + */ + public function __wakeup() + { + // Initialise some variables + $this->_params = new Registry; + + // Load the user if it exists + if (!empty($this->id) && $this->load($this->id)) + { + // Push user into cached instances. + self::$instances[$this->id] = $this; + } + else + { + // Initialise + $this->id = 0; + $this->sendEmail = 0; + $this->aid = 0; + $this->guest = 1; + } + } +} diff --git a/week-03/project/libraries/src/User/UserFactory.php b/week-03/project/libraries/src/User/UserFactory.php new file mode 100644 index 0000000..fd5cb27 --- /dev/null +++ b/week-03/project/libraries/src/User/UserFactory.php @@ -0,0 +1,75 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\User; + +\defined('_JEXEC') or die; + +use Joomla\Database\DatabaseInterface; + +/** + * Default factory for creating User objects + * + * @since 4.0.0 + */ +class UserFactory implements UserFactoryInterface +{ + /** + * The database. + * + * @var DatabaseInterface + */ + private $db; + + /** + * UserFactory constructor. + * + * @param DatabaseInterface $db The database + */ + public function __construct(DatabaseInterface $db) + { + $this->db = $db; + } + + /** + * Method to get an instance of a user for the given id. + * + * @param int $id The id + * + * @return User + * + * @since 4.0.0 + */ + public function loadUserById(int $id): User + { + return new User($id); + } + + /** + * Method to get an instance of a user for the given username. + * + * @param string $username The username + * + * @return User + * + * @since 4.0.0 + */ + public function loadUserByUsername(string $username): User + { + // Initialise some variables + $query = $this->db->getQuery(true) + ->select($this->db->quoteName('id')) + ->from($this->db->quoteName('#__users')) + ->where($this->db->quoteName('username') . ' = :username') + ->bind(':username', $username) + ->setLimit(1); + $this->db->setQuery($query); + + return $this->loadUserById((int) $this->db->loadResult()); + } +} diff --git a/week-03/project/libraries/src/User/UserFactoryInterface.php b/week-03/project/libraries/src/User/UserFactoryInterface.php new file mode 100644 index 0000000..a9a1816 --- /dev/null +++ b/week-03/project/libraries/src/User/UserFactoryInterface.php @@ -0,0 +1,41 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\CMS\User; + +\defined('_JEXEC') or die; + +/** + * Interface defining a factory which can create User objects + * + * @since 4.0.0 + */ +interface UserFactoryInterface +{ + /** + * Method to get an instance of a user for the given id. + * + * @param int $id The id + * + * @return User + * + * @since 4.0.0 + */ + public function loadUserById(int $id): User; + + /** + * Method to get an instance of a user for the given username. + * + * @param string $username The username + * + * @return User + * + * @since 4.0.0 + */ + public function loadUserByUsername(string $username): User; +} diff --git a/week-03/project/libraries/src/View/Admin/DashboardHtmlView.php b/week-03/project/libraries/src/View/Admin/DashboardHtmlView.php new file mode 100644 index 0000000..a04088a --- /dev/null +++ b/week-03/project/libraries/src/View/Admin/DashboardHtmlView.php @@ -0,0 +1,97 @@ +helper = $helper; + $this->packageModel = $packageModel; + $this->releaseModel = $releaseModel; + } + + /** + * Method to render the view + * + * @return string The rendered view + */ + public function render() + { + $package = $this->packageModel->getPackage($this->package); + + $this->setData( + [ + 'releases' => $this->releaseModel->getPackageHistory($package), + 'package' => $package, + ] + ); + + return parent::render(); + } + + /** + * Set the active package + * + * @param string $package The active package name + * + * @return void + */ + public function setPackage(string $package): void + { + $this->package = $package; + } +} diff --git a/week-03/project/libraries/src/View/Page/PageHtmlView.php b/week-03/project/libraries/src/View/Page/PageHtmlView.php new file mode 100644 index 0000000..a04088a --- /dev/null +++ b/week-03/project/libraries/src/View/Page/PageHtmlView.php @@ -0,0 +1,97 @@ +helper = $helper; + $this->packageModel = $packageModel; + $this->releaseModel = $releaseModel; + } + + /** + * Method to render the view + * + * @return string The rendered view + */ + public function render() + { + $package = $this->packageModel->getPackage($this->package); + + $this->setData( + [ + 'releases' => $this->releaseModel->getPackageHistory($package), + 'package' => $package, + ] + ); + + return parent::render(); + } + + /** + * Set the active package + * + * @param string $package The active package name + * + * @return void + */ + public function setPackage(string $package): void + { + $this->package = $package; + } +} diff --git a/week-03/project/libraries/web.config b/week-03/project/libraries/web.config new file mode 100644 index 0000000..f7a77db --- /dev/null +++ b/week-03/project/libraries/web.config @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/week-03/project/media/css/index.html b/week-03/project/media/css/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/week-03/project/media/css/index.html @@ -0,0 +1 @@ + diff --git a/week-03/project/media/css/template.css b/week-03/project/media/css/template.css new file mode 100644 index 0000000..e69de29 diff --git a/week-03/project/media/index.html b/week-03/project/media/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/week-03/project/media/index.html @@ -0,0 +1 @@ + diff --git a/week-03/project/media/js/index.html b/week-03/project/media/js/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/week-03/project/media/js/index.html @@ -0,0 +1 @@ + diff --git a/week-03/project/media/js/template.js b/week-03/project/media/js/template.js new file mode 100644 index 0000000..e69de29 diff --git a/week-03/project/media/mix-manifest.json b/week-03/project/media/mix-manifest.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/week-03/project/media/mix-manifest.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/week-03/project/media/sri-manifest.json b/week-03/project/media/sri-manifest.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/week-03/project/media/sri-manifest.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/week-03/project/robots.txt.dist b/week-03/project/robots.txt.dist new file mode 100644 index 0000000..97b1244 --- /dev/null +++ b/week-03/project/robots.txt.dist @@ -0,0 +1,28 @@ +# If the Joomla site is installed within a folder +# eg www.example.com/joomla/ then the robots.txt file +# MUST be moved to the site root +# eg www.example.com/robots.txt +# AND the joomla folder name MUST be prefixed to all of the +# paths. +# eg the Disallow rule for the /administrator/ folder MUST +# be changed to read +# Disallow: /joomla/administrator/ +# +# For more information about the robots.txt standard, see: +# https://www.robotstxt.org/orig.html + +User-agent: * +Disallow: /administrator/ +Disallow: /bin/ +Disallow: /cache/ +Disallow: /cli/ +Disallow: /components/ +Disallow: /includes/ +Disallow: /installation/ +Disallow: /language/ +Disallow: /layouts/ +Disallow: /libraries/ +Disallow: /logs/ +Disallow: /modules/ +Disallow: /plugins/ +Disallow: /tmp/ diff --git a/week-03/project/sql/index.html b/week-03/project/sql/index.html new file mode 100644 index 0000000..2efb97f --- /dev/null +++ b/week-03/project/sql/index.html @@ -0,0 +1 @@ + diff --git a/week-03/project/sql/install.sql b/week-03/project/sql/install.sql new file mode 100644 index 0000000..9eaa39a --- /dev/null +++ b/week-03/project/sql/install.sql @@ -0,0 +1,141 @@ +-- +-- DATABASE STRUCTURE FOR GENERIC CMS (Adapted from Joomla!) +-- -- subject to change -- +-- + +SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; +SET time_zone = "+00:00"; + +-- +-- Table structure for table `llewellyn_menu` +-- + +CREATE TABLE IF NOT EXISTS `llewellyn_menu` ( + `id` int NOT NULL AUTO_INCREMENT, + `title` varchar(255) NOT NULL COMMENT 'The display title of the menu item.', + `alias` varchar(400) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT 'The SEF alias of the menu item.', + `path` varchar(1024) NOT NULL COMMENT 'The computed path of the menu item based on the alias field.', + `published` tinyint NOT NULL DEFAULT 0 COMMENT 'The published state of the menu link.', + `parent_id` int unsigned NOT NULL DEFAULT 1 COMMENT 'The parent menu item in the menu tree.', + `level` int unsigned NOT NULL DEFAULT 0 COMMENT 'The relative level in the tree.', + `item_id` int unsigned NOT NULL DEFAULT 0 COMMENT 'FK to llewellyn_item.id', + `checked_out` int unsigned COMMENT 'FK to llewellyn_users.id', + `checked_out_time` datetime COMMENT 'The time the menu item was checked out.', + `params` text NOT NULL COMMENT 'JSON encoded data for the menu item.', + `lft` int NOT NULL DEFAULT 0 COMMENT 'Nested set lft.', + `rgt` int NOT NULL DEFAULT 0 COMMENT 'Nested set rgt.', + `home` tinyint unsigned NOT NULL DEFAULT 0 COMMENT 'Indicates if this menu item is the home or default page.', + `publish_up` datetime, + `publish_down` datetime, + PRIMARY KEY (`id`), + KEY `idx_item` (`item_id`), + KEY `idx_left_right` (`lft`,`rgt`), + KEY `idx_alias` (`alias`(100)), + KEY `idx_path` (`path`(100)) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE=utf8mb4_unicode_ci AUTO_INCREMENT=102; + +-- +-- Table structure for table `llewellyn_session` +-- + +CREATE TABLE IF NOT EXISTS `llewellyn_session` ( + `session_id` varbinary(192) NOT NULL, + `guest` tinyint unsigned DEFAULT 1, + `time` int NOT NULL DEFAULT 0, + `data` mediumtext, + `userid` int DEFAULT 0, + `username` varchar(150) DEFAULT '', + PRIMARY KEY (`session_id`), + KEY `userid` (`userid`), + KEY `time` (`time`), + KEY `guest` (`guest`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `llewellyn_users` +-- + +CREATE TABLE IF NOT EXISTS `llewellyn_users` ( + `id` int NOT NULL AUTO_INCREMENT, + `name` varchar(400) NOT NULL DEFAULT '', + `username` varchar(150) NOT NULL DEFAULT '', + `email` varchar(100) NOT NULL DEFAULT '', + `password` varchar(100) NOT NULL DEFAULT '', + `block` tinyint NOT NULL DEFAULT 0, + `sendEmail` tinyint DEFAULT 0, + `registerDate` datetime NOT NULL, + `lastvisitDate` datetime, + `activation` varchar(100) NOT NULL DEFAULT '', + `params` text NOT NULL, + `lastResetTime` datetime COMMENT 'Date of last password reset', + `resetCount` int NOT NULL DEFAULT 0 COMMENT 'Count of password resets since lastResetTime', + `requireReset` tinyint NOT NULL DEFAULT 0 COMMENT 'Require user to reset password on next login', + PRIMARY KEY (`id`), + KEY `idx_name` (`name`(100)), + KEY `idx_block` (`block`), + UNIQUE KEY `idx_username` (`username`), + KEY `email` (`email`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `llewellyn_usergroups` +-- + +CREATE TABLE IF NOT EXISTS `llewellyn_usergroups` ( + `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT 'Primary Key', + `parent_id` int unsigned NOT NULL DEFAULT 0 COMMENT 'Adjacency List Reference Id', + `lft` int NOT NULL DEFAULT 0 COMMENT 'Nested set lft.', + `rgt` int NOT NULL DEFAULT 0 COMMENT 'Nested set rgt.', + `title` varchar(100) NOT NULL DEFAULT '', + PRIMARY KEY (`id`), + UNIQUE KEY `idx_usergroup_parent_title_lookup` (`parent_id`,`title`), + KEY `idx_usergroup_title_lookup` (`title`), + KEY `idx_usergroup_adjacency_lookup` (`parent_id`), + KEY `idx_usergroup_nested_set_lookup` (`lft`,`rgt`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `llewellyn_user_usergroup_map` +-- + +CREATE TABLE IF NOT EXISTS `llewellyn_user_usergroup_map` ( + `user_id` int unsigned NOT NULL DEFAULT 0 COMMENT 'Foreign Key to llewellyn_users.id', + `group_id` int unsigned NOT NULL DEFAULT 0 COMMENT 'Foreign Key to llewellyn_usergroups.id', + PRIMARY KEY (`user_id`,`group_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `llewellyn_item` +-- + +CREATE TABLE IF NOT EXISTS `llewellyn_item` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `title` varchar(255) NOT NULL DEFAULT '', + `alias` varchar(400) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '', + `introtext` mediumtext NOT NULL, + `fulltext` mediumtext NOT NULL, + `state` tinyint NOT NULL DEFAULT 0, + `created` datetime NOT NULL, + `created_by` int unsigned NOT NULL DEFAULT 0, + `created_by_alias` varchar(255) NOT NULL DEFAULT '', + `modified` datetime NOT NULL, + `modified_by` int unsigned NOT NULL DEFAULT 0, + `checked_out` int unsigned, + `checked_out_time` datetime NULL DEFAULT NULL, + `publish_up` datetime NULL DEFAULT NULL, + `publish_down` datetime NULL DEFAULT NULL, + `version` int unsigned NOT NULL DEFAULT 1, + `ordering` int NOT NULL DEFAULT 0, + `metakey` text, + `metadesc` text NOT NULL, + `hits` int unsigned NOT NULL DEFAULT 0, + `metadata` text NOT NULL, + `featured` tinyint unsigned NOT NULL DEFAULT 0 COMMENT 'Set if article is featured.', + PRIMARY KEY (`id`), + KEY `idx_checkout` (`checked_out`), + KEY `idx_state` (`state`), + KEY `idx_createdby` (`created_by`), + KEY `idx_alias` (`alias`(191)) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE=utf8mb4_unicode_ci; + + diff --git a/week-03/project/web.config.txt b/week-03/project/web.config.txt new file mode 100644 index 0000000..e15eb64 --- /dev/null +++ b/week-03/project/web.config.txt @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +