diff --git a/.gitattributes b/.gitattributes index dfe0770..2cbb4fa 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,10 @@ -# Auto detect text files and perform LF normalization * text=auto + +/.github export-ignore +/bin export-ignore +/tests export-ignore +/.gitignore export-ignore +/.gitattributes export-ignore +/phpunit.xml export-ignore +/phpcs.xml export-ignore +/README.md export-ignore \ No newline at end of file diff --git a/.gitignore b/.gitignore index b67bb49..2a47750 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ Homestead.json .DS_Store composer.lock phpunit-report.xml +phpcs-report.xml diff --git a/LICENSE b/LICENSE deleted file mode 100644 index e62ec04..0000000 --- a/LICENSE +++ /dev/null @@ -1,674 +0,0 @@ -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/README.md b/README.md index 744cfb2..46d7b46 100644 --- a/README.md +++ b/README.md @@ -12,22 +12,13 @@ The `Request Validator` provides a robust and extensible system for validating R ### 📦 Installation -If using standalone: +This package is included by default within the **WpMVC** framework. + +However, if you want to use this validation engine independently inside your own custom WordPress plugins, you may install it as a standalone package via Composer: ```bash -composer require wpmvc/validator +composer require wpmvc/request-validator ``` - ---- - -### 🧱 Structure Overview - -| File | Purpose | -| ----------- | ---------------------------------------------- | -| `Validator` | Main validation handler class | -| `Mime` | Utility for validating uploaded file types | -| `DateTime` | Trait for handling date-based validation rules | - --- ### 🚀 Basic Usage @@ -35,8 +26,10 @@ composer require wpmvc/validator Inside a controller method: ```php -public function store( Validator $validator, WP_REST_Request $request ) { - $validator->validate([ +use WpMVC\RequestValidator\Request; + +public function store( Request $request ) { + $request->validate([ 'title' => 'required|string|min:3|max:255', 'email' => 'required|email', 'price' => 'numeric|min:0', @@ -53,40 +46,136 @@ public function store( Validator $validator, WP_REST_Request $request ) { --- +### 🏗 Form Requests + +For more complex validation scenarios, you may wish to create a "form request" class. Form requests are custom request classes that encapsulate their own validation and authorization logic. + +To create one, extend the `FormRequest` class: + +```php +namespace MyPlugin\App\Http\Requests; + +use WpMVC\RequestValidator\FormRequest; + +class StorePostRequest extends FormRequest { + /** + * Determine if the user is authorized to make this request. + */ + public function authorize(): bool { + return true; + } + + /** + * Get the validation rules that apply to the request. + */ + public function rules(): array { + return [ + 'title' => 'required|string|max:255', + 'content' => 'required', + 'status' => 'required|in:publish,draft,pending' + ]; + } + /** + * Get custom messages for validator errors. + */ + public function messages(): array { + return [ + 'title.required' => 'A title is absolutely required for your post.', + ]; + } + + /** + * Get custom attributes for validator errors. + */ + public function attributes(): array { + return [ + 'status' => 'publication status', + ]; + } + + /** + * Configure the validator instance. + */ + public function with_validator( $validator ): void { + $validator->after(function ($validator) { + if ( $this->something_else_is_invalid() ) { + $validator->errors['field_name'][] = 'Something went wrong!'; + } + }); + } +} +``` + +Once defined, you can seamlessly type-hint the class on your controller method. The incoming request is validated and authorized before the controller method is even called: + +```php +public function store( StorePostRequest $request ) { + // The incoming request is valid and authorized... +} +``` + +--- + ### 🛠 Available Rules -| Rule | Description | -| ----------------- | ---------------------------------------------------- | -| `required` | Field must be present and non-empty | -| `string` | Must be a string | -| `email` | Validates email format | -| `numeric` | Must be a numeric value | -| `integer` | Must be an integer | -| `boolean` | Must be `true` or `false` | -| `array` | Must be an array | -| `uuid` | Must be a valid UUID | -| `url` | Must be a valid URL | -| `mac_address` | Must be a valid MAC address | -| `json` | Must be a valid JSON string | -| `confirmed` | `field_confirmation` must match field | -| `accepted` | Value must be in allowed list | -| `file` | Checks file upload validity | -| `mimes` | Allowed file extensions (e.g. `jpg,png`) | -| `max` | Max length/size/value (string, numeric, file, array) | -| `min` | Min length/size/value (string, numeric, file, array) | -| `date:format` | Must be a date in given format | -| `date_equals` | Must exactly match a date | -| `before` | Must be before given date | -| `after` | Must be after given date | -| `before_or_equal` | Must be before or equal to given date | -| `after_or_equal` | Must be after or equal to given date | +| Rule | Description | +| ----------------------------- | ------------------------------------------------------------------------ | +| `accepted` | Must be `yes`, `on`, `1`, `true`, `1`, or `true`. | +| `after:date` | Must be after the given date (supports relative dates like `today`). | +| `after_or_equal:date` | Must be after or equal to the given date. | +| `alpha` | Must be entirely alphabetic characters. | +| `alpha_dash` | Must have alpha-numeric characters, dashes, and underscores. | +| `alpha_num` | Must be entirely alpha-numeric characters. | +| `array` | Must be an array. | +| `bail` | Stop running validation rules after the first failure. | +| `before:date` | Must be before the given date. | +| `before_or_equal:date` | Must be before or equal to the given date. | +| `between:min,max` | Must have a size between the given *min* and *max*. | +| `boolean` | Must be a boolean value. | +| `confirmed` | `field_confirmation` must match the field value. | +| `date:format` | Must be a date in the given format (default: `Y-m-d`). | +| `date_equals:date` | Must exactly match the given date. | +| `different:field` | Must have a different value than the specified *field*. | +| `digits:value` | Must be numeric and have an exact length of *value*. | +| `digits_between:min,max` | Must be numeric and have a length between *min* and *max*. | +| `email` | Must be a valid email address. | +| `ends_with:foo,bar,...` | Must end with one of the given values. | +| `file` | Must be a valid uploaded file. | +| `image` | Must be an image (jpeg, png, bmp, gif, svg, webp). | +| `in:foo,bar,...` | Must be included in the given list of values. | +| `integer` | Must be an integer. | +| `ip` | Must be a valid IP address. | +| `ipv4` | Must be a valid IPv4 address. | +| `ipv6` | Must be a valid IPv6 address. | +| `json` | Must be a valid JSON string. | +| `mac_address` | Must be a valid MAC address. | +| `max:value` | Max length/size/value (string, numeric, file, array). | +| `mimes:jpg,png,...` | Allowed file extensions based on MIME types. | +| `mimetypes:text/plain,...` | Must match one of the given MIME types. | +| `min:value` | Min length/size/value (string, numeric, file, array). | +| `not_in:foo,bar,...` | Must not be included in the given list of values. | +| `not_regex:pattern` | Must not match the given regular expression. | +| `nullable` | Field may be null or empty. | +| `numeric` | Must be a numeric value. | +| `prohibited_unless:field,val` | Prohibited unless *field* is equal to any of the values. | +| `regex:pattern` | Must match the given regular expression. | +| `required` | Field must be present and non-empty. | +| `required_if:field,val` | Required if the *field* is equal to the given *value*. | +| `same:field` | Must match the value of the given *field*. | +| `size:value` | Must have a matching size (string length, array count, file KB). | +| `sometimes` | Run rules only if the field is present in the request. | +| `starts_with:foo,bar,...` | Must start with one of the given values. | +| `string` | Must be a string. | +| `timezone` | Must be a valid timezone identifier. | +| `url` | Must be a valid URL. | +| `uuid` | Must be a valid UUID. | --- ### 📁 File Validation Example ```php -$validator->validate([ +$request->validate([ 'photo' => 'required|file|mimes:png,jpg|max:1024', ]); ``` @@ -100,7 +189,7 @@ $validator->validate([ ### 📅 Date Validation Example ```php -$validator->validate([ +$request->validate([ 'launched' => 'required|date:Y-m-d|after_or_equal:2022-01-01', ]); ``` @@ -109,14 +198,116 @@ Supports custom formats and comparison logic using native PHP `DateTime`. --- -### 📋 Error Handling +### 🧩 Array Validation Example + +The validator supports wildcard dot-notation (`.*`) to validate elements within an array where the exact indexes are unknown. + +For instance, you can validate each email in an array of participants: + +```php +// Request Payload: { "participants": [ { "email": "a@x.com" }, { "email": "b@y.com" } ] } + +$request->validate([ + 'participants' => 'required|array', + 'participants.*.email' => 'required|email' // Evaluates participants.0.email, participants.1.email, etc. +]); +``` + +--- + +### 💬 Custom Error Messages & Rules + +You can customize the error messages by passing a third array to the `$request->make($request, $rules, $messages, $attributes)` method, or by overriding the `messages()` and `attributes()` methods in your `FormRequest` class. + +#### Placeholders +The following placeholders can be used within your custom error messages: +- `:attribute`: The name of the field being validated. +- `:min`, `:max`, `:size`: Respective numeric/size values. +- `:other`: Name of the field being compared against. +- `:value`, `:values`: The value(s) required by the rule. +- `:format`: The expected date format. + +Example: +```php +$messages = [ + 'email.required' => 'We really need your :attribute!', + 'age.min' => 'You must be at least :min years old.', +]; +``` + +You can also pass inline Closures directly into the rules array for fast custom logic: + +```php +$request->validate([ + 'title' => [ + 'required', + function ($attribute, $value, $fail) { + if ($value === 'reserved_word') { + $fail("The {$attribute} contains an invalid word."); + } + }, + ] +]); +``` + +#### Custom Rule Objects + +For complex validation, you can create a dedicated rule class by extending the `WpMVC\RequestValidator\Rules\Rule` base class: + +```php +use WpMVC\RequestValidator\Rules\Rule; + +class UppercaseRule extends Rule { + public static function get_name(): string { + return 'uppercase'; + } + + public function passes(string $attribute, $value): bool { + return strtoupper($value) === $value; + } + + protected function default_message(): string { + /* translators: 1: attribute name */ + return sprintf( __( 'The %1$s must be completely uppercase.', 'wpmvc' ), ':attribute' ); + } +} + +// In your controller: +$request->validate([ + 'title' => ['required', new UppercaseRule()] +]); +``` + +--- + +### �🧱 Fluent Rule Builder + +Instead of using pipe-separated strings, you may fluently construct validation rules using the `Rule` class. This is particularly useful for complex rules or when you need to avoid string concatenation errors (such as using an `in` rule with an array of predefined data). + +```php +use WpMVC\RequestValidator\Rule; + +$request->validate([ + 'email' => [Rule::required(), Rule::email(), Rule::max(255)], + 'status' => [Rule::required(), Rule::in(['active', 'pending', 'banned'])], + 'type' => [Rule::required_if('is_admin', true)], +]); +``` + +--- + +### 📋 Validation Error Response Format + +If validation fails, the engine throws a `Exception` that the WpMVC framework catches and automatically converts into a `422 Unprocessable Entity` JSON response. -If `throw_errors` is `true` (default), the validator throws an exception and returns a 422 JSON response: +The JSON response format is structured as follows, where `errors` contains arrays of error messages keyed by the failing field name: ```json { - "message": "", - "errors": { + "data": { + "status_code": 422 + }, + "messages": { "email": ["The email field must be a valid email address."], "price": ["The price must be at least 0."] } @@ -130,16 +321,61 @@ If `throw_errors` is `true` (default), the validator throws an exception and ret You can also use: ```php -$validator->validate($rules, false); -if ( $validator->is_fail() ) { - return Response::send(['errors' => $validator->errors], 422); +$request->validate($rules, false); +if ( $request->fails() ) { + wp_send_json_error(['errors' => $request->errors], 422); } ``` --- -### 🔧 Internal Utilities +### 🪝 Validation Hooks (After) -* `Mime` class for file type validation using `mime_content_type`. -* `DateTime` trait for parsing and comparing dates based on format. -* `get_format()`, `is_it_valid_date()`, `get_timestamp()` handle date logic. +The `Validation` engine allows you to attach callbacks to be run **after** validation completes. This allows you to easily perform further validation steps across multiple fields and assign custom error messages before failing. + +```php +$validation = $request->make($request, $rules)->after(function ($validation) { + if ( $this->something_else_is_invalid() ) { + $validation->errors['field_name'][] = 'Something went wrong!'; + } +}); + +if ( $validation->fails() ) { + // ... +} +``` + +--- + +### 🌍 Usage Outside of a Controller + +If you want to use the validator in a different context (like a cron job, WP-CLI command, or a regular WordPress hook action), you can manually build a `WP_REST_Request` and pass it to either `Request` or `Validation`. + +#### Using `Validation` directly: +```php +use WpMVC\RequestValidator\Validation; + +$request = new \WP_REST_Request(); +$request->set_param('email', 'test@example.com'); + +$validation = new Validation($request, [ + 'email' => 'required|email' +]); + +if ($validation->fails()) { + $errors = $validation->errors(); +} +``` + +#### Using `Request` class: +```php +use WpMVC\RequestValidator\Request; + +$wp_request = new \WP_REST_Request(); +$wp_request->set_params($_POST); // Hydrate with raw global data + +$request = new Request($wp_request); +$request->validate([ + 'title' => 'required|string|max:255' +]); +``` \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml index b431699..24124b5 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -8,9 +8,6 @@ ./tests/Unit - - ./tests/Integration - diff --git a/src/Contracts/Rule.php b/src/Contracts/Rule.php new file mode 100644 index 0000000..dc2ae5d --- /dev/null +++ b/src/Contracts/Rule.php @@ -0,0 +1,50 @@ +wp_rest_request->has_param( $input_name ) ) { - return; - } - - $value = $this->wp_rest_request->get_param( $input_name ); - - if ( ! empty( $format ) ) { - $this->format = $format; - } - - $format = $this->get_format(); - - if ( ! empty( $value ) && $this->is_it_valid_date( $value, $format ) ) { - return; - } - - $this->set_error( $input_name, 'date', [':attribute'], [$input_name] ); - } - - public function date_equals_validator( string $input_name, $date ) { - if ( ! $this->wp_rest_request->has_param( $input_name ) ) { - return; - } - - $value = $this->wp_rest_request->get_param( $input_name ); - - if ( ! empty( $value ) ) { - $format = $this->get_format(); - - if ( ! $this->is_it_valid_date( $value, $format ) ) { - return; - } - - $timestamp = $this->get_timestamp( $date, $format ); - $input_timestamp = $this->get_timestamp( $value, $format ); - - if ( $input_timestamp === $timestamp ) { - return; - } - } - - $this->set_error( $input_name, 'date_equals', [':attribute', ':date'], [$input_name, $date] ); - } - - public function before_validator( string $input_name, $date ) { - if ( ! $this->wp_rest_request->has_param( $input_name ) ) { - return; - } - - $value = $this->wp_rest_request->get_param( $input_name ); - - if ( ! empty( $value ) ) { - $format = $this->get_format(); - - if ( ! $this->is_it_valid_date( $value, $format ) ) { - return; - } - - $timestamp = $this->get_timestamp( $date, $format ); - $input_timestamp = $this->get_timestamp( $value, $format ); - - if ( $input_timestamp < $timestamp ) { - return; - } - } - - $this->set_error( $input_name, 'before', [':attribute', ':date'], [$input_name, $date] ); - } - - public function after_validator( string $input_name, $date ) { - if ( ! $this->wp_rest_request->has_param( $input_name ) ) { - return; - } - - $value = $this->wp_rest_request->get_param( $input_name ); - - if ( ! empty( $value ) ) { - $format = $this->get_format(); - - if ( ! $this->is_it_valid_date( $value, $format ) ) { - return; - } - - $timestamp = $this->get_timestamp( $date, $format ); - $input_timestamp = $this->get_timestamp( $value, $format ); - - if ( $input_timestamp > $timestamp ) { - return; - } - } - - $this->set_error( $input_name, 'after', [':attribute', ':date'], [$input_name, $date] ); - } - - protected function before_or_equal_validator( string $input_name, $date ) { - if ( ! $this->wp_rest_request->has_param( $input_name ) ) { - return; - } - - $value = $this->wp_rest_request->get_param( $input_name ); - - if ( ! empty( $value ) ) { - $format = $this->get_format(); - - if ( ! $this->is_it_valid_date( $value, $format ) ) { - return; - } - - $timestamp = $this->get_timestamp( $date, $format ); - $input_timestamp = $this->get_timestamp( $value, $format ); - - if ( $input_timestamp < $timestamp || $input_timestamp === $timestamp ) { - return; - } - } - - $this->set_error( $input_name, 'before_or_equal', [':attribute', ':date'], [$input_name, $date] ); - } - - protected function after_or_equal_validator( string $input_name, $date ) { - if ( ! $this->wp_rest_request->has_param( $input_name ) ) { - return; - } - - $value = $this->wp_rest_request->get_param( $input_name ); - - if ( ! empty( $value ) ) { - $format = $this->get_format(); - - if ( ! $this->is_it_valid_date( $value, $format ) ) { - return; - } - - $timestamp = $this->get_timestamp( $date, $format ); - $input_timestamp = $this->get_timestamp( $value, $format ); - - if ( $input_timestamp > $timestamp || $input_timestamp === $timestamp ) { - return; - } - } - - $this->set_error( $input_name, 'after_or_equal', [':attribute', ':date'], [$input_name, $date] ); - } - + /** + * The date/time format to use. + * + * @var string + */ + protected $format = 'Y-m-d'; + + /** + * Determine if a date string is valid for a given format. + * + * @param string $date + * @param string $format + * @return bool + */ private function is_it_valid_date( $date, string $format ) { if ( ! is_string( $date ) ) { return false; @@ -162,29 +43,33 @@ private function is_it_valid_date( $date, string $format ) { return $input_date && $input_date->format( $format ) === $date; } + /** + * Get the Unix timestamp for a given date string and format. + * + * @param string $date + * @param string $format + * @return int + */ private function get_timestamp( string $date, string $format ) { - $date_array = date_parse_from_format( $format, $date ); - return mktime( - ! empty( $date_array['hour'] ) ? $date_array['hour'] : 12, - ! empty( $date_array['minute'] ) ? $date_array['minute'] : 0, - ! empty( $date_array['second'] ) ? $date_array['second'] : 0, - $date_array['month'], - $date_array['day'], - $date_array['year'] - ); - } + $request = property_exists( $this, 'validator' ) && $this->validator ? $this->validator->wp_rest_request : $this->wp_rest_request; + + // If the date string matches a parameter in the request, use its value + if ( $request->has_param( $date ) ) { + $date = $request->get_param( $date ); + } + + $dt = PhpDateTime::createFromFormat( $format, $date ); + + if ( ! $dt ) { + return 0; + } - private function get_format() { - foreach ( $this->explode_rules as $key => $value ) { - $substrings = explode( ':', $value, 2 ); - if ( $substrings[0] !== 'date' ) { - continue; - } - if ( isset( $substrings[1] ) ) { - return $substrings[1]; - } - return $this->format; + // If the format doesn't include time components, createFromFormat might set them to current time or '?' + // We should ensure a consistent start-of-day for formats without time. + if ( strpos( $format, 'H' ) === false && strpos( $format, 'h' ) === false ) { + $dt->setTime( 0, 0, 0 ); } - return $this->format; + + return $dt->getTimestamp(); } } \ No newline at end of file diff --git a/src/FormRequest.php b/src/FormRequest.php new file mode 100644 index 0000000..e353459 --- /dev/null +++ b/src/FormRequest.php @@ -0,0 +1,108 @@ +validate(); + } + + /** + * Determine if the user is authorized to make this request. + * Default to true; can be overridden in child classes. + * + * @return bool + */ + public function authorize(): bool { + return true; + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + abstract public function rules(): array; + + /** + * Get custom messages for validator errors. + * + * @return array + */ + public function messages(): array { + return []; + } + + /** + * Get custom attributes for validator errors. + * + * @return array + */ + public function attributes(): array { + return []; + } + + /** + * Configure the validator instance. + * + * @param Validation $validator + * @return void + */ + public function with_validator( Validation $validator ): void { + // Can be overridden in child classes + } + + /** + * Validate the class rules against the incoming request. + * + * @throws Exception + */ + public function validate( array $rules = [], bool $throw_errors = true ): void { + if ( ! $this->authorize() ) { + throw new Exception( __( 'Unauthorized' ), 403 ); + } + + $this->validation = $this->make( + $this, + ! empty( $rules ) ? $rules : $this->rules(), + $this->messages(), + $this->attributes() + ); + + $this->with_validator( $this->validation ); + + $this->validation->throw_if_fails(); + } +} diff --git a/src/HasMime.php b/src/HasMime.php new file mode 100644 index 0000000..0184983 --- /dev/null +++ b/src/HasMime.php @@ -0,0 +1,67 @@ +get_mimes_list(), $file_mime_type ); + $file_extension = pathinfo( $file['name'], PATHINFO_EXTENSION ); + + foreach ( $allowed_mimes as $allowed_mime ) { + if ( in_array( $allowed_mime, $available_mimes ) && $allowed_mime === $file_extension ) { + return true; + } + } + + return false; + } + + /** + * Get the list of supported mimes. + * + * @return array + */ + protected function get_mimes_list(): array { + if ( self::$mimes_cache === null ) { + self::$mimes_cache = require __DIR__ . '/mimes.php'; + } + + return self::$mimes_cache; + } +} \ No newline at end of file diff --git a/src/Mime.php b/src/Mime.php deleted file mode 100644 index 0c986cb..0000000 --- a/src/Mime.php +++ /dev/null @@ -1,1231 +0,0 @@ -list(), $file_mime_type ); - $file_extension = pathinfo( $file['name'], PATHINFO_EXTENSION ); - - foreach ( $allowed_mimes as $allowed_mime ) { - if ( in_array( $allowed_mime, $available_mimes ) && $allowed_mime === $file_extension ) { - return true; - } - } - return false; - } - - public function list() { - return [ - '123' => 'application/vnd.lotus-1-2-3', - '1km' => 'application/vnd.1000minds.decision-model+xml', - '3dml' => 'text/vnd.in3d.3dml', - '3ds' => 'image/x-3ds', - '3g2' => 'video/3gpp2', - '3gp' => 'video/3gpp', - '3gpp' => 'video/3gpp', - '3mf' => 'model/3mf', - '7z' => 'application/x-7z-compressed', - 'aab' => 'application/x-authorware-bin', - 'aac' => 'audio/x-aac', - 'aam' => 'application/x-authorware-map', - 'aas' => 'application/x-authorware-seg', - 'abw' => 'application/x-abiword', - 'ac' => 'application/vnd.nokia.n-gage.ac+xml', - 'acc' => 'application/vnd.americandynamics.acc', - 'ace' => 'application/x-ace-compressed', - 'acu' => 'application/vnd.acucobol', - 'acutc' => 'application/vnd.acucorp', - 'adp' => 'audio/adpcm', - 'adts' => 'audio/aac', - 'aep' => 'application/vnd.audiograph', - 'afm' => 'application/x-font-type1', - 'afp' => 'application/vnd.ibm.modcap', - 'age' => 'application/vnd.age', - 'ahead' => 'application/vnd.ahead.space', - 'ai' => 'application/postscript', - 'aif' => 'audio/x-aiff', - 'aifc' => 'audio/x-aiff', - 'aiff' => 'audio/x-aiff', - 'air' => 'application/vnd.adobe.air-application-installer-package+zip', - 'ait' => 'application/vnd.dvb.ait', - 'ami' => 'application/vnd.amiga.ami', - 'aml' => 'application/automationml-aml+xml', - 'amlx' => 'application/automationml-amlx+zip', - 'amr' => 'audio/amr', - 'apk' => 'application/vnd.android.package-archive', - 'apng' => 'image/apng', - 'appcache' => 'text/cache-manifest', - 'appinstaller' => 'application/appinstaller', - 'application' => 'application/x-ms-application', - 'appx' => 'application/appx', - 'appxbundle' => 'application/appxbundle', - 'apr' => 'application/vnd.lotus-approach', - 'arc' => 'application/x-freearc', - 'arj' => 'application/x-arj', - 'asc' => 'application/pgp-signature', - 'asf' => 'video/x-ms-asf', - 'asm' => 'text/x-asm', - 'aso' => 'application/vnd.accpac.simply.aso', - 'asx' => 'video/x-ms-asf', - 'atc' => 'application/vnd.acucorp', - 'atom' => 'application/atom+xml', - 'atomcat' => 'application/atomcat+xml', - 'atomdeleted' => 'application/atomdeleted+xml', - 'atomsvc' => 'application/atomsvc+xml', - 'atx' => 'application/vnd.antix.game-component', - 'au' => 'audio/basic', - 'avci' => 'image/avci', - 'avcs' => 'image/avcs', - 'avi' => 'video/x-msvideo', - 'avif' => 'image/avif', - 'aw' => 'application/applixware', - 'azf' => 'application/vnd.airzip.filesecure.azf', - 'azs' => 'application/vnd.airzip.filesecure.azs', - 'azv' => 'image/vnd.airzip.accelerator.azv', - 'azw' => 'application/vnd.amazon.ebook', - 'b16' => 'image/vnd.pco.b16', - 'bat' => 'application/x-msdownload', - 'bcpio' => 'application/x-bcpio', - 'bdf' => 'application/x-font-bdf', - 'bdm' => 'application/vnd.syncml.dm+wbxml', - 'bdoc' => 'application/x-bdoc', - 'bed' => 'application/vnd.realvnc.bed', - 'bh2' => 'application/vnd.fujitsu.oasysprs', - 'bin' => 'application/octet-stream', - 'blb' => 'application/x-blorb', - 'blorb' => 'application/x-blorb', - 'bmi' => 'application/vnd.bmi', - 'bmml' => 'application/vnd.balsamiq.bmml+xml', - 'bmp' => 'image/x-ms-bmp', - 'book' => 'application/vnd.framemaker', - 'box' => 'application/vnd.previewsystems.box', - 'boz' => 'application/x-bzip2', - 'bpk' => 'application/octet-stream', - 'bsp' => 'model/vnd.valve.source.compiled-map', - 'btf' => 'image/prs.btif', - 'btif' => 'image/prs.btif', - 'buffer' => 'application/octet-stream', - 'bz' => 'application/x-bzip', - 'bz2' => 'application/x-bzip2', - 'c' => 'text/x-c', - 'c11amc' => 'application/vnd.cluetrust.cartomobile-config', - 'c11amz' => 'application/vnd.cluetrust.cartomobile-config-pkg', - 'c4d' => 'application/vnd.clonk.c4group', - 'c4f' => 'application/vnd.clonk.c4group', - 'c4g' => 'application/vnd.clonk.c4group', - 'c4p' => 'application/vnd.clonk.c4group', - 'c4u' => 'application/vnd.clonk.c4group', - 'cab' => 'application/vnd.ms-cab-compressed', - 'caf' => 'audio/x-caf', - 'cap' => 'application/vnd.tcpdump.pcap', - 'car' => 'application/vnd.curl.car', - 'cat' => 'application/vnd.ms-pki.seccat', - 'cb7' => 'application/x-cbr', - 'cba' => 'application/x-cbr', - 'cbr' => 'application/x-cbr', - 'cbt' => 'application/x-cbr', - 'cbz' => 'application/x-cbr', - 'cc' => 'text/x-c', - 'cco' => 'application/x-cocoa', - 'cct' => 'application/x-director', - 'ccxml' => 'application/ccxml+xml', - 'cdbcmsg' => 'application/vnd.contact.cmsg', - 'cdf' => 'application/x-netcdf', - 'cdfx' => 'application/cdfx+xml', - 'cdkey' => 'application/vnd.mediastation.cdkey', - 'cdmia' => 'application/cdmi-capability', - 'cdmic' => 'application/cdmi-container', - 'cdmid' => 'application/cdmi-domain', - 'cdmio' => 'application/cdmi-object', - 'cdmiq' => 'application/cdmi-queue', - 'cdx' => 'chemical/x-cdx', - 'cdxml' => 'application/vnd.chemdraw+xml', - 'cdy' => 'application/vnd.cinderella', - 'cer' => 'application/pkix-cert', - 'cfs' => 'application/x-cfs-compressed', - 'cgm' => 'image/cgm', - 'chat' => 'application/x-chat', - 'chm' => 'application/vnd.ms-htmlhelp', - 'chrt' => 'application/vnd.kde.kchart', - 'cif' => 'chemical/x-cif', - 'cii' => 'application/vnd.anser-web-certificate-issue-initiation', - 'cil' => 'application/vnd.ms-artgalry', - 'cjs' => 'application/node', - 'cla' => 'application/vnd.claymore', - 'class' => 'application/java-vm', - 'cld' => 'model/vnd.cld', - 'clkk' => 'application/vnd.crick.clicker.keyboard', - 'clkp' => 'application/vnd.crick.clicker.palette', - 'clkt' => 'application/vnd.crick.clicker.template', - 'clkw' => 'application/vnd.crick.clicker.wordbank', - 'clkx' => 'application/vnd.crick.clicker', - 'clp' => 'application/x-msclip', - 'cmc' => 'application/vnd.cosmocaller', - 'cmdf' => 'chemical/x-cmdf', - 'cml' => 'chemical/x-cml', - 'cmp' => 'application/vnd.yellowriver-custom-menu', - 'cmx' => 'image/x-cmx', - 'cod' => 'application/vnd.rim.cod', - 'coffee' => 'text/coffeescript', - 'com' => 'application/x-msdownload', - 'conf' => 'text/plain', - 'cpio' => 'application/x-cpio', - 'cpl' => 'application/cpl+xml', - 'cpp' => 'text/x-c', - 'cpt' => 'application/mac-compactpro', - 'crd' => 'application/x-mscardfile', - 'crl' => 'application/pkix-crl', - 'crt' => 'application/x-x509-ca-cert', - 'crx' => 'application/x-chrome-extension', - 'cryptonote' => 'application/vnd.rig.cryptonote', - 'csh' => 'application/x-csh', - 'csl' => 'application/vnd.citationstyles.style+xml', - 'csml' => 'chemical/x-csml', - 'csp' => 'application/vnd.commonspace', - 'css' => 'text/css', - 'cst' => 'application/x-director', - 'csv' => 'text/csv', - 'cu' => 'application/cu-seeme', - 'curl' => 'text/vnd.curl', - 'cwl' => 'application/cwl', - 'cww' => 'application/prs.cww', - 'cxt' => 'application/x-director', - 'cxx' => 'text/x-c', - 'dae' => 'model/vnd.collada+xml', - 'daf' => 'application/vnd.mobius.daf', - 'dart' => 'application/vnd.dart', - 'dataless' => 'application/vnd.fdsn.seed', - 'davmount' => 'application/davmount+xml', - 'dbf' => 'application/vnd.dbf', - 'dbk' => 'application/docbook+xml', - 'dcr' => 'application/x-director', - 'dcurl' => 'text/vnd.curl.dcurl', - 'dd2' => 'application/vnd.oma.dd2+xml', - 'ddd' => 'application/vnd.fujixerox.ddd', - 'ddf' => 'application/vnd.syncml.dmddf+xml', - 'dds' => 'image/vnd.ms-dds', - 'deb' => 'application/x-debian-package', - 'def' => 'text/plain', - 'deploy' => 'application/octet-stream', - 'der' => 'application/x-x509-ca-cert', - 'dfac' => 'application/vnd.dreamfactory', - 'dgc' => 'application/x-dgc-compressed', - 'dib' => 'image/bmp', - 'dic' => 'text/x-c', - 'dir' => 'application/x-director', - 'dis' => 'application/vnd.mobius.dis', - 'disposition-notification' => 'message/disposition-notification', - 'dist' => 'application/octet-stream', - 'distz' => 'application/octet-stream', - 'djv' => 'image/vnd.djvu', - 'djvu' => 'image/vnd.djvu', - 'dll' => 'application/x-msdownload', - 'dmg' => 'application/x-apple-diskimage', - 'dmp' => 'application/vnd.tcpdump.pcap', - 'dms' => 'application/octet-stream', - 'dna' => 'application/vnd.dna', - 'doc' => 'application/msword', - 'docm' => 'application/vnd.ms-word.document.macroenabled.12', - 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'dot' => 'application/msword', - 'dotm' => 'application/vnd.ms-word.template.macroenabled.12', - 'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', - 'dp' => 'application/vnd.osgi.dp', - 'dpg' => 'application/vnd.dpgraph', - 'dpx' => 'image/dpx', - 'dra' => 'audio/vnd.dra', - 'drle' => 'image/dicom-rle', - 'dsc' => 'text/prs.lines.tag', - 'dssc' => 'application/dssc+der', - 'dtb' => 'application/x-dtbook+xml', - 'dtd' => 'application/xml-dtd', - 'dts' => 'audio/vnd.dts', - 'dtshd' => 'audio/vnd.dts.hd', - 'dump' => 'application/octet-stream', - 'dvb' => 'video/vnd.dvb.file', - 'dvi' => 'application/x-dvi', - 'dwd' => 'application/atsc-dwd+xml', - 'dwf' => 'model/vnd.dwf', - 'dwg' => 'image/vnd.dwg', - 'dxf' => 'image/vnd.dxf', - 'dxp' => 'application/vnd.spotfire.dxp', - 'dxr' => 'application/x-director', - 'ear' => 'application/java-archive', - 'ecelp4800' => 'audio/vnd.nuera.ecelp4800', - 'ecelp7470' => 'audio/vnd.nuera.ecelp7470', - 'ecelp9600' => 'audio/vnd.nuera.ecelp9600', - 'ecma' => 'application/ecmascript', - 'edm' => 'application/vnd.novadigm.edm', - 'edx' => 'application/vnd.novadigm.edx', - 'efif' => 'application/vnd.picsel', - 'ei6' => 'application/vnd.pg.osasli', - 'elc' => 'application/octet-stream', - 'emf' => 'image/emf', - 'eml' => 'message/rfc822', - 'emma' => 'application/emma+xml', - 'emotionml' => 'application/emotionml+xml', - 'emz' => 'application/x-msmetafile', - 'eol' => 'audio/vnd.digital-winds', - 'eot' => 'application/vnd.ms-fontobject', - 'eps' => 'application/postscript', - 'epub' => 'application/epub+zip', - 'es3' => 'application/vnd.eszigno3+xml', - 'esa' => 'application/vnd.osgi.subsystem', - 'esf' => 'application/vnd.epson.esf', - 'et3' => 'application/vnd.eszigno3+xml', - 'etx' => 'text/x-setext', - 'eva' => 'application/x-eva', - 'evy' => 'application/x-envoy', - 'exe' => 'application/x-msdownload', - 'exi' => 'application/exi', - 'exp' => 'application/express', - 'exr' => 'image/aces', - 'ext' => 'application/vnd.novadigm.ext', - 'ez' => 'application/andrew-inset', - 'ez2' => 'application/vnd.ezpix-album', - 'ez3' => 'application/vnd.ezpix-package', - 'f' => 'text/x-fortran', - 'f4v' => 'video/x-f4v', - 'f77' => 'text/x-fortran', - 'f90' => 'text/x-fortran', - 'fbs' => 'image/vnd.fastbidsheet', - 'fcdt' => 'application/vnd.adobe.formscentral.fcdt', - 'fcs' => 'application/vnd.isac.fcs', - 'fdf' => 'application/vnd.fdf', - 'fdt' => 'application/fdt+xml', - 'fe_launch' => 'application/vnd.denovo.fcselayout-link', - 'fg5' => 'application/vnd.fujitsu.oasysgp', - 'fgd' => 'application/x-director', - 'fh' => 'image/x-freehand', - 'fh4' => 'image/x-freehand', - 'fh5' => 'image/x-freehand', - 'fh7' => 'image/x-freehand', - 'fhc' => 'image/x-freehand', - 'fig' => 'application/x-xfig', - 'fits' => 'image/fits', - 'flac' => 'audio/x-flac', - 'fli' => 'video/x-fli', - 'flo' => 'application/vnd.micrografx.flo', - 'flv' => 'video/x-flv', - 'flw' => 'application/vnd.kde.kivio', - 'flx' => 'text/vnd.fmi.flexstor', - 'fly' => 'text/vnd.fly', - 'fm' => 'application/vnd.framemaker', - 'fnc' => 'application/vnd.frogans.fnc', - 'fo' => 'application/vnd.software602.filler.form+xml', - 'for' => 'text/x-fortran', - 'fpx' => 'image/vnd.fpx', - 'frame' => 'application/vnd.framemaker', - 'fsc' => 'application/vnd.fsc.weblaunch', - 'fst' => 'image/vnd.fst', - 'ftc' => 'application/vnd.fluxtime.clip', - 'fti' => 'application/vnd.anser-web-funds-transfer-initiation', - 'fvt' => 'video/vnd.fvt', - 'fxp' => 'application/vnd.adobe.fxp', - 'fxpl' => 'application/vnd.adobe.fxp', - 'fzs' => 'application/vnd.fuzzysheet', - 'g2w' => 'application/vnd.geoplan', - 'g3' => 'image/g3fax', - 'g3w' => 'application/vnd.geospace', - 'gac' => 'application/vnd.groove-account', - 'gam' => 'application/x-tads', - 'gbr' => 'application/rpki-ghostbusters', - 'gca' => 'application/x-gca-compressed', - 'gdl' => 'model/vnd.gdl', - 'gdoc' => 'application/vnd.google-apps.document', - 'ged' => 'text/vnd.familysearch.gedcom', - 'geo' => 'application/vnd.dynageo', - 'geojson' => 'application/geo+json', - 'gex' => 'application/vnd.geometry-explorer', - 'ggb' => 'application/vnd.geogebra.file', - 'ggt' => 'application/vnd.geogebra.tool', - 'ghf' => 'application/vnd.groove-help', - 'gif' => 'image/gif', - 'gim' => 'application/vnd.groove-identity-message', - 'glb' => 'model/gltf-binary', - 'gltf' => 'model/gltf+json', - 'gml' => 'application/gml+xml', - 'gmx' => 'application/vnd.gmx', - 'gnumeric' => 'application/x-gnumeric', - 'gph' => 'application/vnd.flographit', - 'gpx' => 'application/gpx+xml', - 'gqf' => 'application/vnd.grafeq', - 'gqs' => 'application/vnd.grafeq', - 'gram' => 'application/srgs', - 'gramps' => 'application/x-gramps-xml', - 'gre' => 'application/vnd.geometry-explorer', - 'grv' => 'application/vnd.groove-injector', - 'grxml' => 'application/srgs+xml', - 'gsf' => 'application/x-font-ghostscript', - 'gsheet' => 'application/vnd.google-apps.spreadsheet', - 'gslides' => 'application/vnd.google-apps.presentation', - 'gtar' => 'application/x-gtar', - 'gtm' => 'application/vnd.groove-tool-message', - 'gtw' => 'model/vnd.gtw', - 'gv' => 'text/vnd.graphviz', - 'gxf' => 'application/gxf', - 'gxt' => 'application/vnd.geonext', - 'gz' => 'application/gzip', - 'h' => 'text/x-c', - 'h261' => 'video/h261', - 'h263' => 'video/h263', - 'h264' => 'video/h264', - 'hal' => 'application/vnd.hal+xml', - 'hbci' => 'application/vnd.hbci', - 'hbs' => 'text/x-handlebars-template', - 'hdd' => 'application/x-virtualbox-hdd', - 'hdf' => 'application/x-hdf', - 'heic' => 'image/heic', - 'heics' => 'image/heic-sequence', - 'heif' => 'image/heif', - 'heifs' => 'image/heif-sequence', - 'hej2' => 'image/hej2k', - 'held' => 'application/atsc-held+xml', - 'hh' => 'text/x-c', - 'hjson' => 'application/hjson', - 'hlp' => 'application/winhlp', - 'hpgl' => 'application/vnd.hp-hpgl', - 'hpid' => 'application/vnd.hp-hpid', - 'hps' => 'application/vnd.hp-hps', - 'hqx' => 'application/mac-binhex40', - 'hsj2' => 'image/hsj2', - 'htc' => 'text/x-component', - 'htke' => 'application/vnd.kenameaapp', - 'htm' => 'text/html', - 'html' => 'text/html', - 'hvd' => 'application/vnd.yamaha.hv-dic', - 'hvp' => 'application/vnd.yamaha.hv-voice', - 'hvs' => 'application/vnd.yamaha.hv-script', - 'i2g' => 'application/vnd.intergeo', - 'icc' => 'application/vnd.iccprofile', - 'ice' => 'x-conference/x-cooltalk', - 'icm' => 'application/vnd.iccprofile', - 'ico' => 'image/x-icon', - 'ics' => 'text/calendar', - 'ief' => 'image/ief', - 'ifb' => 'text/calendar', - 'ifm' => 'application/vnd.shana.informed.formdata', - 'iges' => 'model/iges', - 'igl' => 'application/vnd.igloader', - 'igm' => 'application/vnd.insors.igm', - 'igs' => 'model/iges', - 'igx' => 'application/vnd.micrografx.igx', - 'iif' => 'application/vnd.shana.informed.interchange', - 'img' => 'application/octet-stream', - 'imp' => 'application/vnd.accpac.simply.imp', - 'ims' => 'application/vnd.ms-ims', - 'in' => 'text/plain', - 'ini' => 'text/plain', - 'ink' => 'application/inkml+xml', - 'inkml' => 'application/inkml+xml', - 'install' => 'application/x-install-instructions', - 'iota' => 'application/vnd.astraea-software.iota', - 'ipfix' => 'application/ipfix', - 'ipk' => 'application/vnd.shana.informed.package', - 'irm' => 'application/vnd.ibm.rights-management', - 'irp' => 'application/vnd.irepository.package+xml', - 'iso' => 'application/x-iso9660-image', - 'itp' => 'application/vnd.shana.informed.formtemplate', - 'its' => 'application/its+xml', - 'ivp' => 'application/vnd.immervision-ivp', - 'ivu' => 'application/vnd.immervision-ivu', - 'jad' => 'text/vnd.sun.j2me.app-descriptor', - 'jade' => 'text/jade', - 'jam' => 'application/vnd.jam', - 'jar' => 'application/java-archive', - 'jardiff' => 'application/x-java-archive-diff', - 'java' => 'text/x-java-source', - 'jhc' => 'image/jphc', - 'jisp' => 'application/vnd.jisp', - 'jls' => 'image/jls', - 'jlt' => 'application/vnd.hp-jlyt', - 'jng' => 'image/x-jng', - 'jnlp' => 'application/x-java-jnlp-file', - 'joda' => 'application/vnd.joost.joda-archive', - 'jp2' => 'image/jp2', - 'jpe' => 'image/jpeg', - 'jpeg' => 'image/jpeg', - 'jpf' => 'image/jpx', - 'jpg' => 'image/jpeg', - 'jpg2' => 'image/jp2', - 'jpgm' => 'video/jpm', - 'jpgv' => 'video/jpeg', - 'jph' => 'image/jph', - 'jpm' => 'video/jpm', - 'jpx' => 'image/jpx', - 'js' => 'text/javascript', - 'json' => 'application/json', - 'json5' => 'application/json5', - 'jsonld' => 'application/ld+json', - 'jsonml' => 'application/jsonml+json', - 'jsx' => 'text/jsx', - 'jt' => 'model/jt', - 'jxr' => 'image/jxr', - 'jxra' => 'image/jxra', - 'jxrs' => 'image/jxrs', - 'jxs' => 'image/jxs', - 'jxsc' => 'image/jxsc', - 'jxsi' => 'image/jxsi', - 'jxss' => 'image/jxss', - 'kar' => 'audio/midi', - 'karbon' => 'application/vnd.kde.karbon', - 'kdbx' => 'application/x-keepass2', - 'key' => 'application/x-iwork-keynote-sffkey', - 'kfo' => 'application/vnd.kde.kformula', - 'kia' => 'application/vnd.kidspiration', - 'kml' => 'application/vnd.google-earth.kml+xml', - 'kmz' => 'application/vnd.google-earth.kmz', - 'kne' => 'application/vnd.kinar', - 'knp' => 'application/vnd.kinar', - 'kon' => 'application/vnd.kde.kontour', - 'kpr' => 'application/vnd.kde.kpresenter', - 'kpt' => 'application/vnd.kde.kpresenter', - 'kpxx' => 'application/vnd.ds-keypoint', - 'ksp' => 'application/vnd.kde.kspread', - 'ktr' => 'application/vnd.kahootz', - 'ktx' => 'image/ktx', - 'ktx2' => 'image/ktx2', - 'ktz' => 'application/vnd.kahootz', - 'kwd' => 'application/vnd.kde.kword', - 'kwt' => 'application/vnd.kde.kword', - 'lasxml' => 'application/vnd.las.las+xml', - 'latex' => 'application/x-latex', - 'lbd' => 'application/vnd.llamagraphics.life-balance.desktop', - 'lbe' => 'application/vnd.llamagraphics.life-balance.exchange+xml', - 'les' => 'application/vnd.hhe.lesson-player', - 'less' => 'text/less', - 'lgr' => 'application/lgr+xml', - 'lha' => 'application/x-lzh-compressed', - 'link66' => 'application/vnd.route66.link66+xml', - 'list' => 'text/plain', - 'list3820' => 'application/vnd.ibm.modcap', - 'listafp' => 'application/vnd.ibm.modcap', - 'litcoffee' => 'text/coffeescript', - 'lnk' => 'application/x-ms-shortcut', - 'log' => 'text/plain', - 'lostxml' => 'application/lost+xml', - 'lrf' => 'application/octet-stream', - 'lrm' => 'application/vnd.ms-lrm', - 'ltf' => 'application/vnd.frogans.ltf', - 'lua' => 'text/x-lua', - 'luac' => 'application/x-lua-bytecode', - 'lvp' => 'audio/vnd.lucent.voice', - 'lwp' => 'application/vnd.lotus-wordpro', - 'lzh' => 'application/x-lzh-compressed', - 'm13' => 'application/x-msmediaview', - 'm14' => 'application/x-msmediaview', - 'm1v' => 'video/mpeg', - 'm21' => 'application/mp21', - 'm2a' => 'audio/mpeg', - 'm2v' => 'video/mpeg', - 'm3a' => 'audio/mpeg', - 'm3u' => 'audio/x-mpegurl', - 'm3u8' => 'application/vnd.apple.mpegurl', - 'm4a' => 'audio/x-m4a', - 'm4p' => 'application/mp4', - 'm4s' => 'video/iso.segment', - 'm4u' => 'video/vnd.mpegurl', - 'm4v' => 'video/x-m4v', - 'ma' => 'application/mathematica', - 'mads' => 'application/mads+xml', - 'maei' => 'application/mmt-aei+xml', - 'mag' => 'application/vnd.ecowin.chart', - 'maker' => 'application/vnd.framemaker', - 'man' => 'text/troff', - 'manifest' => 'text/cache-manifest', - 'map' => 'application/json', - 'mar' => 'application/octet-stream', - 'markdown' => 'text/markdown', - 'mathml' => 'application/mathml+xml', - 'mb' => 'application/mathematica', - 'mbk' => 'application/vnd.mobius.mbk', - 'mbox' => 'application/mbox', - 'mc1' => 'application/vnd.medcalcdata', - 'mcd' => 'application/vnd.mcd', - 'mcurl' => 'text/vnd.curl.mcurl', - 'md' => 'text/markdown', - 'mdb' => 'application/x-msaccess', - 'mdi' => 'image/vnd.ms-modi', - 'mdx' => 'text/mdx', - 'me' => 'text/troff', - 'mesh' => 'model/mesh', - 'meta4' => 'application/metalink4+xml', - 'metalink' => 'application/metalink+xml', - 'mets' => 'application/mets+xml', - 'mfm' => 'application/vnd.mfmp', - 'mft' => 'application/rpki-manifest', - 'mgp' => 'application/vnd.osgeo.mapguide.package', - 'mgz' => 'application/vnd.proteus.magazine', - 'mid' => 'audio/midi', - 'midi' => 'audio/midi', - 'mie' => 'application/x-mie', - 'mif' => 'application/vnd.mif', - 'mime' => 'message/rfc822', - 'mj2' => 'video/mj2', - 'mjp2' => 'video/mj2', - 'mjs' => 'text/javascript', - 'mk3d' => 'video/x-matroska', - 'mka' => 'audio/x-matroska', - 'mkd' => 'text/x-markdown', - 'mks' => 'video/x-matroska', - 'mkv' => 'video/x-matroska', - 'mlp' => 'application/vnd.dolby.mlp', - 'mmd' => 'application/vnd.chipnuts.karaoke-mmd', - 'mmf' => 'application/vnd.smaf', - 'mml' => 'text/mathml', - 'mmr' => 'image/vnd.fujixerox.edmics-mmr', - 'mng' => 'video/x-mng', - 'mny' => 'application/x-msmoney', - 'mobi' => 'application/x-mobipocket-ebook', - 'mods' => 'application/mods+xml', - 'mov' => 'video/quicktime', - 'movie' => 'video/x-sgi-movie', - 'mp2' => 'audio/mpeg', - 'mp21' => 'application/mp21', - 'mp2a' => 'audio/mpeg', - 'mp3' => 'audio/mpeg', - 'mp4' => 'video/mp4', - 'mp4a' => 'audio/mp4', - 'mp4s' => 'application/mp4', - 'mp4v' => 'video/mp4', - 'mpc' => 'application/vnd.mophun.certificate', - 'mpd' => 'application/dash+xml', - 'mpe' => 'video/mpeg', - 'mpeg' => 'video/mpeg', - 'mpf' => 'application/media-policy-dataset+xml', - 'mpg' => 'video/mpeg', - 'mpg4' => 'video/mp4', - 'mpga' => 'audio/mpeg', - 'mpkg' => 'application/vnd.apple.installer+xml', - 'mpm' => 'application/vnd.blueice.multipass', - 'mpn' => 'application/vnd.mophun.application', - 'mpp' => 'application/vnd.ms-project', - 'mpt' => 'application/vnd.ms-project', - 'mpy' => 'application/vnd.ibm.minipay', - 'mqy' => 'application/vnd.mobius.mqy', - 'mrc' => 'application/marc', - 'mrcx' => 'application/marcxml+xml', - 'ms' => 'text/troff', - 'mscml' => 'application/mediaservercontrol+xml', - 'mseed' => 'application/vnd.fdsn.mseed', - 'mseq' => 'application/vnd.mseq', - 'msf' => 'application/vnd.epson.msf', - 'msg' => 'application/vnd.ms-outlook', - 'msh' => 'model/mesh', - 'msi' => 'application/x-msdownload', - 'msix' => 'application/msix', - 'msixbundle' => 'application/msixbundle', - 'msl' => 'application/vnd.mobius.msl', - 'msm' => 'application/octet-stream', - 'msp' => 'application/octet-stream', - 'msty' => 'application/vnd.muvee.style', - 'mtl' => 'model/mtl', - 'mts' => 'model/vnd.mts', - 'mus' => 'application/vnd.musician', - 'musd' => 'application/mmt-usd+xml', - 'musicxml' => 'application/vnd.recordare.musicxml+xml', - 'mvb' => 'application/x-msmediaview', - 'mvt' => 'application/vnd.mapbox-vector-tile', - 'mwf' => 'application/vnd.mfer', - 'mxf' => 'application/mxf', - 'mxl' => 'application/vnd.recordare.musicxml', - 'mxmf' => 'audio/mobile-xmf', - 'mxml' => 'application/xv+xml', - 'mxs' => 'application/vnd.triscape.mxs', - 'mxu' => 'video/vnd.mpegurl', - 'n-gage' => 'application/vnd.nokia.n-gage.symbian.install', - 'n3' => 'text/n3', - 'nb' => 'application/mathematica', - 'nbp' => 'application/vnd.wolfram.player', - 'nc' => 'application/x-netcdf', - 'ncx' => 'application/x-dtbncx+xml', - 'nfo' => 'text/x-nfo', - 'ngdat' => 'application/vnd.nokia.n-gage.data', - 'nitf' => 'application/vnd.nitf', - 'nlu' => 'application/vnd.neurolanguage.nlu', - 'nml' => 'application/vnd.enliven', - 'nnd' => 'application/vnd.noblenet-directory', - 'nns' => 'application/vnd.noblenet-sealer', - 'nnw' => 'application/vnd.noblenet-web', - 'npx' => 'image/vnd.net-fpx', - 'nq' => 'application/n-quads', - 'nsc' => 'application/x-conference', - 'nsf' => 'application/vnd.lotus-notes', - 'nt' => 'application/n-triples', - 'ntf' => 'application/vnd.nitf', - 'numbers' => 'application/x-iwork-numbers-sffnumbers', - 'nzb' => 'application/x-nzb', - 'oa2' => 'application/vnd.fujitsu.oasys2', - 'oa3' => 'application/vnd.fujitsu.oasys3', - 'oas' => 'application/vnd.fujitsu.oasys', - 'obd' => 'application/x-msbinder', - 'obgx' => 'application/vnd.openblox.game+xml', - 'obj' => 'model/obj', - 'oda' => 'application/oda', - 'odb' => 'application/vnd.oasis.opendocument.database', - 'odc' => 'application/vnd.oasis.opendocument.chart', - 'odf' => 'application/vnd.oasis.opendocument.formula', - 'odft' => 'application/vnd.oasis.opendocument.formula-template', - 'odg' => 'application/vnd.oasis.opendocument.graphics', - 'odi' => 'application/vnd.oasis.opendocument.image', - 'odm' => 'application/vnd.oasis.opendocument.text-master', - 'odp' => 'application/vnd.oasis.opendocument.presentation', - 'ods' => 'application/vnd.oasis.opendocument.spreadsheet', - 'odt' => 'application/vnd.oasis.opendocument.text', - 'oga' => 'audio/ogg', - 'ogex' => 'model/vnd.opengex', - 'ogg' => 'audio/ogg', - 'ogv' => 'video/ogg', - 'ogx' => 'application/ogg', - 'omdoc' => 'application/omdoc+xml', - 'onepkg' => 'application/onenote', - 'onetmp' => 'application/onenote', - 'onetoc' => 'application/onenote', - 'onetoc2' => 'application/onenote', - 'opf' => 'application/oebps-package+xml', - 'opml' => 'text/x-opml', - 'oprc' => 'application/vnd.palm', - 'opus' => 'audio/ogg', - 'org' => 'text/x-org', - 'osf' => 'application/vnd.yamaha.openscoreformat', - 'osfpvg' => 'application/vnd.yamaha.openscoreformat.osfpvg+xml', - 'osm' => 'application/vnd.openstreetmap.data+xml', - 'otc' => 'application/vnd.oasis.opendocument.chart-template', - 'otf' => 'font/otf', - 'otg' => 'application/vnd.oasis.opendocument.graphics-template', - 'oth' => 'application/vnd.oasis.opendocument.text-web', - 'oti' => 'application/vnd.oasis.opendocument.image-template', - 'otp' => 'application/vnd.oasis.opendocument.presentation-template', - 'ots' => 'application/vnd.oasis.opendocument.spreadsheet-template', - 'ott' => 'application/vnd.oasis.opendocument.text-template', - 'ova' => 'application/x-virtualbox-ova', - 'ovf' => 'application/x-virtualbox-ovf', - 'owl' => 'application/rdf+xml', - 'oxps' => 'application/oxps', - 'oxt' => 'application/vnd.openofficeorg.extension', - 'p' => 'text/x-pascal', - 'p10' => 'application/pkcs10', - 'p12' => 'application/x-pkcs12', - 'p7b' => 'application/x-pkcs7-certificates', - 'p7c' => 'application/pkcs7-mime', - 'p7m' => 'application/pkcs7-mime', - 'p7r' => 'application/x-pkcs7-certreqresp', - 'p7s' => 'application/pkcs7-signature', - 'p8' => 'application/pkcs8', - 'pac' => 'application/x-ns-proxy-autoconfig', - 'pages' => 'application/x-iwork-pages-sffpages', - 'pas' => 'text/x-pascal', - 'paw' => 'application/vnd.pawaafile', - 'pbd' => 'application/vnd.powerbuilder6', - 'pbm' => 'image/x-portable-bitmap', - 'pcap' => 'application/vnd.tcpdump.pcap', - 'pcf' => 'application/x-font-pcf', - 'pcl' => 'application/vnd.hp-pcl', - 'pclxl' => 'application/vnd.hp-pclxl', - 'pct' => 'image/x-pict', - 'pcurl' => 'application/vnd.curl.pcurl', - 'pcx' => 'image/x-pcx', - 'pdb' => 'application/x-pilot', - 'pde' => 'text/x-processing', - 'pdf' => 'application/pdf', - 'pem' => 'application/x-x509-ca-cert', - 'pfa' => 'application/x-font-type1', - 'pfb' => 'application/x-font-type1', - 'pfm' => 'application/x-font-type1', - 'pfr' => 'application/font-tdpfr', - 'pfx' => 'application/x-pkcs12', - 'pgm' => 'image/x-portable-graymap', - 'pgn' => 'application/x-chess-pgn', - 'pgp' => 'application/pgp-encrypted', - 'php' => 'application/x-httpd-php', - 'pic' => 'image/x-pict', - 'pkg' => 'application/octet-stream', - 'pki' => 'application/pkixcmp', - 'pkipath' => 'application/pkix-pkipath', - 'pkpass' => 'application/vnd.apple.pkpass', - 'pl' => 'application/x-perl', - 'plb' => 'application/vnd.3gpp.pic-bw-large', - 'plc' => 'application/vnd.mobius.plc', - 'plf' => 'application/vnd.pocketlearn', - 'pls' => 'application/pls+xml', - 'pm' => 'application/x-perl', - 'pml' => 'application/vnd.ctc-posml', - 'png' => 'image/png', - 'pnm' => 'image/x-portable-anymap', - 'portpkg' => 'application/vnd.macports.portpkg', - 'pot' => 'application/vnd.ms-powerpoint', - 'potm' => 'application/vnd.ms-powerpoint.template.macroenabled.12', - 'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template', - 'ppam' => 'application/vnd.ms-powerpoint.addin.macroenabled.12', - 'ppd' => 'application/vnd.cups-ppd', - 'ppm' => 'image/x-portable-pixmap', - 'pps' => 'application/vnd.ms-powerpoint', - 'ppsm' => 'application/vnd.ms-powerpoint.slideshow.macroenabled.12', - 'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', - 'ppt' => 'application/vnd.ms-powerpoint', - 'pptm' => 'application/vnd.ms-powerpoint.presentation.macroenabled.12', - 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - 'pqa' => 'application/vnd.palm', - 'prc' => 'model/prc', - 'pre' => 'application/vnd.lotus-freelance', - 'prf' => 'application/pics-rules', - 'provx' => 'application/provenance+xml', - 'ps' => 'application/postscript', - 'psb' => 'application/vnd.3gpp.pic-bw-small', - 'psd' => 'image/vnd.adobe.photoshop', - 'psf' => 'application/x-font-linux-psf', - 'pskcxml' => 'application/pskc+xml', - 'pti' => 'image/prs.pti', - 'ptid' => 'application/vnd.pvi.ptid1', - 'pub' => 'application/x-mspublisher', - 'pvb' => 'application/vnd.3gpp.pic-bw-var', - 'pwn' => 'application/vnd.3m.post-it-notes', - 'pya' => 'audio/vnd.ms-playready.media.pya', - 'pyo' => 'model/vnd.pytha.pyox', - 'pyox' => 'model/vnd.pytha.pyox', - 'pyv' => 'video/vnd.ms-playready.media.pyv', - 'qam' => 'application/vnd.epson.quickanime', - 'qbo' => 'application/vnd.intu.qbo', - 'qfx' => 'application/vnd.intu.qfx', - 'qps' => 'application/vnd.publishare-delta-tree', - 'qt' => 'video/quicktime', - 'qwd' => 'application/vnd.quark.quarkxpress', - 'qwt' => 'application/vnd.quark.quarkxpress', - 'qxb' => 'application/vnd.quark.quarkxpress', - 'qxd' => 'application/vnd.quark.quarkxpress', - 'qxl' => 'application/vnd.quark.quarkxpress', - 'qxt' => 'application/vnd.quark.quarkxpress', - 'ra' => 'audio/x-realaudio', - 'ram' => 'audio/x-pn-realaudio', - 'raml' => 'application/raml+yaml', - 'rapd' => 'application/route-apd+xml', - 'rar' => 'application/x-rar-compressed', - 'ras' => 'image/x-cmu-raster', - 'rcprofile' => 'application/vnd.ipunplugged.rcprofile', - 'rdf' => 'application/rdf+xml', - 'rdz' => 'application/vnd.data-vision.rdz', - 'relo' => 'application/p2p-overlay+xml', - 'rep' => 'application/vnd.businessobjects', - 'res' => 'application/x-dtbresource+xml', - 'rgb' => 'image/x-rgb', - 'rif' => 'application/reginfo+xml', - 'rip' => 'audio/vnd.rip', - 'ris' => 'application/x-research-info-systems', - 'rl' => 'application/resource-lists+xml', - 'rlc' => 'image/vnd.fujixerox.edmics-rlc', - 'rld' => 'application/resource-lists-diff+xml', - 'rm' => 'application/vnd.rn-realmedia', - 'rmi' => 'audio/midi', - 'rmp' => 'audio/x-pn-realaudio-plugin', - 'rms' => 'application/vnd.jcp.javame.midlet-rms', - 'rmvb' => 'application/vnd.rn-realmedia-vbr', - 'rnc' => 'application/relax-ng-compact-syntax', - 'rng' => 'application/xml', - 'roa' => 'application/rpki-roa', - 'roff' => 'text/troff', - 'rp9' => 'application/vnd.cloanto.rp9', - 'rpm' => 'application/x-redhat-package-manager', - 'rpss' => 'application/vnd.nokia.radio-presets', - 'rpst' => 'application/vnd.nokia.radio-preset', - 'rq' => 'application/sparql-query', - 'rs' => 'application/rls-services+xml', - 'rsat' => 'application/atsc-rsat+xml', - 'rsd' => 'application/rsd+xml', - 'rsheet' => 'application/urc-ressheet+xml', - 'rss' => 'application/rss+xml', - 'rtf' => 'text/rtf', - 'rtx' => 'text/richtext', - 'run' => 'application/x-makeself', - 'rusd' => 'application/route-usd+xml', - 's' => 'text/x-asm', - 's3m' => 'audio/s3m', - 'saf' => 'application/vnd.yamaha.smaf-audio', - 'sass' => 'text/x-sass', - 'sbml' => 'application/sbml+xml', - 'sc' => 'application/vnd.ibm.secure-container', - 'scd' => 'application/x-msschedule', - 'scm' => 'application/vnd.lotus-screencam', - 'scq' => 'application/scvp-cv-request', - 'scs' => 'application/scvp-cv-response', - 'scss' => 'text/x-scss', - 'scurl' => 'text/vnd.curl.scurl', - 'sda' => 'application/vnd.stardivision.draw', - 'sdc' => 'application/vnd.stardivision.calc', - 'sdd' => 'application/vnd.stardivision.impress', - 'sdkd' => 'application/vnd.solent.sdkm+xml', - 'sdkm' => 'application/vnd.solent.sdkm+xml', - 'sdp' => 'application/sdp', - 'sdw' => 'application/vnd.stardivision.writer', - 'sea' => 'application/x-sea', - 'see' => 'application/vnd.seemail', - 'seed' => 'application/vnd.fdsn.seed', - 'sema' => 'application/vnd.sema', - 'semd' => 'application/vnd.semd', - 'semf' => 'application/vnd.semf', - 'senmlx' => 'application/senml+xml', - 'sensmlx' => 'application/sensml+xml', - 'ser' => 'application/java-serialized-object', - 'setpay' => 'application/set-payment-initiation', - 'setreg' => 'application/set-registration-initiation', - 'sfd-hdstx' => 'application/vnd.hydrostatix.sof-data', - 'sfs' => 'application/vnd.spotfire.sfs', - 'sfv' => 'text/x-sfv', - 'sgi' => 'image/sgi', - 'sgl' => 'application/vnd.stardivision.writer-global', - 'sgm' => 'text/sgml', - 'sgml' => 'text/sgml', - 'sh' => 'application/x-sh', - 'shar' => 'application/x-shar', - 'shex' => 'text/shex', - 'shf' => 'application/shf+xml', - 'shtml' => 'text/html', - 'sid' => 'image/x-mrsid-image', - 'sieve' => 'application/sieve', - 'sig' => 'application/pgp-signature', - 'sil' => 'audio/silk', - 'silo' => 'model/mesh', - 'sis' => 'application/vnd.symbian.install', - 'sisx' => 'application/vnd.symbian.install', - 'sit' => 'application/x-stuffit', - 'sitx' => 'application/x-stuffitx', - 'siv' => 'application/sieve', - 'skd' => 'application/vnd.koan', - 'skm' => 'application/vnd.koan', - 'skp' => 'application/vnd.koan', - 'skt' => 'application/vnd.koan', - 'sldm' => 'application/vnd.ms-powerpoint.slide.macroenabled.12', - 'sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide', - 'slim' => 'text/slim', - 'slm' => 'text/slim', - 'sls' => 'application/route-s-tsid+xml', - 'slt' => 'application/vnd.epson.salt', - 'sm' => 'application/vnd.stepmania.stepchart', - 'smf' => 'application/vnd.stardivision.math', - 'smi' => 'application/smil+xml', - 'smil' => 'application/smil+xml', - 'smv' => 'video/x-smv', - 'smzip' => 'application/vnd.stepmania.package', - 'snd' => 'audio/basic', - 'snf' => 'application/x-font-snf', - 'so' => 'application/octet-stream', - 'spc' => 'application/x-pkcs7-certificates', - 'spdx' => 'text/spdx', - 'spf' => 'application/vnd.yamaha.smaf-phrase', - 'spl' => 'application/x-futuresplash', - 'spot' => 'text/vnd.in3d.spot', - 'spp' => 'application/scvp-vp-response', - 'spq' => 'application/scvp-vp-request', - 'spx' => 'audio/ogg', - 'sql' => 'application/x-sql', - 'src' => 'application/x-wais-source', - 'srt' => 'application/x-subrip', - 'sru' => 'application/sru+xml', - 'srx' => 'application/sparql-results+xml', - 'ssdl' => 'application/ssdl+xml', - 'sse' => 'application/vnd.kodak-descriptor', - 'ssf' => 'application/vnd.epson.ssf', - 'ssml' => 'application/ssml+xml', - 'st' => 'application/vnd.sailingtracker.track', - 'stc' => 'application/vnd.sun.xml.calc.template', - 'std' => 'application/vnd.sun.xml.draw.template', - 'stf' => 'application/vnd.wt.stf', - 'sti' => 'application/vnd.sun.xml.impress.template', - 'stk' => 'application/hyperstudio', - 'stl' => 'model/stl', - 'stpx' => 'model/step+xml', - 'stpxz' => 'model/step-xml+zip', - 'stpz' => 'model/step+zip', - 'str' => 'application/vnd.pg.format', - 'stw' => 'application/vnd.sun.xml.writer.template', - 'styl' => 'text/stylus', - 'stylus' => 'text/stylus', - 'sub' => 'text/vnd.dvb.subtitle', - 'sus' => 'application/vnd.sus-calendar', - 'susp' => 'application/vnd.sus-calendar', - 'sv4cpio' => 'application/x-sv4cpio', - 'sv4crc' => 'application/x-sv4crc', - 'svc' => 'application/vnd.dvb.service', - 'svd' => 'application/vnd.svd', - 'svg' => 'image/svg+xml', - 'svgz' => 'image/svg+xml', - 'swa' => 'application/x-director', - 'swf' => 'application/x-shockwave-flash', - 'swi' => 'application/vnd.aristanetworks.swi', - 'swidtag' => 'application/swid+xml', - 'sxc' => 'application/vnd.sun.xml.calc', - 'sxd' => 'application/vnd.sun.xml.draw', - 'sxg' => 'application/vnd.sun.xml.writer.global', - 'sxi' => 'application/vnd.sun.xml.impress', - 'sxm' => 'application/vnd.sun.xml.math', - 'sxw' => 'application/vnd.sun.xml.writer', - 't' => 'text/troff', - 't3' => 'application/x-t3vm-image', - 't38' => 'image/t38', - 'taglet' => 'application/vnd.mynfc', - 'tao' => 'application/vnd.tao.intent-module-archive', - 'tap' => 'image/vnd.tencent.tap', - 'tar' => 'application/x-tar', - 'tcap' => 'application/vnd.3gpp2.tcap', - 'tcl' => 'application/x-tcl', - 'td' => 'application/urc-targetdesc+xml', - 'teacher' => 'application/vnd.smart.teacher', - 'tei' => 'application/tei+xml', - 'teicorpus' => 'application/tei+xml', - 'tex' => 'application/x-tex', - 'texi' => 'application/x-texinfo', - 'texinfo' => 'application/x-texinfo', - 'text' => 'text/plain', - 'tfi' => 'application/thraud+xml', - 'tfm' => 'application/x-tex-tfm', - 'tfx' => 'image/tiff-fx', - 'tga' => 'image/x-tga', - 'thmx' => 'application/vnd.ms-officetheme', - 'tif' => 'image/tiff', - 'tiff' => 'image/tiff', - 'tk' => 'application/x-tcl', - 'tmo' => 'application/vnd.tmobile-livetv', - 'toml' => 'application/toml', - 'torrent' => 'application/x-bittorrent', - 'tpl' => 'application/vnd.groove-tool-template', - 'tpt' => 'application/vnd.trid.tpt', - 'tr' => 'text/troff', - 'tra' => 'application/vnd.trueapp', - 'trig' => 'application/trig', - 'trm' => 'application/x-msterminal', - 'ts' => 'video/mp2t', - 'tsd' => 'application/timestamped-data', - 'tsv' => 'text/tab-separated-values', - 'ttc' => 'font/collection', - 'ttf' => 'font/ttf', - 'ttl' => 'text/turtle', - 'ttml' => 'application/ttml+xml', - 'twd' => 'application/vnd.simtech-mindmapper', - 'twds' => 'application/vnd.simtech-mindmapper', - 'txd' => 'application/vnd.genomatix.tuxedo', - 'txf' => 'application/vnd.mobius.txf', - 'txt' => 'text/plain', - 'u32' => 'application/x-authorware-bin', - 'u3d' => 'model/u3d', - 'u8dsn' => 'message/global-delivery-status', - 'u8hdr' => 'message/global-headers', - 'u8mdn' => 'message/global-disposition-notification', - 'u8msg' => 'message/global', - 'ubj' => 'application/ubjson', - 'udeb' => 'application/x-debian-package', - 'ufd' => 'application/vnd.ufdl', - 'ufdl' => 'application/vnd.ufdl', - 'ulx' => 'application/x-glulx', - 'umj' => 'application/vnd.umajin', - 'unityweb' => 'application/vnd.unity', - 'uo' => 'application/vnd.uoml+xml', - 'uoml' => 'application/vnd.uoml+xml', - 'uri' => 'text/uri-list', - 'uris' => 'text/uri-list', - 'urls' => 'text/uri-list', - 'usda' => 'model/vnd.usda', - 'usdz' => 'model/vnd.usdz+zip', - 'ustar' => 'application/x-ustar', - 'utz' => 'application/vnd.uiq.theme', - 'uu' => 'text/x-uuencode', - 'uva' => 'audio/vnd.dece.audio', - 'uvd' => 'application/vnd.dece.data', - 'uvf' => 'application/vnd.dece.data', - 'uvg' => 'image/vnd.dece.graphic', - 'uvh' => 'video/vnd.dece.hd', - 'uvi' => 'image/vnd.dece.graphic', - 'uvm' => 'video/vnd.dece.mobile', - 'uvp' => 'video/vnd.dece.pd', - 'uvs' => 'video/vnd.dece.sd', - 'uvt' => 'application/vnd.dece.ttml+xml', - 'uvu' => 'video/vnd.uvvu.mp4', - 'uvv' => 'video/vnd.dece.video', - 'uvva' => 'audio/vnd.dece.audio', - 'uvvd' => 'application/vnd.dece.data', - 'uvvf' => 'application/vnd.dece.data', - 'uvvg' => 'image/vnd.dece.graphic', - 'uvvh' => 'video/vnd.dece.hd', - 'uvvi' => 'image/vnd.dece.graphic', - 'uvvm' => 'video/vnd.dece.mobile', - 'uvvp' => 'video/vnd.dece.pd', - 'uvvs' => 'video/vnd.dece.sd', - 'uvvt' => 'application/vnd.dece.ttml+xml', - 'uvvu' => 'video/vnd.uvvu.mp4', - 'uvvv' => 'video/vnd.dece.video', - 'uvvx' => 'application/vnd.dece.unspecified', - 'uvvz' => 'application/vnd.dece.zip', - 'uvx' => 'application/vnd.dece.unspecified', - 'uvz' => 'application/vnd.dece.zip', - 'vbox' => 'application/x-virtualbox-vbox', - 'vbox-extpack' => 'application/x-virtualbox-vbox-extpack', - 'vcard' => 'text/vcard', - 'vcd' => 'application/x-cdlink', - 'vcf' => 'text/x-vcard', - 'vcg' => 'application/vnd.groove-vcard', - 'vcs' => 'text/x-vcalendar', - 'vcx' => 'application/vnd.vcx', - 'vdi' => 'application/x-virtualbox-vdi', - 'vds' => 'model/vnd.sap.vds', - 'vhd' => 'application/x-virtualbox-vhd', - 'vis' => 'application/vnd.visionary', - 'viv' => 'video/vnd.vivo', - 'vmdk' => 'application/x-virtualbox-vmdk', - 'vob' => 'video/x-ms-vob', - 'vor' => 'application/vnd.stardivision.writer', - 'vox' => 'application/x-authorware-bin', - 'vrml' => 'model/vrml', - 'vsd' => 'application/vnd.visio', - 'vsf' => 'application/vnd.vsf', - 'vss' => 'application/vnd.visio', - 'vst' => 'application/vnd.visio', - 'vsw' => 'application/vnd.visio', - 'vtf' => 'image/vnd.valve.source.texture', - 'vtt' => 'text/vtt', - 'vtu' => 'model/vnd.vtu', - 'vxml' => 'application/voicexml+xml', - 'w3d' => 'application/x-director', - 'wad' => 'application/x-doom', - 'wadl' => 'application/vnd.sun.wadl+xml', - 'war' => 'application/java-archive', - 'wasm' => 'application/wasm', - 'wav' => 'audio/x-wav', - 'wax' => 'audio/x-ms-wax', - 'wbmp' => 'image/vnd.wap.wbmp', - 'wbs' => 'application/vnd.criticaltools.wbs+xml', - 'wbxml' => 'application/vnd.wap.wbxml', - 'wcm' => 'application/vnd.ms-works', - 'wdb' => 'application/vnd.ms-works', - 'wdp' => 'image/vnd.ms-photo', - 'weba' => 'audio/webm', - 'webapp' => 'application/x-web-app-manifest+json', - 'webm' => 'video/webm', - 'webmanifest' => 'application/manifest+json', - 'webp' => 'image/webp', - 'wg' => 'application/vnd.pmi.widget', - 'wgsl' => 'text/wgsl', - 'wgt' => 'application/widget', - 'wif' => 'application/watcherinfo+xml', - 'wks' => 'application/vnd.ms-works', - 'wm' => 'video/x-ms-wm', - 'wma' => 'audio/x-ms-wma', - 'wmd' => 'application/x-ms-wmd', - 'wmf' => 'image/wmf', - 'wml' => 'text/vnd.wap.wml', - 'wmlc' => 'application/vnd.wap.wmlc', - 'wmls' => 'text/vnd.wap.wmlscript', - 'wmlsc' => 'application/vnd.wap.wmlscriptc', - 'wmv' => 'video/x-ms-wmv', - 'wmx' => 'video/x-ms-wmx', - 'wmz' => 'application/x-msmetafile', - 'woff' => 'font/woff', - 'woff2' => 'font/woff2', - 'wpd' => 'application/vnd.wordperfect', - 'wpl' => 'application/vnd.ms-wpl', - 'wps' => 'application/vnd.ms-works', - 'wqd' => 'application/vnd.wqd', - 'wri' => 'application/x-mswrite', - 'wrl' => 'model/vrml', - 'wsc' => 'message/vnd.wfa.wsc', - 'wsdl' => 'application/wsdl+xml', - 'wspolicy' => 'application/wspolicy+xml', - 'wtb' => 'application/vnd.webturbo', - 'wvx' => 'video/x-ms-wvx', - 'x32' => 'application/x-authorware-bin', - 'x3d' => 'model/x3d+xml', - 'x3db' => 'model/x3d+fastinfoset', - 'x3dbz' => 'model/x3d+binary', - 'x3dv' => 'model/x3d-vrml', - 'x3dvz' => 'model/x3d+vrml', - 'x3dz' => 'model/x3d+xml', - 'x_b' => 'model/vnd.parasolid.transmit.binary', - 'x_t' => 'model/vnd.parasolid.transmit.text', - 'xaml' => 'application/xaml+xml', - 'xap' => 'application/x-silverlight-app', - 'xar' => 'application/vnd.xara', - 'xav' => 'application/xcap-att+xml', - 'xbap' => 'application/x-ms-xbap', - 'xbd' => 'application/vnd.fujixerox.docuworks.binder', - 'xbm' => 'image/x-xbitmap', - 'xca' => 'application/xcap-caps+xml', - 'xcs' => 'application/calendar+xml', - 'xdf' => 'application/xcap-diff+xml', - 'xdm' => 'application/vnd.syncml.dm+xml', - 'xdp' => 'application/vnd.adobe.xdp+xml', - 'xdssc' => 'application/dssc+xml', - 'xdw' => 'application/vnd.fujixerox.docuworks', - 'xel' => 'application/xcap-el+xml', - 'xenc' => 'application/xenc+xml', - 'xer' => 'application/patch-ops-error+xml', - 'xfdf' => 'application/xfdf', - 'xfdl' => 'application/vnd.xfdl', - 'xht' => 'application/xhtml+xml', - 'xhtm' => 'application/vnd.pwg-xhtml-print+xml', - 'xhtml' => 'application/xhtml+xml', - 'xhvml' => 'application/xv+xml', - 'xif' => 'image/vnd.xiff', - 'xla' => 'application/vnd.ms-excel', - 'xlam' => 'application/vnd.ms-excel.addin.macroenabled.12', - 'xlc' => 'application/vnd.ms-excel', - 'xlf' => 'application/xliff+xml', - 'xlm' => 'application/vnd.ms-excel', - 'xls' => 'application/vnd.ms-excel', - 'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroenabled.12', - 'xlsm' => 'application/vnd.ms-excel.sheet.macroenabled.12', - 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'xlt' => 'application/vnd.ms-excel', - 'xltm' => 'application/vnd.ms-excel.template.macroenabled.12', - 'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', - 'xlw' => 'application/vnd.ms-excel', - 'xm' => 'audio/xm', - 'xml' => 'text/xml', - 'xns' => 'application/xcap-ns+xml', - 'xo' => 'application/vnd.olpc-sugar', - 'xop' => 'application/xop+xml', - 'xpi' => 'application/x-xpinstall', - 'xpl' => 'application/xproc+xml', - 'xpm' => 'image/x-xpixmap', - 'xpr' => 'application/vnd.is-xpr', - 'xps' => 'application/vnd.ms-xpsdocument', - 'xpw' => 'application/vnd.intercon.formnet', - 'xpx' => 'application/vnd.intercon.formnet', - 'xsd' => 'application/xml', - 'xsf' => 'application/prs.xsf+xml', - 'xsl' => 'application/xslt+xml', - 'xslt' => 'application/xslt+xml', - 'xsm' => 'application/vnd.syncml+xml', - 'xspf' => 'application/xspf+xml', - 'xul' => 'application/vnd.mozilla.xul+xml', - 'xvm' => 'application/xv+xml', - 'xvml' => 'application/xv+xml', - 'xwd' => 'image/x-xwindowdump', - 'xyz' => 'chemical/x-xyz', - 'xz' => 'application/x-xz', - 'yaml' => 'text/yaml', - 'yang' => 'application/yang', - 'yin' => 'application/yin+xml', - 'yml' => 'text/yaml', - 'ymp' => 'text/x-suse-ymp', - 'z1' => 'application/x-zmachine', - 'z2' => 'application/x-zmachine', - 'z3' => 'application/x-zmachine', - 'z4' => 'application/x-zmachine', - 'z5' => 'application/x-zmachine', - 'z6' => 'application/x-zmachine', - 'z7' => 'application/x-zmachine', - 'z8' => 'application/x-zmachine', - 'zaz' => 'application/vnd.zzazz.deck+xml', - 'zip' => 'application/zip', - 'zir' => 'application/vnd.zul', - 'zirz' => 'application/vnd.zul', - 'zmm' => 'application/vnd.handheld-entertainment+xml', - ]; - } -} \ No newline at end of file diff --git a/src/Request.php b/src/Request.php new file mode 100644 index 0000000..7bac4f2 --- /dev/null +++ b/src/Request.php @@ -0,0 +1,106 @@ +set_method( $request->get_method() ); + $this->set_url_params( $request->get_url_params() ); + $this->set_query_params( $request->get_query_params() ); + $this->set_body_params( $request->get_body_params() ); + $this->set_file_params( $request->get_file_params() ); + $this->set_default_params( $request->get_default_params() ); + $this->set_headers( $request->get_headers() ); + $this->set_body( $request->get_body() ); + $this->set_route( $request->get_route() ); + $this->set_attributes( $request->get_attributes() ); + } + + /** + * Backward-compatible validation method and primary entry point for simple arrays of rules. + * Instantiates the new Validation engine under the hood. + * + * @param array $rules + * @param bool $throw_errors + * @return array + * @throws Exception + */ + public function validate( array $rules, bool $throw_errors = true ) { + $validation = $this->make( $this, $rules ); + + if ( $throw_errors ) { + $validation->throw_if_fails(); + } + + // Keep backwards compatibility for $this->errors state + $this->errors = $validation->errors(); + + return $this->errors; + } + + /** + * Determine if the request has any validation errors. + * + * @return bool + */ + public function fails(): bool { + return ! empty( $this->errors ); + } + + /** + * Determine if the request has no validation errors. + * + * @return bool + */ + public function passes(): bool { + return empty( $this->errors ); + } + + /** + * Create a new Validation instance to evaluate. + * Useful for fluent checking without throwing exceptions automatically. + * + * @param WP_REST_Request $request + * @param array $rules + * @param array $messages + * @param array $custom_attributes + * @return Validation + */ + public function make( WP_REST_Request $request, array $rules, array $messages = [], array $custom_attributes = [] ): Validation { + return new Validation( $request, $rules, $messages, $custom_attributes ); + } +} diff --git a/src/Rule.php b/src/Rule.php new file mode 100644 index 0000000..60c9d4d --- /dev/null +++ b/src/Rule.php @@ -0,0 +1,185 @@ + Rules\Accepted::class, + 'required' => Rules\Required::class, + 'required_if' => Rules\RequiredIf::class, + 'prohibited_unless' => Rules\ProhibitedUnless::class, + 'date' => Rules\Date::class, + 'date_equals' => Rules\DateEquals::class, + 'before' => Rules\Before::class, + 'before_or_equal' => Rules\BeforeOrEqual::class, + 'after' => Rules\After::class, + 'after_or_equal' => Rules\AfterOrEqual::class, + 'confirmed' => Rules\Confirmed::class, + 'bail' => Rules\Bail::class, + 'file' => Rules\File::class, + 'mimes' => Rules\Mimes::class, + 'mimetypes' => Rules\Mimetypes::class, + 'image' => Rules\Image::class, + 'min' => Rules\Min::class, + 'max' => Rules\Max::class, + 'between' => Rules\Between::class, + 'same' => Rules\Same::class, + 'different' => Rules\Different::class, + 'size' => Rules\Size::class, + 'digits' => Rules\Digits::class, + 'digits_between' => Rules\DigitsBetween::class, + 'in' => Rules\In::class, + 'not_in' => Rules\NotIn::class, + 'email' => Rules\Email::class, + 'url' => Rules\Url::class, + 'uuid' => Rules\Uuid::class, + 'regex' => Rules\Regex::class, + 'not_regex' => Rules\NotRegex::class, + 'alpha' => Rules\Alpha::class, + 'alpha_dash' => Rules\AlphaDash::class, + 'alpha_num' => Rules\AlphaNum::class, + 'mac_address' => Rules\MacAddress::class, + 'numeric' => Rules\Numeric::class, + 'integer' => Rules\Integer::class, + 'boolean' => Rules\Boolean::class, + 'json' => Rules\Json::class, + 'timezone' => Rules\Timezone::class, + 'ip' => Rules\Ip::class, + 'ipv4' => Rules\Ipv4::class, + 'ipv6' => Rules\Ipv6::class, + 'starts_with' => Rules\StartsWith::class, + 'ends_with' => Rules\EndsWith::class, + 'array' => Rules\ArrayRule::class, + 'string' => Rules\StringRule::class, + ]; + + /** + * Cached instances of stateless rules. + * + * @var array + */ + protected static array $cache = []; + + /** + * Resolve a rule string to a Rule instance. + * + * @param string $rule + * @param array $parameters + * @return \WpMVC\RequestValidator\Contracts\Rule|null + */ + public static function resolve( string $rule, array $parameters = [] ) { + if ( ! isset( self::$rules[$rule] ) ) { + return null; + } + + $class = self::$rules[$rule]; + + // Some rules might need special instantiation or multiple parameters + switch ( $rule ) { + case 'between': + case 'digits_between': + return new $class( $parameters[0], $parameters[1] ); + case 'date_equals': + case 'before': + case 'before_or_equal': + case 'after': + case 'after_or_equal': + return new $class( $parameters[0], $parameters[1] ?? 'Y-m-d' ); + case 'date': + return new $class( $parameters[0] ?? 'Y-m-d' ); + case 'required_if': + return new $class( $parameters[0], $parameters[1] ); + case 'prohibited_unless': + return new $class( $parameters[0], array_slice( $parameters, 1 ) ); + case 'mimes': + case 'mimetypes': + case 'in': + case 'not_in': + case 'starts_with': + case 'ends_with': + return new $class( $parameters ); + case 'min': + case 'max': + case 'size': + case 'digits': + case 'regex': + case 'not_regex': + return new $class( $parameters[0] ); + default: + if ( empty( $parameters ) ) { + if ( ! isset( self::$cache[$rule] ) ) { + self::$cache[$rule] = new $class(); + } + return self::$cache[$rule]; + } + return new $class( ...$parameters ); + } + } +} diff --git a/src/Rules/Accepted.php b/src/Rules/Accepted.php new file mode 100644 index 0000000..6a1d735 --- /dev/null +++ b/src/Rules/Accepted.php @@ -0,0 +1,32 @@ +date = $date; + $this->format = $format; + } + + public function passes( string $attribute, $value ): bool { + if ( empty( $value ) || ! $this->is_it_valid_date( $value, $this->format ) ) { + return false; + } + + $timestamp = $this->get_timestamp( $this->date, $this->format ); + $input_timestamp = $this->get_timestamp( $value, $this->format ); + + return $input_timestamp > $timestamp; + } + + protected function default_message(): string { + /* translators: 1: attribute name, 2: date */ + return sprintf( __( 'The %1$s must be a date after %2$s.', 'wpmvc' ), ':attribute', ':date' ); + } + + public function replace_placeholders( string $message ): string { + return str_replace( ':date', $this->date, $message ); + } +} diff --git a/src/Rules/AfterOrEqual.php b/src/Rules/AfterOrEqual.php new file mode 100644 index 0000000..999b665 --- /dev/null +++ b/src/Rules/AfterOrEqual.php @@ -0,0 +1,54 @@ +date = $date; + $this->format = $format; + } + + public static function get_name(): string { + return 'after_or_equal'; + } + + public function passes( string $attribute, $value ): bool { + if ( empty( $value ) || ! $this->is_it_valid_date( $value, $this->format ) ) { + return false; + } + + $timestamp = $this->get_timestamp( $this->date, $this->format ); + $input_timestamp = $this->get_timestamp( $value, $this->format ); + + return $input_timestamp >= $timestamp; + } + + protected function default_message(): string { + /* translators: 1: attribute name, 2: date */ + return sprintf( __( 'The %1$s must be a date after or equal to %2$s.', 'wpmvc' ), ':attribute', ':date' ); + } + + public function replace_placeholders( string $message ): string { + return str_replace( ':date', $this->date, $message ); + } +} diff --git a/src/Rules/Alpha.php b/src/Rules/Alpha.php new file mode 100644 index 0000000..7831df9 --- /dev/null +++ b/src/Rules/Alpha.php @@ -0,0 +1,32 @@ +date = $date; + $this->format = $format; + } + + public static function get_name(): string { + return 'before'; + } + + public function passes( string $attribute, $value ): bool { + if ( empty( $value ) || ! $this->is_it_valid_date( $value, $this->format ) ) { + return false; + } + + $timestamp = $this->get_timestamp( $this->date, $this->format ); + $input_timestamp = $this->get_timestamp( $value, $this->format ); + + return $input_timestamp < $timestamp; + } + + protected function default_message(): string { + /* translators: 1: attribute name, 2: date */ + return sprintf( __( 'The %1$s must be a date before %2$s.', 'wpmvc' ), ':attribute', ':date' ); + } + + public function replace_placeholders( string $message ): string { + return str_replace( ':date', $this->date, $message ); + } +} diff --git a/src/Rules/BeforeOrEqual.php b/src/Rules/BeforeOrEqual.php new file mode 100644 index 0000000..cd00a2b --- /dev/null +++ b/src/Rules/BeforeOrEqual.php @@ -0,0 +1,54 @@ +date = $date; + $this->format = $format; + } + + public static function get_name(): string { + return 'before_or_equal'; + } + + public function passes( string $attribute, $value ): bool { + if ( empty( $value ) || ! $this->is_it_valid_date( $value, $this->format ) ) { + return false; + } + + $timestamp = $this->get_timestamp( $this->date, $this->format ); + $input_timestamp = $this->get_timestamp( $value, $this->format ); + + return $input_timestamp <= $timestamp; + } + + protected function default_message(): string { + /* translators: 1: attribute name, 2: date */ + return sprintf( __( 'The %1$s must be a date before or equal to %2$s.', 'wpmvc' ), ':attribute', ':date' ); + } + + public function replace_placeholders( string $message ): string { + return str_replace( ':date', $this->date, $message ); + } +} diff --git a/src/Rules/Between.php b/src/Rules/Between.php new file mode 100644 index 0000000..08a6c66 --- /dev/null +++ b/src/Rules/Between.php @@ -0,0 +1,100 @@ +min = $min; + $this->max = $max; + } + + public static function get_name(): string { + return 'between'; + } + + public function passes( string $attribute, $value ): bool { + if ( ! $this->validator ) { + return true; + } + + $rules = $this->validator->get_attribute_rules( $attribute ); + + // 1. Check if it's a file + $files = $this->validator->wp_rest_request->get_file_params(); + if ( isset( $files[$attribute] ) && is_array( $files[$attribute] ) && isset( $files[$attribute]['tmp_name'] ) ) { + if ( empty( $files[$attribute]['size'] ) ) { + return true; + } + $size = $files[$attribute]['size'] / 1024; + return $size >= $this->min && $size <= $this->max; + } + + // 2. Check if it's numeric + if ( is_numeric( $value ) && ( in_array( 'numeric', $rules, true ) || in_array( 'integer', $rules, true ) ) ) { + $num = (float) $value; + return $num >= $this->min && $num <= $this->max; + } + + // 3. Check if it's an array + if ( is_array( $value ) || in_array( 'array', $rules, true ) ) { + $count = is_countable( $value ) ? count( $value ) : 0; + if ( ! is_array( $value ) && ! is_countable( $value ) ) { + $count = 0; + } + return $count >= $this->min && $count <= $this->max; + } + + // 4. Fallback to string length + $length = mb_strlen( (string) $value ); + return $length >= $this->min && $length <= $this->max; + } + + protected function default_message(): string { + $type = $this->get_attribute_type(); + + switch ( $type ) { + case 'numeric': + /* translators: 1: attribute name, 2: min value, 3: max value */ + return sprintf( __( 'The %1$s must be between %2$s and %3$s.', 'wpmvc' ), ':attribute', ':min', ':max' ); + case 'file': + /* translators: 1: attribute name, 2: min value, 3: max value */ + return sprintf( __( 'The %1$s must be between %2$s and %3$s kilobytes.', 'wpmvc' ), ':attribute', ':min', ':max' ); + case 'array': + /* translators: 1: attribute name, 2: min items, 3: max items */ + return sprintf( __( 'The %1$s must have between %2$s and %3$s items.', 'wpmvc' ), ':attribute', ':min', ':max' ); + default: + /* translators: 1: attribute name, 2: min characters, 3: max characters */ + return sprintf( __( 'The %1$s must be between %2$s and %3$s characters.', 'wpmvc' ), ':attribute', ':min', ':max' ); + } + } + + public function replace_placeholders( string $message ): string { + return str_replace( [':min', ':max'], [$this->min, $this->max], $message ); + } + + public function get_min() { + return $this->min; + } + + public function get_max() { + return $this->max; + } +} diff --git a/src/Rules/Boolean.php b/src/Rules/Boolean.php new file mode 100644 index 0000000..67d0878 --- /dev/null +++ b/src/Rules/Boolean.php @@ -0,0 +1,32 @@ +validator ) { + return true; + } + + $confirmation_field = "{$attribute}_confirmation"; + $confirmation_value = $this->validator->get_value( $confirmation_field ); + + return $value === $confirmation_value; + } + + protected function default_message(): string { + /* translators: %s: attribute name */ + return sprintf( __( 'The %1$s confirmation does not match.', 'wpmvc' ), ':attribute' ); + } +} diff --git a/src/Rules/Date.php b/src/Rules/Date.php new file mode 100644 index 0000000..590f5c0 --- /dev/null +++ b/src/Rules/Date.php @@ -0,0 +1,40 @@ +format = $format; + } + + public static function get_name(): string { + return 'date'; + } + + public function passes( string $attribute, $value ): bool { + return ! empty( $value ) && $this->is_it_valid_date( $value, $this->format ); + } + + protected function default_message(): string { + /* translators: %s: attribute name */ + return sprintf( __( 'The %1$s is not a valid date.', 'wpmvc' ), ':attribute' ); + } +} diff --git a/src/Rules/DateEquals.php b/src/Rules/DateEquals.php new file mode 100644 index 0000000..692534c --- /dev/null +++ b/src/Rules/DateEquals.php @@ -0,0 +1,54 @@ +date = $date; + $this->format = $format; + } + + public static function get_name(): string { + return 'date_equals'; + } + + public function passes( string $attribute, $value ): bool { + if ( empty( $value ) || ! $this->is_it_valid_date( $value, $this->format ) ) { + return false; + } + + $timestamp = $this->get_timestamp( $this->date, $this->format ); + $input_timestamp = $this->get_timestamp( $value, $this->format ); + + return $input_timestamp === $timestamp; + } + + protected function default_message(): string { + /* translators: 1: attribute name, 2: date */ + return sprintf( __( 'The %1$s must be a date equal to %2$s.', 'wpmvc' ), ':attribute', ':date' ); + } + + public function replace_placeholders( string $message ): string { + return str_replace( ':date', $this->date, $message ); + } +} diff --git a/src/Rules/Different.php b/src/Rules/Different.php new file mode 100644 index 0000000..2feeb9c --- /dev/null +++ b/src/Rules/Different.php @@ -0,0 +1,53 @@ +other_field = $other_field; + } + + public static function get_name(): string { + return 'different'; + } + + public function passes( string $attribute, $value ): bool { + if ( ! $this->validator ) { + return true; + } + + $other_value = $this->validator->get_value( $this->other_field ); + + return $value !== $other_value; + } + + public function replace_placeholders( string $message ): string { + $other_name = $this->validator ? ( $this->validator->custom_attributes[$this->other_field] ?? $this->other_field ) : $this->other_field; + return str_replace( ':other', $other_name, $message ); + } + + protected function default_message(): string { + /* translators: 1: attribute name, 2: other field name */ + return sprintf( __( 'The %1$s and %2$s must be different.', 'wpmvc' ), ':attribute', ':other' ); + } + + public function get_other_field() { + return $this->other_field; + } +} diff --git a/src/Rules/Digits.php b/src/Rules/Digits.php new file mode 100644 index 0000000..5eb21bf --- /dev/null +++ b/src/Rules/Digits.php @@ -0,0 +1,46 @@ +digits = $digits; + } + + public static function get_name(): string { + return 'digits'; + } + + public function passes( string $attribute, $value ): bool { + return is_numeric( $value ) && strlen( (string) $value ) === (int) $this->digits; + } + + protected function default_message(): string { + /* translators: 1: attribute name, 2: digits count */ + return sprintf( __( 'The %1$s must be %2$s digits.', 'wpmvc' ), ':attribute', ':digits' ); + } + + public function replace_placeholders( string $message ): string { + return str_replace( ':digits', $this->digits, $message ); + } + + public function get_digits() { + return $this->digits; + } +} diff --git a/src/Rules/DigitsBetween.php b/src/Rules/DigitsBetween.php new file mode 100644 index 0000000..403d0ee --- /dev/null +++ b/src/Rules/DigitsBetween.php @@ -0,0 +1,54 @@ +min = $min; + $this->max = $max; + } + + public static function get_name(): string { + return 'digits_between'; + } + + public function passes( string $attribute, $value ): bool { + $length = strlen( (string) $value ); + return is_numeric( $value ) && $length >= $this->min && $length <= $this->max; + } + + protected function default_message(): string { + /* translators: 1: attribute name, 2: min digits, 3: max digits */ + return sprintf( __( 'The %1$s must be between %2$s and %3$s digits.', 'wpmvc' ), ':attribute', ':min', ':max' ); + } + + public function replace_placeholders( string $message ): string { + return str_replace( [':min', ':max'], [$this->min, $this->max], $message ); + } + + public function get_min() { + return $this->min; + } + + public function get_max() { + return $this->max; + } +} diff --git a/src/Rules/Email.php b/src/Rules/Email.php new file mode 100644 index 0000000..33d6737 --- /dev/null +++ b/src/Rules/Email.php @@ -0,0 +1,32 @@ +suffixes = $suffixes; + } + + public static function get_name(): string { + return 'ends_with'; + } + + public function passes( string $attribute, $value ): bool { + if ( ! is_string( $value ) ) { + return false; + } + foreach ( $this->suffixes as $suffix ) { + if ( substr( $value, -strlen( $suffix ) ) === $suffix ) { + return true; + } + } + return false; + } + + protected function default_message(): string { + /* translators: 1: attribute name, 2: list of suffixes */ + return sprintf( __( 'The %1$s must end with one of the following: %2$s.', 'wpmvc' ), ':attribute', ':values' ); + } + + public function replace_placeholders( string $message ): string { + return str_replace( ':values', implode( ', ', $this->suffixes ), $message ); + } +} diff --git a/src/Rules/File.php b/src/Rules/File.php new file mode 100644 index 0000000..0240efa --- /dev/null +++ b/src/Rules/File.php @@ -0,0 +1,38 @@ +validator ) { + return true; + } + + $files = $this->validator->wp_rest_request->get_file_params(); + + return ! empty( $files[$attribute] ) && $files[$attribute]['error'] === UPLOAD_ERR_OK; + } + + protected function default_message(): string { + /* translators: %s: attribute name */ + return sprintf( __( 'The %1$s must be a file.', 'wpmvc' ), ':attribute' ); + } +} diff --git a/src/Rules/Image.php b/src/Rules/Image.php new file mode 100644 index 0000000..0d124b9 --- /dev/null +++ b/src/Rules/Image.php @@ -0,0 +1,44 @@ +validator ) { + return true; + } + + $files = $this->validator->wp_rest_request->get_file_params(); + + if ( empty( $files[$attribute] ) ) { + return true; // Use 'required' to fail if empty + } + + $allowed_extensions = [ 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'webp' ]; + + return $this->validator->validate_mime( $files[$attribute], implode( ',', $allowed_extensions ) ); + } + + protected function default_message(): string { + /* translators: %s: attribute name */ + return sprintf( __( 'The %1$s must be an image.', 'wpmvc' ), ':attribute' ); + } +} diff --git a/src/Rules/In.php b/src/Rules/In.php new file mode 100644 index 0000000..a22c251 --- /dev/null +++ b/src/Rules/In.php @@ -0,0 +1,38 @@ +values = $values; + } + + public static function get_name(): string { + return 'in'; + } + + public function passes( string $attribute, $value ): bool { + return in_array( (string) $value, $this->values, true ); + } + + protected function default_message(): string { + /* translators: %s: attribute name */ + return sprintf( __( 'The selected %1$s is invalid.', 'wpmvc' ), ':attribute' ); + } +} diff --git a/src/Rules/Integer.php b/src/Rules/Integer.php new file mode 100644 index 0000000..21b1ba2 --- /dev/null +++ b/src/Rules/Integer.php @@ -0,0 +1,32 @@ +max = $max; + } + + public static function get_name(): string { + return 'max'; + } + + public function passes( string $attribute, $value ): bool { + if ( ! $this->validator ) { + return true; + } + + $rules = $this->validator->get_attribute_rules( $attribute ); + + // 1. Check if it's a file + $files = $this->validator->wp_rest_request->get_file_params(); + if ( isset( $files[$attribute] ) && is_array( $files[$attribute] ) && isset( $files[$attribute]['tmp_name'] ) ) { + if ( empty( $files[$attribute]['size'] ) ) { + return true; + } + return ( $files[$attribute]['size'] / 1024 ) <= $this->max; + } + + // 2. Check if it's numeric + if ( is_numeric( $value ) && ( in_array( 'numeric', $rules, true ) || in_array( 'integer', $rules, true ) ) ) { + return (float) $value <= $this->max; + } + + // 3. Check if it's an array + if ( is_array( $value ) || in_array( 'array', $rules, true ) ) { + return is_countable( $value ) && count( $value ) <= $this->max; + } + + // 4. Fallback to string length + return mb_strlen( (string) $value ) <= $this->max; + } + + protected function default_message(): string { + $type = $this->get_attribute_type(); + + switch ( $type ) { + case 'numeric': + /* translators: 1: attribute name, 2: max value */ + return sprintf( __( 'The %1$s must not be greater than %2$s.', 'wpmvc' ), ':attribute', ':max' ); + case 'file': + /* translators: 1: attribute name, 2: max value */ + return sprintf( __( 'The %1$s must not be greater than %2$s kilobytes.', 'wpmvc' ), ':attribute', ':max' ); + case 'array': + /* translators: 1: attribute name, 2: max items */ + return sprintf( __( 'The %1$s must not have more than %2$s items.', 'wpmvc' ), ':attribute', ':max' ); + default: + /* translators: 1: attribute name, 2: max characters */ + return sprintf( __( 'The %1$s must not be greater than %2$s characters.', 'wpmvc' ), ':attribute', ':max' ); + } + } + + public function replace_placeholders( string $message ): string { + return str_replace( ':max', $this->max, $message ); + } + + public function get_max() { + return $this->max; + } +} diff --git a/src/Rules/Mimes.php b/src/Rules/Mimes.php new file mode 100644 index 0000000..7a1a5a3 --- /dev/null +++ b/src/Rules/Mimes.php @@ -0,0 +1,52 @@ +allowed_extensions = $allowed_extensions; + } + + public static function get_name(): string { + return 'mimes'; + } + + public function passes( string $attribute, $value ): bool { + if ( ! $this->validator ) { + return true; + } + + $files = $this->validator->wp_rest_request->get_file_params(); + + if ( empty( $files[$attribute] ) ) { + return true; + } + + return $this->validator->validate_mime( $files[$attribute], implode( ',', $this->allowed_extensions ) ); + } + + protected function default_message(): string { + /* translators: 1: attribute name, 2: allowed mimes */ + return sprintf( __( 'The %1$s must be a file of type: %2$s.', 'wpmvc' ), ':attribute', ':values' ); + } + + public function replace_placeholders( string $message ): string { + return str_replace( ':values', implode( ', ', $this->allowed_extensions ), $message ); + } +} diff --git a/src/Rules/Mimetypes.php b/src/Rules/Mimetypes.php new file mode 100644 index 0000000..23337c3 --- /dev/null +++ b/src/Rules/Mimetypes.php @@ -0,0 +1,58 @@ +allowed_mimetypes = $allowed_mimetypes; + } + + public static function get_name(): string { + return 'mimetypes'; + } + + public function passes( string $attribute, $value ): bool { + if ( ! $this->validator ) { + return true; + } + + $files = $this->validator->wp_rest_request->get_file_params(); + + if ( empty( $files[$attribute] ) || empty( $files[$attribute]['tmp_name'] ) ) { + return true; // Use 'required' to fail if empty + } + + $file_mime_type = mime_content_type( $files[$attribute]['tmp_name'] ); + + return in_array( $file_mime_type, $this->allowed_mimetypes, true ); + } + + protected function default_message(): string { + /* translators: 1: attribute name, 2: allowed mimetypes */ + return sprintf( __( 'The %1$s must be a file of type: %2$s.', 'wpmvc' ), ':attribute', ':values' ); + } + + public function replace_placeholders( string $message ): string { + return str_replace( ':values', implode( ', ', $this->allowed_mimetypes ), $message ); + } + + public function get_allowed_mimetypes() { + return $this->allowed_mimetypes; + } +} diff --git a/src/Rules/Min.php b/src/Rules/Min.php new file mode 100644 index 0000000..3099eee --- /dev/null +++ b/src/Rules/Min.php @@ -0,0 +1,86 @@ +min = $min; + } + + public static function get_name(): string { + return 'min'; + } + + public function passes( string $attribute, $value ): bool { + if ( ! $this->validator ) { + return true; + } + + $rules = $this->validator->get_attribute_rules( $attribute ); + + // 1. Check if it's a file + $files = $this->validator->wp_rest_request->get_file_params(); + if ( isset( $files[$attribute] ) && is_array( $files[$attribute] ) && isset( $files[$attribute]['tmp_name'] ) ) { + if ( empty( $files[$attribute]['size'] ) ) { + return true; + } + return ( $files[$attribute]['size'] / 1024 ) >= $this->min; + } + + // 2. Check if it's numeric + if ( is_numeric( $value ) && in_array( 'numeric', $rules, true ) || in_array( 'integer', $rules, true ) ) { + return (float) $value >= $this->min; + } + + // 3. Check if it's an array + if ( is_array( $value ) || in_array( 'array', $rules, true ) ) { + return is_countable( $value ) && count( $value ) >= $this->min; + } + + // 4. Fallback to string length + return mb_strlen( (string) $value ) >= $this->min; + } + + protected function default_message(): string { + $type = $this->get_attribute_type(); + + switch ( $type ) { + case 'numeric': + /* translators: 1: attribute name, 2: min value */ + return sprintf( __( 'The %1$s must be at least %2$s.', 'wpmvc' ), ':attribute', ':min' ); + case 'file': + /* translators: 1: attribute name, 2: min value */ + return sprintf( __( 'The %1$s must be at least %2$s kilobytes.', 'wpmvc' ), ':attribute', ':min' ); + case 'array': + /* translators: 1: attribute name, 2: min items */ + return sprintf( __( 'The %1$s must have at least %2$s items.', 'wpmvc' ), ':attribute', ':min' ); + default: + /* translators: 1: attribute name, 2: min characters */ + return sprintf( __( 'The %1$s must be at least %2$s characters.', 'wpmvc' ), ':attribute', ':min' ); + } + } + + public function replace_placeholders( string $message ): string { + return str_replace( ':min', $this->min, $message ); + } + + public function get_min() { + return $this->min; + } +} diff --git a/src/Rules/NotIn.php b/src/Rules/NotIn.php new file mode 100644 index 0000000..c418825 --- /dev/null +++ b/src/Rules/NotIn.php @@ -0,0 +1,38 @@ +values = $values; + } + + public static function get_name(): string { + return 'not_in'; + } + + public function passes( string $attribute, $value ): bool { + return ! in_array( (string) $value, $this->values, true ); + } + + protected function default_message(): string { + /* translators: %s: attribute name */ + return sprintf( __( 'The selected %1$s is invalid.', 'wpmvc' ), ':attribute' ); + } +} diff --git a/src/Rules/NotRegex.php b/src/Rules/NotRegex.php new file mode 100644 index 0000000..0720cb7 --- /dev/null +++ b/src/Rules/NotRegex.php @@ -0,0 +1,38 @@ +pattern = $pattern; + } + + public static function get_name(): string { + return 'not_regex'; + } + + public function passes( string $attribute, $value ): bool { + return is_string( $value ) && ! preg_match( $this->pattern, $value ); + } + + protected function default_message(): string { + /* translators: %s: attribute name */ + return sprintf( __( 'The %1$s format is invalid.', 'wpmvc' ), ':attribute' ); + } +} diff --git a/src/Rules/Numeric.php b/src/Rules/Numeric.php new file mode 100644 index 0000000..ee205fd --- /dev/null +++ b/src/Rules/Numeric.php @@ -0,0 +1,33 @@ +other_field = $other_field; + $this->values = $values; + } + + public static function get_name(): string { + return 'prohibited_unless'; + } + + public function passes( string $attribute, $value ): bool { + if ( ! $this->validator ) { + return true; + } + + $target_value = $this->validator->get_value( $this->other_field ); + + // If the other field's value is in our allowed values, then the field is allowed + if ( in_array( (string) $target_value, array_map( 'strval', $this->values ), true ) ) { + return true; + } + + // Otherwise, it is prohibited (must not be present) + return ! $this->validator->data_has( $attribute ); + } + + protected function default_message(): string { + /* translators: 1: attribute name, 2: other field name, 3: list of values */ + return sprintf( __( 'The %1$s field is prohibited unless %2$s is in %3$s.', 'wpmvc' ), ':attribute', ':other', ':values' ); + } + + public function replace_placeholders( string $message ): string { + $other_name = $this->validator ? ( $this->validator->custom_attributes[$this->other_field] ?? $this->other_field ) : $this->other_field; + return str_replace( [':other', ':values'], [$other_name, implode( ', ', $this->values )], $message ); + } + + public function get_other_field() { + return $this->other_field; + } + + public function get_values() { + return $this->values; + } +} diff --git a/src/Rules/Regex.php b/src/Rules/Regex.php new file mode 100644 index 0000000..83c67ff --- /dev/null +++ b/src/Rules/Regex.php @@ -0,0 +1,38 @@ +pattern = $pattern; + } + + public static function get_name(): string { + return 'regex'; + } + + public function passes( string $attribute, $value ): bool { + return is_string( $value ) && preg_match( $this->pattern, $value ); + } + + protected function default_message(): string { + /* translators: %s: attribute name */ + return sprintf( __( 'The %1$s format is invalid.', 'wpmvc' ), ':attribute' ); + } +} diff --git a/src/Rules/Required.php b/src/Rules/Required.php new file mode 100644 index 0000000..12c5d54 --- /dev/null +++ b/src/Rules/Required.php @@ -0,0 +1,51 @@ +validator ) { + return ! empty( $value ); + } + + if ( ! $this->validator->data_has( $attribute ) ) { + return false; + } + + // ISSUE FIX: Previously, this rule only checked for the existence of the key in the data. + // It now correctly validates that the value is actually non-empty (Laravel-style). + // This ensures nested 'required' rules work as expected when the key exists but is empty. + if ( is_array( $value ) ) { + return count( $value ) > 0; + } + + if ( is_string( $value ) ) { + return trim( $value ) !== ''; + } + + return ! is_null( $value ); + } + + protected function default_message(): string { + /* translators: %s: attribute name */ + return sprintf( __( 'The %1$s field is required.', 'wpmvc' ), ':attribute' ); + } +} diff --git a/src/Rules/RequiredIf.php b/src/Rules/RequiredIf.php new file mode 100644 index 0000000..bbfeaab --- /dev/null +++ b/src/Rules/RequiredIf.php @@ -0,0 +1,64 @@ +other_field = $other_field; + $this->value = $value; + } + + public static function get_name(): string { + return 'required_if'; + } + + public function passes( string $attribute, $value ): bool { + if ( ! $this->validator ) { + return true; + } + + $target_value = $this->validator->get_value( $this->other_field ); + + if ( (string) $target_value === (string) $this->value ) { + return ! $this->validator->data_is_empty( $attribute ); + } + + return true; + } + + protected function default_message(): string { + /* translators: 1: attribute name, 2: other field name, 3: value */ + return sprintf( __( 'The %1$s field is required when %2$s is %3$s.', 'wpmvc' ), ':attribute', ':other', ':value' ); + } + + public function replace_placeholders( string $message ): string { + $other_name = $this->validator ? ( $this->validator->custom_attributes[$this->other_field] ?? $this->other_field ) : $this->other_field; + return str_replace( [':other', ':value'], [$other_name, $this->value], $message ); + } + + public function get_other_field() { + return $this->other_field; + } + + public function get_value() { + return $this->value; + } +} diff --git a/src/Rules/Rule.php b/src/Rules/Rule.php new file mode 100644 index 0000000..6355d0b --- /dev/null +++ b/src/Rules/Rule.php @@ -0,0 +1,124 @@ +validator = $validator; + return $this; + } + + /** + * Set a custom validation error message. + * + * @param string $message + * @return $this + */ + public function message( $message ) { + $this->custom_message = $message; + return $this; + } + + /** + * Replace placeholders in the given message. + * + * @param string $message + * @return string + */ + public function replace_placeholders( string $message ): string { + return $message; + } + + /** + * Get the validation error message. + * + * @return string + */ + public function get_message(): string { + return (string) ( $this->custom_message ?? $this->default_message() ); + } + + /** + * Get the fluently-set custom validation error message. + * + * @return string|null + */ + public function get_custom_message(): ?string { + return $this->custom_message; + } + + /** + * Get the default validation error message. + * + * @return string + */ + protected function default_message(): string { + return ''; + } + + /** + * Get the type of the attribute being validated. + * + * @return string + */ + public function get_attribute_type(): string { + if ( ! $this->validator ) { + return 'string'; + } + + $rules = $this->validator->explode_rules ?? []; + + if ( in_array( 'numeric', $rules, true ) || in_array( 'integer', $rules, true ) ) { + return 'numeric'; + } + + if ( in_array( 'array', $rules, true ) ) { + return 'array'; + } + + if ( in_array( 'file', $rules, true ) ) { + return 'file'; + } + + return 'string'; + } +} diff --git a/src/Rules/Same.php b/src/Rules/Same.php new file mode 100644 index 0000000..fe4d5ba --- /dev/null +++ b/src/Rules/Same.php @@ -0,0 +1,53 @@ +other_field = $other_field; + } + + public static function get_name(): string { + return 'same'; + } + + public function passes( string $attribute, $value ): bool { + if ( ! $this->validator ) { + return true; + } + + $other_value = $this->validator->get_value( $this->other_field ); + + return $value === $other_value; + } + + public function replace_placeholders( string $message ): string { + $other_name = $this->validator ? ( $this->validator->custom_attributes[$this->other_field] ?? $this->other_field ) : $this->other_field; + return str_replace( ':other', $other_name, $message ); + } + + protected function default_message(): string { + /* translators: 1: attribute name, 2: other field name */ + return sprintf( __( 'The %1$s and %2$s must match.', 'wpmvc' ), ':attribute', ':other' ); + } + + public function get_other_field() { + return $this->other_field; + } +} diff --git a/src/Rules/Size.php b/src/Rules/Size.php new file mode 100644 index 0000000..0ac7f05 --- /dev/null +++ b/src/Rules/Size.php @@ -0,0 +1,86 @@ +size = $size; + } + + public static function get_name(): string { + return 'size'; + } + + public function passes( string $attribute, $value ): bool { + if ( ! $this->validator ) { + return true; + } + + $rules = $this->validator->get_attribute_rules( $attribute ); + + // 1. Check if it's a file + $files = $this->validator->wp_rest_request->get_file_params(); + if ( isset( $files[$attribute] ) && is_array( $files[$attribute] ) && isset( $files[$attribute]['tmp_name'] ) ) { + if ( empty( $files[$attribute]['size'] ) ) { + return true; + } + return ( $files[$attribute]['size'] / 1024 ) == $this->size; + } + + // 2. Check if it's numeric + if ( is_numeric( $value ) && ( in_array( 'numeric', $rules, true ) || in_array( 'integer', $rules, true ) ) ) { + return (float) $value == $this->size; + } + + // 3. Check if it's an array + if ( is_array( $value ) || in_array( 'array', $rules, true ) ) { + return is_countable( $value ) && count( $value ) == $this->size; + } + + // 4. Fallback to string length + return mb_strlen( (string) $value ) == $this->size; + } + + protected function default_message(): string { + $type = $this->get_attribute_type(); + + switch ( $type ) { + case 'numeric': + /* translators: 1: attribute name, 2: size */ + return sprintf( __( 'The %1$s must be %2$s.', 'wpmvc' ), ':attribute', ':size' ); + case 'file': + /* translators: 1: attribute name, 2: size */ + return sprintf( __( 'The %1$s must be %2$s kilobytes.', 'wpmvc' ), ':attribute', ':size' ); + case 'array': + /* translators: 1: attribute name, 2: size */ + return sprintf( __( 'The %1$s must contain %2$s items.', 'wpmvc' ), ':attribute', ':size' ); + default: + /* translators: 1: attribute name, 2: size */ + return sprintf( __( 'The %1$s must be %2$s characters.', 'wpmvc' ), ':attribute', ':size' ); + } + } + + public function replace_placeholders( string $message ): string { + return str_replace( ':size', $this->size, $message ); + } + + public function get_size() { + return $this->size; + } +} diff --git a/src/Rules/StartsWith.php b/src/Rules/StartsWith.php new file mode 100644 index 0000000..ecfe608 --- /dev/null +++ b/src/Rules/StartsWith.php @@ -0,0 +1,50 @@ +prefixes = $prefixes; + } + + public static function get_name(): string { + return 'starts_with'; + } + + public function passes( string $attribute, $value ): bool { + if ( ! is_string( $value ) ) { + return false; + } + foreach ( $this->prefixes as $prefix ) { + if ( strpos( $value, $prefix ) === 0 ) { + return true; + } + } + return false; + } + + protected function default_message(): string { + /* translators: 1: attribute name, 2: list of prefixes */ + return sprintf( __( 'The %1$s must start with one of the following: %2$s.', 'wpmvc' ), ':attribute', ':values' ); + } + + public function replace_placeholders( string $message ): string { + return str_replace( ':values', implode( ', ', $this->prefixes ), $message ); + } +} diff --git a/src/Rules/StringRule.php b/src/Rules/StringRule.php new file mode 100644 index 0000000..604b2fb --- /dev/null +++ b/src/Rules/StringRule.php @@ -0,0 +1,32 @@ +wp_rest_request = $wp_rest_request; + $this->rules = $rules; + $this->messages = $messages; + $this->custom_attributes = $custom_attributes; + + $this->validate(); + } + + /** + * Run the validation rules. + * + * @return void + */ + protected function validate() { + foreach ( $this->rules as $input_name => $rule ) { + $this->validate_attribute( $input_name, $rule ); + } + } + + /** + * Validate a given attribute against a set of rules. + * + * @param string $attribute + * @param mixed $rules + * @return void + */ + protected function validate_attribute( string $attribute, $rules ) { + if ( strpos( $attribute, '*' ) !== false ) { + $this->validate_wildcard_attribute( $attribute, $rules ); + return; + } + + $rules_key = is_string( $rules ) ? $rules : null; + if ( $rules_key && ! isset( $this->rule_cache[$rules_key] ) ) { + $this->rule_cache[$rules_key] = explode( '|', $rules ); + } + $explode_rules = $rules_key ? $this->rule_cache[$rules_key] : ( is_array( $rules ) ? $rules : [ $rules ] ); + + // Handle 'sometimes' rule: skip if attribute is missing + if ( in_array( 'sometimes', $explode_rules, true ) && ! $this->data_has( $attribute ) ) { + return; + } + + $this->explode_rules = $explode_rules; + + $value = $this->get_value( $attribute ); + $is_nullable = in_array( 'nullable', $explode_rules, true ); + $is_bail = in_array( 'bail', $explode_rules, true ) || in_array( new Rules\Bail, $explode_rules, false ); + $is_empty = $value === '' || $value === null; + + if ( $is_nullable && $is_empty ) { + return; + } + + foreach ( $explode_rules as $explode_rule ) { + if ( $is_bail && ! empty( $this->errors[$attribute] ) ) { + break; + } + + if ( in_array( $explode_rule, [ 'nullable', 'sometimes', 'bail' ], true ) || $explode_rule instanceof Rules\Bail ) { + continue; + } + + // Skip validation if field is missing and rule is not implicit + if ( ! $this->data_has( $attribute ) && ! $this->is_implicit_rule( $explode_rule ) ) { + continue; + } + + // Skip validation if value is empty and nullable is present + if ( $is_empty && in_array( 'nullable', $explode_rules, true ) && ! $this->is_implicit_rule( $explode_rule ) ) { + continue; + } + + if ( $explode_rule instanceof Contracts\Rule ) { + $this->validate_custom_rule( $attribute, $explode_rule ); + } elseif ( $explode_rule instanceof \Closure ) { + $this->validate_closure_rule( $attribute, $explode_rule ); + } else { + $this->validate_rule( $attribute, $explode_rule ); + } + } + } + + /** + * Validate a wildcard attribute. + * + * @param string $attribute + * @param mixed $rules + * @return void + */ + protected function validate_wildcard_attribute( string $attribute, $rules ) { + if ( empty( $this->flattened_keys ) ) { + $this->flattened_keys = $this->get_all_keys( $this->wp_rest_request->get_params() ); + } + + $pattern = str_replace( '\*', '[^.]+', preg_quote( $attribute, '/' ) ); + + $matched_keys = preg_grep( '/^' . $pattern . '$/', $this->flattened_keys ); + + foreach ( $matched_keys as $matched_key ) { + $this->validate_attribute( $matched_key, $rules ); + } + } + + /** + * Get all keys from an array recursively. + * + * @param array $array + * @param string $prefix + * @param array $keys + * @return array + */ + protected function get_all_keys( array $array, $prefix = '', array &$keys = [] ) { + foreach ( $array as $key => $value ) { + $full_key = $prefix === '' ? $key : "{$prefix}.{$key}"; + $keys[] = $full_key; + if ( is_array( $value ) ) { + $this->get_all_keys( $value, $full_key, $keys ); + } + } + return $keys; + } + + /** + * Get the value of an attribute. + * + * @param string $key + * @return mixed + */ + public function get_value( string $key ) { + $data = $this->wp_rest_request->get_params(); + return $this->data_get( $data, $key ); + } + + /** + * Get an item from an array using "dot" notation. + * + * @param mixed $target + * @param string $key + * @param mixed $default + * @return mixed + */ + protected function data_get( $target, $key, $default = null ) { + if ( is_null( $key ) ) { + return $target; + } + + $segments = $this->segment_cache[$key] ?? ( $this->segment_cache[$key] = explode( '.', $key ) ); + + foreach ( $segments as $segment ) { + if ( is_array( $target ) && array_key_exists( $segment, $target ) ) { + $target = $target[$segment]; + } else { + return $default; + } + } + + return $target; + } + + /** + * Determine if the validation fails. + * + * @return bool + */ + public function fails(): bool { + $this->run_after_hooks(); + return ! empty( $this->errors ); + } + + /** + * Determine if the validation passes. + * + * @return bool + */ + public function passes(): bool { + $this->run_after_hooks(); + return empty( $this->errors ); + } + + /** + * Get the validation errors. + * + * @return array + */ + public function errors(): array { + $this->run_after_hooks(); + return $this->errors; + } + + /** + * Throw an exception if the validation fails. + * + * @return void + * + * @throws Exception + */ + public function throw_if_fails() { + if ( $this->fails() ) { + throw ( new Exception( '', 422 ) )->set_messages( $this->errors ); + } + } + + /** + * Register an "after" validation callback. + * + * @param callable $callback + * @return $this + */ + public function after( callable $callback ): self { + $this->after_callbacks[] = $callback; + + return $this; + } + + /** + * Run all of the "after" validation hooks. + * + * @return void + */ + protected function run_after_hooks() { + if ( $this->hooks_ran ) { + return; + } + + $this->hooks_ran = true; + + foreach ( $this->after_callbacks as $callback ) { + call_user_func( $callback, $this ); + } + } + + /** + * Validate a rule by name. + * + * @param string $input_name + * @param string $rule + * @return void + * + * @throws Exception + */ + protected function validate_rule( string $input_name, string $rule ) { + $rule_explode = explode( ':', $rule, 2 ); + $rule_name = $rule_explode[0]; + $parameters = isset( $rule_explode[1] ) ? explode( ',', $rule_explode[1] ) : []; + + // Resolve via RuleResolver + $rule_instance = RuleResolver::resolve( $rule_name, $parameters ); + + if ( $rule_instance ) { + $this->validate_custom_rule( $input_name, $rule_instance ); + return; + } + + // Fallback for custom methods if they exist (backward compatibility or internal) + $method = "{$rule_name}_validator"; + if ( method_exists( static::class, $method ) ) { + $this->$method( $input_name, isset( $rule_explode[1] ) ? $rule_explode[1] : '' ); + } else { + throw new Exception( + sprintf( + /* translators: %s: rule name */ + __( '%s rule not found' ), $rule_name + ) + ); + } + } + + /** + * Validate an attribute using a custom rule object. + * + * @param string $input_name + * @param Contracts\Rule $rule + * @return void + */ + protected function validate_custom_rule( string $input_name, Contracts\Rule $rule ) { + // Make the rule validator-aware if it's one of our built-in Rule objects + if ( $rule instanceof Rules\Rule ) { + $rule->set_validator( $this ); + } + + if ( ! $this->data_has( $input_name ) ) { + // By default, Custom rules shouldn't fail if empty. They require 'required' usually. + // But we can check empty string to mirror Laravel. + $value = null; + } else { + $value = $this->get_value( $input_name ); + } + + if ( ! $rule->passes( $input_name, $value ) ) { + $rule_name = $rule::get_name(); + + // ISSUE FIX: Hierarchical message lookup. + // 1. Fluent message (set via ->message() on rule object) + // 2. Attribute-specific custom message (e.g., 'email.required') + // 3. Rule-generic custom message (e.g., 'required') + // 4. Type-aware custom message (e.g., 'min.numeric') + // 5. Default message (defined in the rule class) + + $message = null; + + // COMPATIBILITY: We check for 'get_custom_message' to support external custom rules + // that might not extend our base Rule class but still want to provide custom messages. + if ( method_exists( $rule, 'get_custom_message' ) ) { + $message = $rule->get_custom_message(); + } + + if ( empty( $message ) ) { + $custom_key = "{$input_name}.{$rule_name}"; + if ( isset( $this->messages[$custom_key] ) ) { + $message = $this->messages[$custom_key]; + } elseif ( isset( $this->messages[$rule_name] ) ) { + $message = $this->messages[$rule_name]; + } elseif ( in_array( $rule_name, [ 'min', 'max', 'size', 'between' ], true ) ) { + $type = $rule->get_attribute_type(); + if ( isset( $this->messages["{$rule_name}.{$type}"] ) ) { + $message = $this->messages["{$rule_name}.{$type}"]; + } + } + } + + // Fallback to the rule's internal get_message() if no custom message found + if ( empty( $message ) ) { + $message = $rule->get_message(); + } + + $attribute_name = $this->custom_attributes[$input_name] ?? $input_name; + $message = (string) str_replace( ':attribute', $attribute_name, $message ); + $this->errors[$input_name][] = $message; + + // Always run the placeholder replacement logic for rule-specific placeholders (like :min, :max, :date) + $this->apply_custom_rule_message( $input_name, $rule ); + } + } + + /** + * Apply the custom rule message and replace placeholders. + * + * @param string $input_name + * @param Contracts\Rule $rule + * @return void + */ + protected function apply_custom_rule_message( string $input_name, Contracts\Rule $rule ) { + if ( empty( $this->errors[$input_name] ) ) { + return; + } + + // We'll replace placeholders in the last added error message for this input + $message = $this->errors[$input_name][ count( $this->errors[$input_name] ) - 1 ]; + + // Use the rule's own placeholder replacement logic + $message = $rule->replace_placeholders( $message ); + + $this->errors[$input_name][ count( $this->errors[$input_name] ) - 1 ] = $message; + } + + /** + * Validate an attribute using a closure. + * + * @param string $input_name + * @param \Closure $rule + * @return void + */ + protected function validate_closure_rule( string $input_name, \Closure $rule ) { + if ( ! $this->data_has( $input_name ) ) { + $value = null; + } else { + $value = $this->get_value( $input_name ); + } + + $fail = function ( $message ) use ( $input_name ) { + $attribute_name = $this->custom_attributes[$input_name] ?? $input_name; + $message = (string) str_replace( ':attribute', $attribute_name, $message ); + $this->errors[$input_name][] = $message; + }; + + $rule( $input_name, $value, $fail ); + } + + /** + * Determine if the data contains the given key. + * + * @param string $key + * @return bool + */ + public function data_has( string $key ): bool { + $data = $this->wp_rest_request->get_params(); + + $segments = $this->segment_cache[$key] ?? ( $this->segment_cache[$key] = explode( '.', $key ) ); + + foreach ( $segments as $segment ) { + if ( is_array( $data ) && array_key_exists( $segment, $data ) ) { + $data = $data[$segment]; + } else { + // Check files for top-level keys + if ( count( $segments ) === 1 ) { + $files = $this->wp_rest_request->get_file_params(); + return isset( $files[$key] ); + } + return false; + } + } + + return true; + } + + /** + * Determine if a rule is "implicit" (should run even if value is empty). + * + * @param mixed $rule + * @return bool + */ + protected function is_implicit_rule( $rule ): bool { + $implicit_rules = [ 'required', 'required_if', 'required_unless', 'required_with', 'required_with_all', 'required_without', 'required_without_all', 'accepted', 'filled', 'confirmed' ]; + + if ( is_string( $rule ) ) { + $rule_name = explode( ':', $rule )[0]; + return in_array( $rule_name, $implicit_rules, true ); + } + + if ( $rule instanceof Contracts\Rule ) { + return in_array( $rule::get_name(), $implicit_rules, true ); + } + + return false; + } + + /** + * Determine if the given attribute is empty. + * + * @param string $attribute + * @return bool + */ + public function data_is_empty( string $attribute ): bool { + $value = $this->get_value( $attribute ); + return is_null( $value ) || ( is_string( $value ) && trim( $value ) === '' ) || ( is_array( $value ) && count( $value ) === 0 ); + } + + /** + * Get the rules for a given attribute. + * + * @param string $attribute + * @return array + */ + public function get_attribute_rules( string $attribute ): array { + $rules = $this->rules[$attribute] ?? []; + + if ( is_string( $rules ) ) { + return explode( '|', $rules ); + } + + return $rules; + } +} diff --git a/src/Validator.php b/src/Validator.php deleted file mode 100644 index 6aed7e2..0000000 --- a/src/Validator.php +++ /dev/null @@ -1,485 +0,0 @@ -wp_rest_request = $wp_rest_request; - $this->mime = $mime; - } - - public function validate( array $rules, bool $throw_errors = true ) { - foreach ( $rules as $input_name => $rule ) { - $explode_rules = explode( '|', $rule ); - $this->explode_rules = $explode_rules; - - foreach ( $explode_rules as $explode_rule ) { - static::validate_rule( $input_name, $explode_rule ); - } - } - - if ( $throw_errors && ! empty( $this->errors ) ) { - throw (new Exception( '', 422 ))->set_messages( $this->errors ); - } - - return $this->errors; - } - - public function is_fail() { - return ! empty( $this->errors ); - } - - protected function validate_rule( string $input_name, string $rule ) { - $rule_explode = explode( ':', $rule, 2 ); - $method = "{$rule_explode[0]}_validator"; - if ( method_exists( static::class, $method ) ) { - static::$method( $input_name, isset( $rule_explode[1] ) ? $rule_explode[1] : null ); - } else { - throw new \Exception( "{$rule_explode[0]} rule not found" ); - } - } - - protected function required_validator( string $input_name ) { - - if ( $this->wp_rest_request->has_param( $input_name ) ) { - return; - } - - $files = $this->wp_rest_request->get_file_params(); - - if ( ! empty( $files[$input_name] ) ) { - return; - } - - $this->set_error( $input_name, 'required', [':attribute'], [$input_name] ); - } - - protected function string_validator( string $input_name ) { - if ( ! $this->wp_rest_request->has_param( $input_name ) ) { - return; - } - - $value = $this->wp_rest_request->get_param( $input_name ); - - if ( is_string( $value ) ) { - return; - } - - $this->set_error( $input_name, 'string', [':attribute'], [$input_name] ); - } - - protected function max_validator( string $input_name, int $max ) { - - if ( $this->wp_rest_request->has_param( $input_name ) ) { - - $value = $this->wp_rest_request->get_param( $input_name ); - - if ( in_array( 'numeric', $this->explode_rules ) || in_array( 'integer', $this->explode_rules ) ) { - $value = intval( $value ); - - if ( $value > $max ) { - $message_key = 'max.numeric'; - } - } elseif ( in_array( 'array', $this->explode_rules ) ) { - if ( ! is_array( $value ) || count( $value ) > $max ) { - $message_key = 'max.array'; - } - } else { - if ( ! is_string( $value ) || strlen( $value ) > $max ) { - $message_key = 'max.string'; - } - } - - if ( ! empty( $message_key ) ) { - $this->set_error( $input_name, $message_key, [':attribute', ':max'], [$input_name, $max] ); - } - - } elseif ( in_array( 'file', $this->explode_rules ) ) { - - $files = $this->wp_rest_request->get_file_params(); - - if ( empty( $files[$input_name]['size'] ) ) { - return; - } - - $file_size = $files[$input_name]['size'] / 1024; //KB - - if ( $file_size <= $max ) { - return; - } - - $this->set_error( $input_name, 'max.file', [':attribute', ':max'], [$input_name, $max] ); - } - } - - protected function min_validator( string $input_name, int $min ) { - - if ( $this->wp_rest_request->has_param( $input_name ) ) { - - $value = $this->wp_rest_request->get_param( $input_name ); - - if ( in_array( 'numeric', $this->explode_rules ) || in_array( 'integer', $this->explode_rules ) ) { - $value = intval( $value ); - - if ( $value < $min ) { - $message_key = 'min.numeric'; - } - } elseif ( in_array( 'array', $this->explode_rules ) ) { - if ( ! is_array( $value ) || count( $value ) < $min ) { - $message_key = 'min.array'; - } - } else { - if ( ! is_string( $value ) || strlen( $value ) < $min ) { - $message_key = 'min.string'; - } - } - - if ( ! empty( $message_key ) ) { - $this->set_error( $input_name, $message_key, [':attribute', ':min'], [$input_name, $min] ); - } - - } elseif ( in_array( 'file', $this->explode_rules ) ) { - - $files = $this->wp_rest_request->get_file_params(); - - if ( empty( $files[$input_name]['size'] ) ) { - return; - } - - $file_size = $files[$input_name]['size'] / 1024; //KB - - if ( $file_size >= $min ) { - return; - } - - $this->set_error( $input_name, 'min.file', [':attribute', ':min'], [$input_name, $min] ); - } - } - - protected function boolean_validator( string $input_name ) { - if ( ! $this->wp_rest_request->has_param( $input_name ) || is_bool( $this->wp_rest_request->get_param( $input_name ) ) ) { - return; - } - - $this->set_error( $input_name, 'boolean', [':attribute'], [$input_name] ); - } - - protected function uuid_validator( string $input_name ) { - if ( ! $this->wp_rest_request->has_param( $input_name ) || wp_is_uuid( $this->wp_rest_request->get_param( $input_name ) ) ) { - return; - } - - $this->set_error( $input_name, 'uuid', [':attribute'], [$input_name] ); - } - - protected function url_validator( string $input_name ) { - if ( ! $this->wp_rest_request->has_param( $input_name ) || filter_var( $this->wp_rest_request->get_param( $input_name ), FILTER_VALIDATE_URL ) ) { - return; - } - - $this->set_error( $input_name, 'url', [':attribute'], [$input_name] ); - } - - protected function mac_address_validator( string $input_name ) { - if ( ! $this->wp_rest_request->has_param( $input_name ) ) { - return; - } - - $value = $this->wp_rest_request->get_param( $input_name ); - - if ( is_string( $value ) && preg_match( '/^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/', $value ) ) { - return; - } - - $this->set_error( $input_name, 'mac_address', [':attribute'], [$input_name] ); - } - - protected function email_validator( string $input_name ) { - if ( ! $this->wp_rest_request->has_param( $input_name ) ) { - return; - } - - $value = $this->wp_rest_request->get_param( $input_name ); - - if ( is_string( $value ) && is_email( $value ) ) { - return; - } - - $this->set_error( $input_name, 'email', [':attribute'], [$input_name] ); - } - - protected function array_validator( string $input_name ) { - if ( ! $this->wp_rest_request->has_param( $input_name ) || is_array( $this->wp_rest_request->get_param( $input_name ) ) ) { - return; - } - - $this->set_error( $input_name, 'array', [':attribute'], [$input_name] ); - } - - protected function numeric_validator( string $input_name ) { - if ( ! $this->wp_rest_request->has_param( $input_name ) || is_numeric( $this->wp_rest_request->get_param( $input_name ) ) ) { - return; - } - - $this->set_error( $input_name, 'numeric', [':attribute'], [$input_name] ); - } - - protected function integer_validator( string $input_name ) { - if ( ! $this->wp_rest_request->has_param( $input_name ) || is_int( $this->wp_rest_request->get_param( $input_name ) ) ) { - return; - } - - $this->set_error( $input_name, 'integer', [':attribute'], [$input_name] ); - } - - protected function file_validator( string $input_name ) { - $files = $this->wp_rest_request->get_file_params(); - - if ( ! isset( $files[$input_name] ) ) { - return; - } - - if ( ! empty( $files[$input_name]['size'] ) ) { - return; - } - - $this->set_error( $input_name, 'file', [':attribute'], [$input_name] ); - } - - public function confirmed_validator( string $input_name ) { - $value1 = $this->wp_rest_request->get_param( $input_name ); - $value2 = $this->wp_rest_request->get_param( "{$input_name}_confirmation" ); - - if ( $value1 === $value2 ) { - return; - } - - $this->set_error( $input_name, 'confirmed', [':attribute'], [$input_name] ); - } - - protected function mimes_validator( string $input_name, string $mimes ) { - $files = $this->wp_rest_request->get_file_params(); - - if ( empty( $files[$input_name] ) || $this->mime->validate( $files[$input_name], $mimes ) ) { - return; - } - - $this->set_error( $input_name, 'mimes', [':attribute', ':values'], [$input_name, $mimes] ); - } - - protected function json_validator( string $input_name ) { - if ( ! $this->wp_rest_request->has_param( $input_name ) ) { - return; - } - - $value = $this->wp_rest_request->get_param( $input_name ); - - if ( is_string( $value ) ) { - json_decode( $value ); - if ( json_last_error() === JSON_ERROR_NONE ) { - return; - } - } - - $this->set_error( $input_name, 'json', [':attribute'], [$input_name] ); - } - - protected function accepted_validator( string $input_name, string $items ) { - if ( ! $this->wp_rest_request->has_param( $input_name ) ) { - return; - } - - $value = $this->wp_rest_request->get_param( $input_name ); - $item_array = explode( ',', $items ); - - if ( in_array( $value, $item_array ) ) { - return; - } - - $this->set_error( $input_name, 'accepted', [':attribute', ':value'], [$input_name, $items] ); - } - - private function set_error( string $input_name, string $rule, array $keys, array $values ) { - $message = $this->get_message( $rule ); - $message = (string) str_replace( $keys, $values, $message ); - - $this->errors[$input_name][] = $message; - } - - protected function get_message( $key ) { - $keys = explode( '.', $key ); - $messages = $this->messages(); - foreach ( $keys as $key ) { - if ( ! isset( $messages[$key] ) ) { - return null; - } - $messages = $messages[$key]; - } - - return $messages; - } - - protected function messages() { - return [ - - /* - |-------------------------------------------------------------------------- - | Validation Language Lines - |-------------------------------------------------------------------------- - | - | The following language lines contain the default error messages used by - | the validator class. Some of these rules have multiple versions such - | as the size rules. Feel free to tweak each of these messages here. - | - */ - - 'accepted' => 'The :attribute must be one of :value.', - 'accepted_if' => 'The :attribute must be accepted when :other is :value.', - 'active_url' => 'The :attribute is not a valid URL.', - 'after' => 'The :attribute must be a date after :date.', - 'after_or_equal' => 'The :attribute must be a date after or equal to :date.', - 'alpha' => 'The :attribute must only contain letters.', - 'alpha_dash' => 'The :attribute must only contain letters, numbers, dashes and underscores.', - 'alpha_num' => 'The :attribute must only contain letters and numbers.', - 'array' => 'The :attribute must be an array.', - 'before' => 'The :attribute must be a date before :date.', - 'before_or_equal' => 'The :attribute must be a date before or equal to :date.', - 'between' => [ - 'numeric' => 'The :attribute must be between :min and :max.', - 'file' => 'The :attribute must be between :min and :max kilobytes.', - 'string' => 'The :attribute must be between :min and :max characters.', - 'array' => 'The :attribute must have between :min and :max items.', - ], - 'boolean' => 'The :attribute field must be true or false.', - 'confirmed' => 'The :attribute confirmation does not match.', - 'current_password' => 'The password is incorrect.', - 'date' => 'The :attribute is not a valid date.', - 'date_equals' => 'The :attribute must be a date equal to :date.', - 'date_format' => 'The :attribute does not match the format :format.', - 'declined' => 'The :attribute must be declined.', - 'declined_if' => 'The :attribute must be declined when :other is :value.', - 'different' => 'The :attribute and :other must be different.', - 'digits' => 'The :attribute must be :digits digits.', - 'digits_between' => 'The :attribute must be between :min and :max digits.', - 'dimensions' => 'The :attribute has invalid image dimensions.', - 'distinct' => 'The :attribute field has a duplicate value.', - 'email' => 'The :attribute must be a valid email address.', - 'ends_with' => 'The :attribute must end with one of the following: :values.', - 'enum' => 'The selected :attribute is invalid.', - 'exists' => 'The selected :attribute is invalid.', - 'file' => 'The :attribute must be a file.', - 'filled' => 'The :attribute field must have a value.', - 'gt' => [ - 'numeric' => 'The :attribute must be greater than :value.', - 'file' => 'The :attribute must be greater than :value kilobytes.', - 'string' => 'The :attribute must be greater than :value characters.', - 'array' => 'The :attribute must have more than :value items.', - ], - 'gte' => [ - 'numeric' => 'The :attribute must be greater than or equal to :value.', - 'file' => 'The :attribute must be greater than or equal to :value kilobytes.', - 'string' => 'The :attribute must be greater than or equal to :value characters.', - 'array' => 'The :attribute must have :value items or more.', - ], - 'image' => 'The :attribute must be an image.', - 'in' => 'The selected :attribute is invalid.', - 'in_array' => 'The :attribute field does not exist in :other.', - 'integer' => 'The :attribute must be an integer.', - 'ip' => 'The :attribute must be a valid IP address.', - 'ipv4' => 'The :attribute must be a valid IPv4 address.', - 'ipv6' => 'The :attribute must be a valid IPv6 address.', - 'json' => 'The :attribute must be a valid JSON string.', - 'lt' => [ - 'numeric' => 'The :attribute must be less than :value.', - 'file' => 'The :attribute must be less than :value kilobytes.', - 'string' => 'The :attribute must be less than :value characters.', - 'array' => 'The :attribute must have less than :value items.', - ], - 'lte' => [ - 'numeric' => 'The :attribute must be less than or equal to :value.', - 'file' => 'The :attribute must be less than or equal to :value kilobytes.', - 'string' => 'The :attribute must be less than or equal to :value characters.', - 'array' => 'The :attribute must not have more than :value items.', - ], - 'mac_address' => 'The :attribute must be a valid MAC address.', - 'max' => [ - 'numeric' => 'The :attribute must not be greater than :max.', - 'file' => 'The :attribute must not be greater than :max kilobytes.', - 'string' => 'The :attribute must not be greater than :max characters.', - 'array' => 'The :attribute must not have more than :max items.', - ], - 'mimes' => 'The :attribute must be a file of type: :values.', - 'min' => [ - 'numeric' => 'The :attribute must be at least :min.', - 'file' => 'The :attribute must be at least :min kilobytes.', - 'string' => 'The :attribute must be at least :min characters.', - 'array' => 'The :attribute must have at least :min items.', - ], - 'multiple_of' => 'The :attribute must be a multiple of :value.', - 'not_in' => 'The selected :attribute is invalid.', - 'not_regex' => 'The :attribute format is invalid.', - 'numeric' => 'The :attribute must be a number.', - 'password' => 'The password is incorrect.', - 'present' => 'The :attribute field must be present.', - 'prohibited' => 'The :attribute field is prohibited.', - 'prohibited_if' => 'The :attribute field is prohibited when :other is :value.', - 'prohibited_unless' => 'The :attribute field is prohibited unless :other is in :values.', - 'prohibits' => 'The :attribute field prohibits :other from being present.', - 'regex' => 'The :attribute format is invalid.', - 'required' => 'The :attribute field is required.', - 'required_array_keys' => 'The :attribute field must contain entries for: :values.', - 'required_if' => 'The :attribute field is required when :other is :value.', - 'required_unless' => 'The :attribute field is required unless :other is in :values.', - 'required_with' => 'The :attribute field is required when :values is present.', - 'required_with_all' => 'The :attribute field is required when :values are present.', - 'required_without' => 'The :attribute field is required when :values is not present.', - 'required_without_all' => 'The :attribute field is required when none of :values are present.', - 'same' => 'The :attribute and :other must match.', - 'size' => [ - 'numeric' => 'The :attribute must be :size.', - 'file' => 'The :attribute must be :size kilobytes.', - 'string' => 'The :attribute must be :size characters.', - 'array' => 'The :attribute must contain :size items.', - ], - 'starts_with' => 'The :attribute must start with one of the following: :values.', - 'string' => 'The :attribute must be a string.', - 'timezone' => 'The :attribute must be a valid timezone.', - 'unique' => 'The :attribute has already been taken.', - 'uploaded' => 'The :attribute failed to upload.', - 'url' => 'The :attribute must be a valid URL.', - 'uuid' => 'The :attribute must be a valid UUID.', - ]; - } -} diff --git a/src/mimes.php b/src/mimes.php new file mode 100644 index 0000000..83830c8 --- /dev/null +++ b/src/mimes.php @@ -0,0 +1,1214 @@ + 'application/vnd.lotus-1-2-3', + '1km' => 'application/vnd.1000minds.decision-model+xml', + '3dml' => 'text/vnd.in3d.3dml', + '3ds' => 'image/x-3ds', + '3g2' => 'video/3gpp2', + '3gp' => 'video/3gpp', + '3gpp' => 'video/3gpp', + '3mf' => 'model/3mf', + '7z' => 'application/x-7z-compressed', + 'aab' => 'application/x-authorware-bin', + 'aac' => 'audio/x-aac', + 'aam' => 'application/x-authorware-map', + 'aas' => 'application/x-authorware-seg', + 'abw' => 'application/x-abiword', + 'ac' => 'application/vnd.nokia.n-gage.ac+xml', + 'acc' => 'application/vnd.americandynamics.acc', + 'ace' => 'application/x-ace-compressed', + 'acu' => 'application/vnd.acucobol', + 'acutc' => 'application/vnd.acucorp', + 'adp' => 'audio/adpcm', + 'adts' => 'audio/aac', + 'aep' => 'application/vnd.audiograph', + 'afm' => 'application/x-font-type1', + 'afp' => 'application/vnd.ibm.modcap', + 'age' => 'application/vnd.age', + 'ahead' => 'application/vnd.ahead.space', + 'ai' => 'application/postscript', + 'aif' => 'audio/x-aiff', + 'aifc' => 'audio/x-aiff', + 'aiff' => 'audio/x-aiff', + 'air' => 'application/vnd.adobe.air-application-installer-package+zip', + 'ait' => 'application/vnd.dvb.ait', + 'ami' => 'application/vnd.amiga.ami', + 'aml' => 'application/automationml-aml+xml', + 'amlx' => 'application/automationml-amlx+zip', + 'amr' => 'audio/amr', + 'apk' => 'application/vnd.android.package-archive', + 'apng' => 'image/apng', + 'appcache' => 'text/cache-manifest', + 'appinstaller' => 'application/appinstaller', + 'application' => 'application/x-ms-application', + 'appx' => 'application/appx', + 'appxbundle' => 'application/appxbundle', + 'apr' => 'application/vnd.lotus-approach', + 'arc' => 'application/x-freearc', + 'arj' => 'application/x-arj', + 'asc' => 'application/pgp-signature', + 'asf' => 'video/x-ms-asf', + 'asm' => 'text/x-asm', + 'aso' => 'application/vnd.accpac.simply.aso', + 'asx' => 'video/x-ms-asf', + 'atc' => 'application/vnd.acucorp', + 'atom' => 'application/atom+xml', + 'atomcat' => 'application/atomcat+xml', + 'atomdeleted' => 'application/atomdeleted+xml', + 'atomsvc' => 'application/atomsvc+xml', + 'atx' => 'application/vnd.antix.game-component', + 'au' => 'audio/basic', + 'avci' => 'image/avci', + 'avcs' => 'image/avcs', + 'avi' => 'video/x-msvideo', + 'avif' => 'image/avif', + 'aw' => 'application/applixware', + 'azf' => 'application/vnd.airzip.filesecure.azf', + 'azs' => 'application/vnd.airzip.filesecure.azs', + 'azv' => 'image/vnd.airzip.accelerator.azv', + 'azw' => 'application/vnd.amazon.ebook', + 'b16' => 'image/vnd.pco.b16', + 'bat' => 'application/x-msdownload', + 'bcpio' => 'application/x-bcpio', + 'bdf' => 'application/x-font-bdf', + 'bdm' => 'application/vnd.syncml.dm+wbxml', + 'bdoc' => 'application/x-bdoc', + 'bed' => 'application/vnd.realvnc.bed', + 'bh2' => 'application/vnd.fujitsu.oasysprs', + 'bin' => 'application/octet-stream', + 'blb' => 'application/x-blorb', + 'blorb' => 'application/x-blorb', + 'bmi' => 'application/vnd.bmi', + 'bmml' => 'application/vnd.balsamiq.bmml+xml', + 'bmp' => 'image/x-ms-bmp', + 'book' => 'application/vnd.framemaker', + 'box' => 'application/vnd.previewsystems.box', + 'boz' => 'application/x-bzip2', + 'bpk' => 'application/octet-stream', + 'bsp' => 'model/vnd.valve.source.compiled-map', + 'btf' => 'image/prs.btif', + 'btif' => 'image/prs.btif', + 'buffer' => 'application/octet-stream', + 'bz' => 'application/x-bzip', + 'bz2' => 'application/x-bzip2', + 'c' => 'text/x-c', + 'c11amc' => 'application/vnd.cluetrust.cartomobile-config', + 'c11amz' => 'application/vnd.cluetrust.cartomobile-config-pkg', + 'c4d' => 'application/vnd.clonk.c4group', + 'c4f' => 'application/vnd.clonk.c4group', + 'c4g' => 'application/vnd.clonk.c4group', + 'c4p' => 'application/vnd.clonk.c4group', + 'c4u' => 'application/vnd.clonk.c4group', + 'cab' => 'application/vnd.ms-cab-compressed', + 'caf' => 'audio/x-caf', + 'cap' => 'application/vnd.tcpdump.pcap', + 'car' => 'application/vnd.curl.car', + 'cat' => 'application/vnd.ms-pki.seccat', + 'cb7' => 'application/x-cbr', + 'cba' => 'application/x-cbr', + 'cbr' => 'application/x-cbr', + 'cbt' => 'application/x-cbr', + 'cbz' => 'application/x-cbr', + 'cc' => 'text/x-c', + 'cco' => 'application/x-cocoa', + 'cct' => 'application/x-director', + 'ccxml' => 'application/ccxml+xml', + 'cdbcmsg' => 'application/vnd.contact.cmsg', + 'cdf' => 'application/x-netcdf', + 'cdfx' => 'application/cdfx+xml', + 'cdkey' => 'application/vnd.mediastation.cdkey', + 'cdmia' => 'application/cdmi-capability', + 'cdmic' => 'application/cdmi-container', + 'cdmid' => 'application/cdmi-domain', + 'cdmio' => 'application/cdmi-object', + 'cdmiq' => 'application/cdmi-queue', + 'cdx' => 'chemical/x-cdx', + 'cdxml' => 'application/vnd.chemdraw+xml', + 'cdy' => 'application/vnd.cinderella', + 'cer' => 'application/pkix-cert', + 'cfs' => 'application/x-cfs-compressed', + 'cgm' => 'image/cgm', + 'chat' => 'application/x-chat', + 'chm' => 'application/vnd.ms-htmlhelp', + 'chrt' => 'application/vnd.kde.kchart', + 'cif' => 'chemical/x-cif', + 'cii' => 'application/vnd.anser-web-certificate-issue-initiation', + 'cil' => 'application/vnd.ms-artgalry', + 'cjs' => 'application/node', + 'cla' => 'application/vnd.claymore', + 'class' => 'application/java-vm', + 'cld' => 'model/vnd.cld', + 'clkk' => 'application/vnd.crick.clicker.keyboard', + 'clkp' => 'application/vnd.crick.clicker.palette', + 'clkt' => 'application/vnd.crick.clicker.template', + 'clkw' => 'application/vnd.crick.clicker.wordbank', + 'clkx' => 'application/vnd.crick.clicker', + 'clp' => 'application/x-msclip', + 'cmc' => 'application/vnd.cosmocaller', + 'cmdf' => 'chemical/x-cmdf', + 'cml' => 'chemical/x-cml', + 'cmp' => 'application/vnd.yellowriver-custom-menu', + 'cmx' => 'image/x-cmx', + 'cod' => 'application/vnd.rim.cod', + 'coffee' => 'text/coffeescript', + 'com' => 'application/x-msdownload', + 'conf' => 'text/plain', + 'cpio' => 'application/x-cpio', + 'cpl' => 'application/cpl+xml', + 'cpp' => 'text/x-c', + 'cpt' => 'application/mac-compactpro', + 'crd' => 'application/x-mscardfile', + 'crl' => 'application/pkix-crl', + 'crt' => 'application/x-x509-ca-cert', + 'crx' => 'application/x-chrome-extension', + 'cryptonote' => 'application/vnd.rig.cryptonote', + 'csh' => 'application/x-csh', + 'csl' => 'application/vnd.citationstyles.style+xml', + 'csml' => 'chemical/x-csml', + 'csp' => 'application/vnd.commonspace', + 'css' => 'text/css', + 'cst' => 'application/x-director', + 'csv' => 'text/csv', + 'cu' => 'application/cu-seeme', + 'curl' => 'text/vnd.curl', + 'cwl' => 'application/cwl', + 'cww' => 'application/prs.cww', + 'cxt' => 'application/x-director', + 'cxx' => 'text/x-c', + 'dae' => 'model/vnd.collada+xml', + 'daf' => 'application/vnd.mobius.daf', + 'dart' => 'application/vnd.dart', + 'dataless' => 'application/vnd.fdsn.seed', + 'davmount' => 'application/davmount+xml', + 'dbf' => 'application/vnd.dbf', + 'dbk' => 'application/docbook+xml', + 'dcr' => 'application/x-director', + 'dcurl' => 'text/vnd.curl.dcurl', + 'dd2' => 'application/vnd.oma.dd2+xml', + 'ddd' => 'application/vnd.fujixerox.ddd', + 'ddf' => 'application/vnd.syncml.dmddf+xml', + 'dds' => 'image/vnd.ms-dds', + 'deb' => 'application/x-debian-package', + 'def' => 'text/plain', + 'deploy' => 'application/octet-stream', + 'der' => 'application/x-x509-ca-cert', + 'dfac' => 'application/vnd.dreamfactory', + 'dgc' => 'application/x-dgc-compressed', + 'dib' => 'image/bmp', + 'dic' => 'text/x-c', + 'dir' => 'application/x-director', + 'dis' => 'application/vnd.mobius.dis', + 'disposition-notification' => 'message/disposition-notification', + 'dist' => 'application/octet-stream', + 'distz' => 'application/octet-stream', + 'djv' => 'image/vnd.djvu', + 'djvu' => 'image/vnd.djvu', + 'dll' => 'application/x-msdownload', + 'dmg' => 'application/x-apple-diskimage', + 'dmp' => 'application/vnd.tcpdump.pcap', + 'dms' => 'application/octet-stream', + 'dna' => 'application/vnd.dna', + 'doc' => 'application/msword', + 'docm' => 'application/vnd.ms-word.document.macroenabled.12', + 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'dot' => 'application/msword', + 'dotm' => 'application/vnd.ms-word.template.macroenabled.12', + 'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', + 'dp' => 'application/vnd.osgi.dp', + 'dpg' => 'application/vnd.dpgraph', + 'dpx' => 'image/dpx', + 'dra' => 'audio/vnd.dra', + 'drle' => 'image/dicom-rle', + 'dsc' => 'text/prs.lines.tag', + 'dssc' => 'application/dssc+der', + 'dtb' => 'application/x-dtbook+xml', + 'dtd' => 'application/xml-dtd', + 'dts' => 'audio/vnd.dts', + 'dtshd' => 'audio/vnd.dts.hd', + 'dump' => 'application/octet-stream', + 'dvb' => 'video/vnd.dvb.file', + 'dvi' => 'application/x-dvi', + 'dwd' => 'application/atsc-dwd+xml', + 'dwf' => 'model/vnd.dwf', + 'dwg' => 'image/vnd.dwg', + 'dxf' => 'image/vnd.dxf', + 'dxp' => 'application/vnd.spotfire.dxp', + 'dxr' => 'application/x-director', + 'ear' => 'application/java-archive', + 'ecelp4800' => 'audio/vnd.nuera.ecelp4800', + 'ecelp7470' => 'audio/vnd.nuera.ecelp7470', + 'ecelp9600' => 'audio/vnd.nuera.ecelp9600', + 'ecma' => 'application/ecmascript', + 'edm' => 'application/vnd.novadigm.edm', + 'edx' => 'application/vnd.novadigm.edx', + 'efif' => 'application/vnd.picsel', + 'ei6' => 'application/vnd.pg.osasli', + 'elc' => 'application/octet-stream', + 'emf' => 'image/emf', + 'eml' => 'message/rfc822', + 'emma' => 'application/emma+xml', + 'emotionml' => 'application/emotionml+xml', + 'emz' => 'application/x-msmetafile', + 'eol' => 'audio/vnd.digital-winds', + 'eot' => 'application/vnd.ms-fontobject', + 'eps' => 'application/postscript', + 'epub' => 'application/epub+zip', + 'es3' => 'application/vnd.eszigno3+xml', + 'esa' => 'application/vnd.osgi.subsystem', + 'esf' => 'application/vnd.epson.esf', + 'et3' => 'application/vnd.eszigno3+xml', + 'etx' => 'text/x-setext', + 'eva' => 'application/x-eva', + 'evy' => 'application/x-envoy', + 'exe' => 'application/x-msdownload', + 'exi' => 'application/exi', + 'exp' => 'application/express', + 'exr' => 'image/aces', + 'ext' => 'application/vnd.novadigm.ext', + 'ez' => 'application/andrew-inset', + 'ez2' => 'application/vnd.ezpix-album', + 'ez3' => 'application/vnd.ezpix-package', + 'f' => 'text/x-fortran', + 'f4v' => 'video/x-f4v', + 'f77' => 'text/x-fortran', + 'f90' => 'text/x-fortran', + 'fbs' => 'image/vnd.fastbidsheet', + 'fcdt' => 'application/vnd.adobe.formscentral.fcdt', + 'fcs' => 'application/vnd.isac.fcs', + 'fdf' => 'application/vnd.fdf', + 'fdt' => 'application/fdt+xml', + 'fe_launch' => 'application/vnd.denovo.fcselayout-link', + 'fg5' => 'application/vnd.fujitsu.oasysgp', + 'fgd' => 'application/x-director', + 'fh' => 'image/x-freehand', + 'fh4' => 'image/x-freehand', + 'fh5' => 'image/x-freehand', + 'fh7' => 'image/x-freehand', + 'fhc' => 'image/x-freehand', + 'fig' => 'application/x-xfig', + 'fits' => 'image/fits', + 'flac' => 'audio/x-flac', + 'fli' => 'video/x-fli', + 'flo' => 'application/vnd.micrografx.flo', + 'flv' => 'video/x-flv', + 'flw' => 'application/vnd.kde.kivio', + 'flx' => 'text/vnd.fmi.flexstor', + 'fly' => 'text/vnd.fly', + 'fm' => 'application/vnd.framemaker', + 'fnc' => 'application/vnd.frogans.fnc', + 'fo' => 'application/vnd.software602.filler.form+xml', + 'for' => 'text/x-fortran', + 'fpx' => 'image/vnd.fpx', + 'frame' => 'application/vnd.framemaker', + 'fsc' => 'application/vnd.fsc.weblaunch', + 'fst' => 'image/vnd.fst', + 'ftc' => 'application/vnd.fluxtime.clip', + 'fti' => 'application/vnd.anser-web-funds-transfer-initiation', + 'fvt' => 'video/vnd.fvt', + 'fxp' => 'application/vnd.adobe.fxp', + 'fxpl' => 'application/vnd.adobe.fxp', + 'fzs' => 'application/vnd.fuzzysheet', + 'g2w' => 'application/vnd.geoplan', + 'g3' => 'image/g3fax', + 'g3w' => 'application/vnd.geospace', + 'gac' => 'application/vnd.groove-account', + 'gam' => 'application/x-tads', + 'gbr' => 'application/rpki-ghostbusters', + 'gca' => 'application/x-gca-compressed', + 'gdl' => 'model/vnd.gdl', + 'gdoc' => 'application/vnd.google-apps.document', + 'ged' => 'text/vnd.familysearch.gedcom', + 'geo' => 'application/vnd.dynageo', + 'geojson' => 'application/geo+json', + 'gex' => 'application/vnd.geometry-explorer', + 'ggb' => 'application/vnd.geogebra.file', + 'ggt' => 'application/vnd.geogebra.tool', + 'ghf' => 'application/vnd.groove-help', + 'gif' => 'image/gif', + 'gim' => 'application/vnd.groove-identity-message', + 'glb' => 'model/gltf-binary', + 'gltf' => 'model/gltf+json', + 'gml' => 'application/gml+xml', + 'gmx' => 'application/vnd.gmx', + 'gnumeric' => 'application/x-gnumeric', + 'gph' => 'application/vnd.flographit', + 'gpx' => 'application/gpx+xml', + 'gqf' => 'application/vnd.grafeq', + 'gqs' => 'application/vnd.grafeq', + 'gram' => 'application/srgs', + 'gramps' => 'application/x-gramps-xml', + 'gre' => 'application/vnd.geometry-explorer', + 'grv' => 'application/vnd.groove-injector', + 'grxml' => 'application/srgs+xml', + 'gsf' => 'application/x-font-ghostscript', + 'gsheet' => 'application/vnd.google-apps.spreadsheet', + 'gslides' => 'application/vnd.google-apps.presentation', + 'gtar' => 'application/x-gtar', + 'gtm' => 'application/vnd.groove-tool-message', + 'gtw' => 'model/vnd.gtw', + 'gv' => 'text/vnd.graphviz', + 'gxf' => 'application/gxf', + 'gxt' => 'application/vnd.geonext', + 'gz' => 'application/gzip', + 'h' => 'text/x-c', + 'h261' => 'video/h261', + 'h263' => 'video/h263', + 'h264' => 'video/h264', + 'hal' => 'application/vnd.hal+xml', + 'hbci' => 'application/vnd.hbci', + 'hbs' => 'text/x-handlebars-template', + 'hdd' => 'application/x-virtualbox-hdd', + 'hdf' => 'application/x-hdf', + 'heic' => 'image/heic', + 'heics' => 'image/heic-sequence', + 'heif' => 'image/heif', + 'heifs' => 'image/heif-sequence', + 'hej2' => 'image/hej2k', + 'held' => 'application/atsc-held+xml', + 'hh' => 'text/x-c', + 'hjson' => 'application/hjson', + 'hlp' => 'application/winhlp', + 'hpgl' => 'application/vnd.hp-hpgl', + 'hpid' => 'application/vnd.hp-hpid', + 'hps' => 'application/vnd.hp-hps', + 'hqx' => 'application/mac-binhex40', + 'hsj2' => 'image/hsj2', + 'htc' => 'text/x-component', + 'htke' => 'application/vnd.kenameaapp', + 'htm' => 'text/html', + 'html' => 'text/html', + 'hvd' => 'application/vnd.yamaha.hv-dic', + 'hvp' => 'application/vnd.yamaha.hv-voice', + 'hvs' => 'application/vnd.yamaha.hv-script', + 'i2g' => 'application/vnd.intergeo', + 'icc' => 'application/vnd.iccprofile', + 'ice' => 'x-conference/x-cooltalk', + 'icm' => 'application/vnd.iccprofile', + 'ico' => 'image/x-icon', + 'ics' => 'text/calendar', + 'ief' => 'image/ief', + 'ifb' => 'text/calendar', + 'ifm' => 'application/vnd.shana.informed.formdata', + 'iges' => 'model/iges', + 'igl' => 'application/vnd.igloader', + 'igm' => 'application/vnd.insors.igm', + 'igs' => 'model/iges', + 'igx' => 'application/vnd.micrografx.igx', + 'iif' => 'application/vnd.shana.informed.interchange', + 'img' => 'application/octet-stream', + 'imp' => 'application/vnd.accpac.simply.imp', + 'ims' => 'application/vnd.ms-ims', + 'in' => 'text/plain', + 'ini' => 'text/plain', + 'ink' => 'application/inkml+xml', + 'inkml' => 'application/inkml+xml', + 'install' => 'application/x-install-instructions', + 'iota' => 'application/vnd.astraea-software.iota', + 'ipfix' => 'application/ipfix', + 'ipk' => 'application/vnd.shana.informed.package', + 'irm' => 'application/vnd.ibm.rights-management', + 'irp' => 'application/vnd.irepository.package+xml', + 'iso' => 'application/x-iso9660-image', + 'itp' => 'application/vnd.shana.informed.formtemplate', + 'its' => 'application/its+xml', + 'ivp' => 'application/vnd.immervision-ivp', + 'ivu' => 'application/vnd.immervision-ivu', + 'jad' => 'text/vnd.sun.j2me.app-descriptor', + 'jade' => 'text/jade', + 'jam' => 'application/vnd.jam', + 'jar' => 'application/java-archive', + 'jardiff' => 'application/x-java-archive-diff', + 'java' => 'text/x-java-source', + 'jhc' => 'image/jphc', + 'jisp' => 'application/vnd.jisp', + 'jls' => 'image/jls', + 'jlt' => 'application/vnd.hp-jlyt', + 'jng' => 'image/x-jng', + 'jnlp' => 'application/x-java-jnlp-file', + 'joda' => 'application/vnd.joost.joda-archive', + 'jp2' => 'image/jp2', + 'jpe' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'jpf' => 'image/jpx', + 'jpg' => 'image/jpeg', + 'jpg2' => 'image/jp2', + 'jpgm' => 'video/jpm', + 'jpgv' => 'video/jpeg', + 'jph' => 'image/jph', + 'jpm' => 'video/jpm', + 'jpx' => 'image/jpx', + 'js' => 'text/javascript', + 'json' => 'application/json', + 'json5' => 'application/json5', + 'jsonld' => 'application/ld+json', + 'jsonml' => 'application/jsonml+json', + 'jsx' => 'text/jsx', + 'jt' => 'model/jt', + 'jxr' => 'image/jxr', + 'jxra' => 'image/jxra', + 'jxrs' => 'image/jxrs', + 'jxs' => 'image/jxs', + 'jxsc' => 'image/jxsc', + 'jxsi' => 'image/jxsi', + 'jxss' => 'image/jxss', + 'kar' => 'audio/midi', + 'karbon' => 'application/vnd.kde.karbon', + 'kdbx' => 'application/x-keepass2', + 'key' => 'application/x-iwork-keynote-sffkey', + 'kfo' => 'application/vnd.kde.kformula', + 'kia' => 'application/vnd.kidspiration', + 'kml' => 'application/vnd.google-earth.kml+xml', + 'kmz' => 'application/vnd.google-earth.kmz', + 'kne' => 'application/vnd.kinar', + 'knp' => 'application/vnd.kinar', + 'kon' => 'application/vnd.kde.kontour', + 'kpr' => 'application/vnd.kde.kpresenter', + 'kpt' => 'application/vnd.kde.kpresenter', + 'kpxx' => 'application/vnd.ds-keypoint', + 'ksp' => 'application/vnd.kde.kspread', + 'ktr' => 'application/vnd.kahootz', + 'ktx' => 'image/ktx', + 'ktx2' => 'image/ktx2', + 'ktz' => 'application/vnd.kahootz', + 'kwd' => 'application/vnd.kde.kword', + 'kwt' => 'application/vnd.kde.kword', + 'lasxml' => 'application/vnd.las.las+xml', + 'latex' => 'application/x-latex', + 'lbd' => 'application/vnd.llamagraphics.life-balance.desktop', + 'lbe' => 'application/vnd.llamagraphics.life-balance.exchange+xml', + 'les' => 'application/vnd.hhe.lesson-player', + 'less' => 'text/less', + 'lgr' => 'application/lgr+xml', + 'lha' => 'application/x-lzh-compressed', + 'link66' => 'application/vnd.route66.link66+xml', + 'list' => 'text/plain', + 'list3820' => 'application/vnd.ibm.modcap', + 'listafp' => 'application/vnd.ibm.modcap', + 'litcoffee' => 'text/coffeescript', + 'lnk' => 'application/x-ms-shortcut', + 'log' => 'text/plain', + 'lostxml' => 'application/lost+xml', + 'lrf' => 'application/octet-stream', + 'lrm' => 'application/vnd.ms-lrm', + 'ltf' => 'application/vnd.frogans.ltf', + 'lua' => 'text/x-lua', + 'luac' => 'application/x-lua-bytecode', + 'lvp' => 'audio/vnd.lucent.voice', + 'lwp' => 'application/vnd.lotus-wordpro', + 'lzh' => 'application/x-lzh-compressed', + 'm13' => 'application/x-msmediaview', + 'm14' => 'application/x-msmediaview', + 'm1v' => 'video/mpeg', + 'm21' => 'application/mp21', + 'm2a' => 'audio/mpeg', + 'm2v' => 'video/mpeg', + 'm3a' => 'audio/mpeg', + 'm3u' => 'audio/x-mpegurl', + 'm3u8' => 'application/vnd.apple.mpegurl', + 'm4a' => 'audio/x-m4a', + 'm4p' => 'application/mp4', + 'm4s' => 'video/iso.segment', + 'm4u' => 'video/vnd.mpegurl', + 'm4v' => 'video/x-m4v', + 'ma' => 'application/mathematica', + 'mads' => 'application/mads+xml', + 'maei' => 'application/mmt-aei+xml', + 'mag' => 'application/vnd.ecowin.chart', + 'maker' => 'application/vnd.framemaker', + 'man' => 'text/troff', + 'manifest' => 'text/cache-manifest', + 'map' => 'application/json', + 'mar' => 'application/octet-stream', + 'markdown' => 'text/markdown', + 'mathml' => 'application/mathml+xml', + 'mb' => 'application/mathematica', + 'mbk' => 'application/vnd.mobius.mbk', + 'mbox' => 'application/mbox', + 'mc1' => 'application/vnd.medcalcdata', + 'mcd' => 'application/vnd.mcd', + 'mcurl' => 'text/vnd.curl.mcurl', + 'md' => 'text/markdown', + 'mdb' => 'application/x-msaccess', + 'mdi' => 'image/vnd.ms-modi', + 'mdx' => 'text/mdx', + 'me' => 'text/troff', + 'mesh' => 'model/mesh', + 'meta4' => 'application/metalink4+xml', + 'metalink' => 'application/metalink+xml', + 'mets' => 'application/mets+xml', + 'mfm' => 'application/vnd.mfmp', + 'mft' => 'application/rpki-manifest', + 'mgp' => 'application/vnd.osgeo.mapguide.package', + 'mgz' => 'application/vnd.proteus.magazine', + 'mid' => 'audio/midi', + 'midi' => 'audio/midi', + 'mie' => 'application/x-mie', + 'mif' => 'application/vnd.mif', + 'mime' => 'message/rfc822', + 'mj2' => 'video/mj2', + 'mjp2' => 'video/mj2', + 'mjs' => 'text/javascript', + 'mk3d' => 'video/x-matroska', + 'mka' => 'audio/x-matroska', + 'mkd' => 'text/x-markdown', + 'mks' => 'video/x-matroska', + 'mkv' => 'video/x-matroska', + 'mlp' => 'application/vnd.dolby.mlp', + 'mmd' => 'application/vnd.chipnuts.karaoke-mmd', + 'mmf' => 'application/vnd.smaf', + 'mml' => 'text/mathml', + 'mmr' => 'image/vnd.fujixerox.edmics-mmr', + 'mng' => 'video/x-mng', + 'mny' => 'application/x-msmoney', + 'mobi' => 'application/x-mobipocket-ebook', + 'mods' => 'application/mods+xml', + 'mov' => 'video/quicktime', + 'movie' => 'video/x-sgi-movie', + 'mp2' => 'audio/mpeg', + 'mp21' => 'application/mp21', + 'mp2a' => 'audio/mpeg', + 'mp3' => 'audio/mpeg', + 'mp4' => 'video/mp4', + 'mp4a' => 'audio/mp4', + 'mp4s' => 'application/mp4', + 'mp4v' => 'video/mp4', + 'mpc' => 'application/vnd.mophun.certificate', + 'mpd' => 'application/dash+xml', + 'mpe' => 'video/mpeg', + 'mpeg' => 'video/mpeg', + 'mpf' => 'application/media-policy-dataset+xml', + 'mpg' => 'video/mpeg', + 'mpg4' => 'video/mp4', + 'mpga' => 'audio/mpeg', + 'mpkg' => 'application/vnd.apple.installer+xml', + 'mpm' => 'application/vnd.blueice.multipass', + 'mpn' => 'application/vnd.mophun.application', + 'mpp' => 'application/vnd.ms-project', + 'mpt' => 'application/vnd.ms-project', + 'mpy' => 'application/vnd.ibm.minipay', + 'mqy' => 'application/vnd.mobius.mqy', + 'mrc' => 'application/marc', + 'mrcx' => 'application/marcxml+xml', + 'ms' => 'text/troff', + 'mscml' => 'application/mediaservercontrol+xml', + 'mseed' => 'application/vnd.fdsn.mseed', + 'mseq' => 'application/vnd.mseq', + 'msf' => 'application/vnd.epson.msf', + 'msg' => 'application/vnd.ms-outlook', + 'msh' => 'model/mesh', + 'msi' => 'application/x-msdownload', + 'msix' => 'application/msix', + 'msixbundle' => 'application/msixbundle', + 'msl' => 'application/vnd.mobius.msl', + 'msm' => 'application/octet-stream', + 'msp' => 'application/octet-stream', + 'msty' => 'application/vnd.muvee.style', + 'mtl' => 'model/mtl', + 'mts' => 'model/vnd.mts', + 'mus' => 'application/vnd.musician', + 'musd' => 'application/mmt-usd+xml', + 'musicxml' => 'application/vnd.recordare.musicxml+xml', + 'mvb' => 'application/x-msmediaview', + 'mvt' => 'application/vnd.mapbox-vector-tile', + 'mwf' => 'application/vnd.mfer', + 'mxf' => 'application/mxf', + 'mxl' => 'application/vnd.recordare.musicxml', + 'mxmf' => 'audio/mobile-xmf', + 'mxml' => 'application/xv+xml', + 'mxs' => 'application/vnd.triscape.mxs', + 'mxu' => 'video/vnd.mpegurl', + 'n-gage' => 'application/vnd.nokia.n-gage.symbian.install', + 'n3' => 'text/n3', + 'nb' => 'application/mathematica', + 'nbp' => 'application/vnd.wolfram.player', + 'nc' => 'application/x-netcdf', + 'ncx' => 'application/x-dtbncx+xml', + 'nfo' => 'text/x-nfo', + 'ngdat' => 'application/vnd.nokia.n-gage.data', + 'nitf' => 'application/vnd.nitf', + 'nlu' => 'application/vnd.neurolanguage.nlu', + 'nml' => 'application/vnd.enliven', + 'nnd' => 'application/vnd.noblenet-directory', + 'nns' => 'application/vnd.noblenet-sealer', + 'nnw' => 'application/vnd.noblenet-web', + 'npx' => 'image/vnd.net-fpx', + 'nq' => 'application/n-quads', + 'nsc' => 'application/x-conference', + 'nsf' => 'application/vnd.lotus-notes', + 'nt' => 'application/n-triples', + 'ntf' => 'application/vnd.nitf', + 'numbers' => 'application/x-iwork-numbers-sffnumbers', + 'nzb' => 'application/x-nzb', + 'oa2' => 'application/vnd.fujitsu.oasys2', + 'oa3' => 'application/vnd.fujitsu.oasys3', + 'oas' => 'application/vnd.fujitsu.oasys', + 'obd' => 'application/x-msbinder', + 'obgx' => 'application/vnd.openblox.game+xml', + 'obj' => 'model/obj', + 'oda' => 'application/oda', + 'odb' => 'application/vnd.oasis.opendocument.database', + 'odc' => 'application/vnd.oasis.opendocument.chart', + 'odf' => 'application/vnd.oasis.opendocument.formula', + 'odft' => 'application/vnd.oasis.opendocument.formula-template', + 'odg' => 'application/vnd.oasis.opendocument.graphics', + 'odi' => 'application/vnd.oasis.opendocument.image', + 'odm' => 'application/vnd.oasis.opendocument.text-master', + 'odp' => 'application/vnd.oasis.opendocument.presentation', + 'ods' => 'application/vnd.oasis.opendocument.spreadsheet', + 'odt' => 'application/vnd.oasis.opendocument.text', + 'oga' => 'audio/ogg', + 'ogex' => 'model/vnd.opengex', + 'ogg' => 'audio/ogg', + 'ogv' => 'video/ogg', + 'ogx' => 'application/ogg', + 'omdoc' => 'application/omdoc+xml', + 'onepkg' => 'application/onenote', + 'onetmp' => 'application/onenote', + 'onetoc' => 'application/onenote', + 'onetoc2' => 'application/onenote', + 'opf' => 'application/oebps-package+xml', + 'opml' => 'text/x-opml', + 'oprc' => 'application/vnd.palm', + 'opus' => 'audio/ogg', + 'org' => 'text/x-org', + 'osf' => 'application/vnd.yamaha.openscoreformat', + 'osfpvg' => 'application/vnd.yamaha.openscoreformat.osfpvg+xml', + 'osm' => 'application/vnd.openstreetmap.data+xml', + 'otc' => 'application/vnd.oasis.opendocument.chart-template', + 'otf' => 'font/otf', + 'otg' => 'application/vnd.oasis.opendocument.graphics-template', + 'oth' => 'application/vnd.oasis.opendocument.text-web', + 'oti' => 'application/vnd.oasis.opendocument.image-template', + 'otp' => 'application/vnd.oasis.opendocument.presentation-template', + 'ots' => 'application/vnd.oasis.opendocument.spreadsheet-template', + 'ott' => 'application/vnd.oasis.opendocument.text-template', + 'ova' => 'application/x-virtualbox-ova', + 'ovf' => 'application/x-virtualbox-ovf', + 'owl' => 'application/rdf+xml', + 'oxps' => 'application/oxps', + 'oxt' => 'application/vnd.openofficeorg.extension', + 'p' => 'text/x-pascal', + 'p10' => 'application/pkcs10', + 'p12' => 'application/x-pkcs12', + 'p7b' => 'application/x-pkcs7-certificates', + 'p7c' => 'application/pkcs7-mime', + 'p7m' => 'application/pkcs7-mime', + 'p7r' => 'application/x-pkcs7-certreqresp', + 'p7s' => 'application/pkcs7-signature', + 'p8' => 'application/pkcs8', + 'pac' => 'application/x-ns-proxy-autoconfig', + 'pages' => 'application/x-iwork-pages-sffpages', + 'pas' => 'text/x-pascal', + 'paw' => 'application/vnd.pawaafile', + 'pbd' => 'application/vnd.powerbuilder6', + 'pbm' => 'image/x-portable-bitmap', + 'pcap' => 'application/vnd.tcpdump.pcap', + 'pcf' => 'application/x-font-pcf', + 'pcl' => 'application/vnd.hp-pcl', + 'pclxl' => 'application/vnd.hp-pclxl', + 'pct' => 'image/x-pict', + 'pcurl' => 'application/vnd.curl.pcurl', + 'pcx' => 'image/x-pcx', + 'pdb' => 'application/x-pilot', + 'pde' => 'text/x-processing', + 'pdf' => 'application/pdf', + 'pem' => 'application/x-x509-ca-cert', + 'pfa' => 'application/x-font-type1', + 'pfb' => 'application/x-font-type1', + 'pfm' => 'application/x-font-type1', + 'pfr' => 'application/font-tdpfr', + 'pfx' => 'application/x-pkcs12', + 'pgm' => 'image/x-portable-graymap', + 'pgn' => 'application/x-chess-pgn', + 'pgp' => 'application/pgp-encrypted', + 'php' => 'application/x-httpd-php', + 'pic' => 'image/x-pict', + 'pkg' => 'application/octet-stream', + 'pki' => 'application/pkixcmp', + 'pkipath' => 'application/pkix-pkipath', + 'pkpass' => 'application/vnd.apple.pkpass', + 'pl' => 'application/x-perl', + 'plb' => 'application/vnd.3gpp.pic-bw-large', + 'plc' => 'application/vnd.mobius.plc', + 'plf' => 'application/vnd.pocketlearn', + 'pls' => 'application/pls+xml', + 'pm' => 'application/x-perl', + 'pml' => 'application/vnd.ctc-posml', + 'png' => 'image/png', + 'pnm' => 'image/x-portable-anymap', + 'portpkg' => 'application/vnd.macports.portpkg', + 'pot' => 'application/vnd.ms-powerpoint', + 'potm' => 'application/vnd.ms-powerpoint.template.macroenabled.12', + 'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template', + 'ppam' => 'application/vnd.ms-powerpoint.addin.macroenabled.12', + 'ppd' => 'application/vnd.cups-ppd', + 'ppm' => 'image/x-portable-pixmap', + 'pps' => 'application/vnd.ms-powerpoint', + 'ppsm' => 'application/vnd.ms-powerpoint.slideshow.macroenabled.12', + 'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', + 'ppt' => 'application/vnd.ms-powerpoint', + 'pptm' => 'application/vnd.ms-powerpoint.presentation.macroenabled.12', + 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'pqa' => 'application/vnd.palm', + 'prc' => 'model/prc', + 'pre' => 'application/vnd.lotus-freelance', + 'prf' => 'application/pics-rules', + 'provx' => 'application/provenance+xml', + 'ps' => 'application/postscript', + 'psb' => 'application/vnd.3gpp.pic-bw-small', + 'psd' => 'image/vnd.adobe.photoshop', + 'psf' => 'application/x-font-linux-psf', + 'pskcxml' => 'application/pskc+xml', + 'pti' => 'image/prs.pti', + 'ptid' => 'application/vnd.pvi.ptid1', + 'pub' => 'application/x-mspublisher', + 'pvb' => 'application/vnd.3gpp.pic-bw-var', + 'pwn' => 'application/vnd.3m.post-it-notes', + 'pya' => 'audio/vnd.ms-playready.media.pya', + 'pyo' => 'model/vnd.pytha.pyox', + 'pyox' => 'model/vnd.pytha.pyox', + 'pyv' => 'video/vnd.ms-playready.media.pyv', + 'qam' => 'application/vnd.epson.quickanime', + 'qbo' => 'application/vnd.intu.qbo', + 'qfx' => 'application/vnd.intu.qfx', + 'qps' => 'application/vnd.publishare-delta-tree', + 'qt' => 'video/quicktime', + 'qwd' => 'application/vnd.quark.quarkxpress', + 'qwt' => 'application/vnd.quark.quarkxpress', + 'qxb' => 'application/vnd.quark.quarkxpress', + 'qxd' => 'application/vnd.quark.quarkxpress', + 'qxl' => 'application/vnd.quark.quarkxpress', + 'qxt' => 'application/vnd.quark.quarkxpress', + 'ra' => 'audio/x-realaudio', + 'ram' => 'audio/x-pn-realaudio', + 'raml' => 'application/raml+yaml', + 'rapd' => 'application/route-apd+xml', + 'rar' => 'application/x-rar-compressed', + 'ras' => 'image/x-cmu-raster', + 'rcprofile' => 'application/vnd.ipunplugged.rcprofile', + 'rdf' => 'application/rdf+xml', + 'rdz' => 'application/vnd.data-vision.rdz', + 'relo' => 'application/p2p-overlay+xml', + 'rep' => 'application/vnd.businessobjects', + 'res' => 'application/x-dtbresource+xml', + 'rgb' => 'image/x-rgb', + 'rif' => 'application/reginfo+xml', + 'rip' => 'audio/vnd.rip', + 'ris' => 'application/x-research-info-systems', + 'rl' => 'application/resource-lists+xml', + 'rlc' => 'image/vnd.fujixerox.edmics-rlc', + 'rld' => 'application/resource-lists-diff+xml', + 'rm' => 'application/vnd.rn-realmedia', + 'rmi' => 'audio/midi', + 'rmp' => 'audio/x-pn-realaudio-plugin', + 'rms' => 'application/vnd.jcp.javame.midlet-rms', + 'rmvb' => 'application/vnd.rn-realmedia-vbr', + 'rnc' => 'application/relax-ng-compact-syntax', + 'rng' => 'application/xml', + 'roa' => 'application/rpki-roa', + 'roff' => 'text/troff', + 'rp9' => 'application/vnd.cloanto.rp9', + 'rpm' => 'application/x-redhat-package-manager', + 'rpss' => 'application/vnd.nokia.radio-presets', + 'rpst' => 'application/vnd.nokia.radio-preset', + 'rq' => 'application/sparql-query', + 'rs' => 'application/rls-services+xml', + 'rsat' => 'application/atsc-rsat+xml', + 'rsd' => 'application/rsd+xml', + 'rsheet' => 'application/urc-ressheet+xml', + 'rss' => 'application/rss+xml', + 'rtf' => 'text/rtf', + 'rtx' => 'text/richtext', + 'run' => 'application/x-makeself', + 'rusd' => 'application/route-usd+xml', + 's' => 'text/x-asm', + 's3m' => 'audio/s3m', + 'saf' => 'application/vnd.yamaha.smaf-audio', + 'sass' => 'text/x-sass', + 'sbml' => 'application/sbml+xml', + 'sc' => 'application/vnd.ibm.secure-container', + 'scd' => 'application/x-msschedule', + 'scm' => 'application/vnd.lotus-screencam', + 'scq' => 'application/scvp-cv-request', + 'scs' => 'application/scvp-cv-response', + 'scss' => 'text/x-scss', + 'scurl' => 'text/vnd.curl.scurl', + 'sda' => 'application/vnd.stardivision.draw', + 'sdc' => 'application/vnd.stardivision.calc', + 'sdd' => 'application/vnd.stardivision.impress', + 'sdkd' => 'application/vnd.solent.sdkm+xml', + 'sdkm' => 'application/vnd.solent.sdkm+xml', + 'sdp' => 'application/sdp', + 'sdw' => 'application/vnd.stardivision.writer', + 'sea' => 'application/x-sea', + 'see' => 'application/vnd.seemail', + 'seed' => 'application/vnd.fdsn.seed', + 'sema' => 'application/vnd.sema', + 'semd' => 'application/vnd.semd', + 'semf' => 'application/vnd.semf', + 'senmlx' => 'application/senml+xml', + 'sensmlx' => 'application/sensml+xml', + 'ser' => 'application/java-serialized-object', + 'setpay' => 'application/set-payment-initiation', + 'setreg' => 'application/set-registration-initiation', + 'sfd-hdstx' => 'application/vnd.hydrostatix.sof-data', + 'sfs' => 'application/vnd.spotfire.sfs', + 'sfv' => 'text/x-sfv', + 'sgi' => 'image/sgi', + 'sgl' => 'application/vnd.stardivision.writer-global', + 'sgm' => 'text/sgml', + 'sgml' => 'text/sgml', + 'sh' => 'application/x-sh', + 'shar' => 'application/x-shar', + 'shex' => 'text/shex', + 'shf' => 'application/shf+xml', + 'shtml' => 'text/html', + 'sid' => 'image/x-mrsid-image', + 'sieve' => 'application/sieve', + 'sig' => 'application/pgp-signature', + 'sil' => 'audio/silk', + 'silo' => 'model/mesh', + 'sis' => 'application/vnd.symbian.install', + 'sisx' => 'application/vnd.symbian.install', + 'sit' => 'application/x-stuffit', + 'sitx' => 'application/x-stuffitx', + 'siv' => 'application/sieve', + 'skd' => 'application/vnd.koan', + 'skm' => 'application/vnd.koan', + 'skp' => 'application/vnd.koan', + 'skt' => 'application/vnd.koan', + 'sldm' => 'application/vnd.ms-powerpoint.slide.macroenabled.12', + 'sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide', + 'slim' => 'text/slim', + 'slm' => 'text/slim', + 'sls' => 'application/route-s-tsid+xml', + 'slt' => 'application/vnd.epson.salt', + 'sm' => 'application/vnd.stepmania.stepchart', + 'smf' => 'application/vnd.stardivision.math', + 'smi' => 'application/smil+xml', + 'smil' => 'application/smil+xml', + 'smv' => 'video/x-smv', + 'smzip' => 'application/vnd.stepmania.package', + 'snd' => 'audio/basic', + 'snf' => 'application/x-font-snf', + 'so' => 'application/octet-stream', + 'spc' => 'application/x-pkcs7-certificates', + 'spdx' => 'text/spdx', + 'spf' => 'application/vnd.yamaha.smaf-phrase', + 'spl' => 'application/x-futuresplash', + 'spot' => 'text/vnd.in3d.spot', + 'spp' => 'application/scvp-vp-response', + 'spq' => 'application/scvp-vp-request', + 'spx' => 'audio/ogg', + 'sql' => 'application/x-sql', + 'src' => 'application/x-wais-source', + 'srt' => 'application/x-subrip', + 'sru' => 'application/sru+xml', + 'srx' => 'application/sparql-results+xml', + 'ssdl' => 'application/ssdl+xml', + 'sse' => 'application/vnd.kodak-descriptor', + 'ssf' => 'application/vnd.epson.ssf', + 'ssml' => 'application/ssml+xml', + 'st' => 'application/vnd.sailingtracker.track', + 'stc' => 'application/vnd.sun.xml.calc.template', + 'std' => 'application/vnd.sun.xml.draw.template', + 'stf' => 'application/vnd.wt.stf', + 'sti' => 'application/vnd.sun.xml.impress.template', + 'stk' => 'application/hyperstudio', + 'stl' => 'model/stl', + 'stpx' => 'model/step+xml', + 'stpxz' => 'model/step-xml+zip', + 'stpz' => 'model/step+zip', + 'str' => 'application/vnd.pg.format', + 'stw' => 'application/vnd.sun.xml.writer.template', + 'styl' => 'text/stylus', + 'stylus' => 'text/stylus', + 'sub' => 'text/vnd.dvb.subtitle', + 'sus' => 'application/vnd.sus-calendar', + 'susp' => 'application/vnd.sus-calendar', + 'sv4cpio' => 'application/x-sv4cpio', + 'sv4crc' => 'application/x-sv4crc', + 'svc' => 'application/vnd.dvb.service', + 'svd' => 'application/vnd.svd', + 'svg' => 'image/svg+xml', + 'svgz' => 'image/svg+xml', + 'swa' => 'application/x-director', + 'swf' => 'application/x-shockwave-flash', + 'swi' => 'application/vnd.aristanetworks.swi', + 'swidtag' => 'application/swid+xml', + 'sxc' => 'application/vnd.sun.xml.calc', + 'sxd' => 'application/vnd.sun.xml.draw', + 'sxg' => 'application/vnd.sun.xml.writer.global', + 'sxi' => 'application/vnd.sun.xml.impress', + 'sxm' => 'application/vnd.sun.xml.math', + 'sxw' => 'application/vnd.sun.xml.writer', + 't' => 'text/troff', + 't3' => 'application/x-t3vm-image', + 't38' => 'image/t38', + 'taglet' => 'application/vnd.mynfc', + 'tao' => 'application/vnd.tao.intent-module-archive', + 'tap' => 'image/vnd.tencent.tap', + 'tar' => 'application/x-tar', + 'tcap' => 'application/vnd.3gpp2.tcap', + 'tcl' => 'application/x-tcl', + 'td' => 'application/urc-targetdesc+xml', + 'teacher' => 'application/vnd.smart.teacher', + 'tei' => 'application/tei+xml', + 'teicorpus' => 'application/tei+xml', + 'tex' => 'application/x-tex', + 'texi' => 'application/x-texinfo', + 'texinfo' => 'application/x-texinfo', + 'text' => 'text/plain', + 'tfi' => 'application/thraud+xml', + 'tfm' => 'application/x-tex-tfm', + 'tfx' => 'image/tiff-fx', + 'tga' => 'image/x-tga', + 'thmx' => 'application/vnd.ms-officetheme', + 'tif' => 'image/tiff', + 'tiff' => 'image/tiff', + 'tk' => 'application/x-tcl', + 'tmo' => 'application/vnd.tmobile-livetv', + 'toml' => 'application/toml', + 'torrent' => 'application/x-bittorrent', + 'tpl' => 'application/vnd.groove-tool-template', + 'tpt' => 'application/vnd.trid.tpt', + 'tr' => 'text/troff', + 'tra' => 'application/vnd.trueapp', + 'trig' => 'application/trig', + 'trm' => 'application/x-msterminal', + 'ts' => 'video/mp2t', + 'tsd' => 'application/timestamped-data', + 'tsv' => 'text/tab-separated-values', + 'ttc' => 'font/collection', + 'ttf' => 'font/ttf', + 'ttl' => 'text/turtle', + 'ttml' => 'application/ttml+xml', + 'twd' => 'application/vnd.simtech-mindmapper', + 'twds' => 'application/vnd.simtech-mindmapper', + 'txd' => 'application/vnd.genomatix.tuxedo', + 'txf' => 'application/vnd.mobius.txf', + 'txt' => 'text/plain', + 'u32' => 'application/x-authorware-bin', + 'u3d' => 'model/u3d', + 'u8dsn' => 'message/global-delivery-status', + 'u8hdr' => 'message/global-headers', + 'u8mdn' => 'message/global-disposition-notification', + 'u8msg' => 'message/global', + 'ubj' => 'application/ubjson', + 'udeb' => 'application/x-debian-package', + 'ufd' => 'application/vnd.ufdl', + 'ufdl' => 'application/vnd.ufdl', + 'ulx' => 'application/x-glulx', + 'umj' => 'application/vnd.umajin', + 'unityweb' => 'application/vnd.unity', + 'uo' => 'application/vnd.uoml+xml', + 'uoml' => 'application/vnd.uoml+xml', + 'uri' => 'text/uri-list', + 'uris' => 'text/uri-list', + 'urls' => 'text/uri-list', + 'usda' => 'model/vnd.usda', + 'usdz' => 'model/vnd.usdz+zip', + 'ustar' => 'application/x-ustar', + 'utz' => 'application/vnd.uiq.theme', + 'uu' => 'text/x-uuencode', + 'uva' => 'audio/vnd.dece.audio', + 'uvd' => 'application/vnd.dece.data', + 'uvf' => 'application/vnd.dece.data', + 'uvg' => 'image/vnd.dece.graphic', + 'uvh' => 'video/vnd.dece.hd', + 'uvi' => 'image/vnd.dece.graphic', + 'uvm' => 'video/vnd.dece.mobile', + 'uvp' => 'video/vnd.dece.pd', + 'uvs' => 'video/vnd.dece.sd', + 'uvt' => 'application/vnd.dece.ttml+xml', + 'uvu' => 'video/vnd.uvvu.mp4', + 'uvv' => 'video/vnd.dece.video', + 'uvva' => 'audio/vnd.dece.audio', + 'uvvd' => 'application/vnd.dece.data', + 'uvvf' => 'application/vnd.dece.data', + 'uvvg' => 'image/vnd.dece.graphic', + 'uvvh' => 'video/vnd.dece.hd', + 'uvvi' => 'image/vnd.dece.graphic', + 'uvvm' => 'video/vnd.dece.mobile', + 'uvvp' => 'video/vnd.dece.pd', + 'uvvs' => 'video/vnd.dece.sd', + 'uvvt' => 'application/vnd.dece.ttml+xml', + 'uvvu' => 'video/vnd.uvvu.mp4', + 'uvvv' => 'video/vnd.dece.video', + 'uvvx' => 'application/vnd.dece.unspecified', + 'uvvz' => 'application/vnd.dece.zip', + 'uvx' => 'application/vnd.dece.unspecified', + 'uvz' => 'application/vnd.dece.zip', + 'vbox' => 'application/x-virtualbox-vbox', + 'vbox-extpack' => 'application/x-virtualbox-vbox-extpack', + 'vcard' => 'text/vcard', + 'vcd' => 'application/x-cdlink', + 'vcf' => 'text/x-vcard', + 'vcg' => 'application/vnd.groove-vcard', + 'vcs' => 'text/x-vcalendar', + 'vcx' => 'application/vnd.vcx', + 'vdi' => 'application/x-virtualbox-vdi', + 'vds' => 'model/vnd.sap.vds', + 'vhd' => 'application/x-virtualbox-vhd', + 'vis' => 'application/vnd.visionary', + 'viv' => 'video/vnd.vivo', + 'vmdk' => 'application/x-virtualbox-vmdk', + 'vob' => 'video/x-ms-vob', + 'vor' => 'application/vnd.stardivision.writer', + 'vox' => 'application/x-authorware-bin', + 'vrml' => 'model/vrml', + 'vsd' => 'application/vnd.visio', + 'vsf' => 'application/vnd.vsf', + 'vss' => 'application/vnd.visio', + 'vst' => 'application/vnd.visio', + 'vsw' => 'application/vnd.visio', + 'vtf' => 'image/vnd.valve.source.texture', + 'vtt' => 'text/vtt', + 'vtu' => 'model/vnd.vtu', + 'vxml' => 'application/voicexml+xml', + 'w3d' => 'application/x-director', + 'wad' => 'application/x-doom', + 'wadl' => 'application/vnd.sun.wadl+xml', + 'war' => 'application/java-archive', + 'wasm' => 'application/wasm', + 'wav' => 'audio/x-wav', + 'wax' => 'audio/x-ms-wax', + 'wbmp' => 'image/vnd.wap.wbmp', + 'wbs' => 'application/vnd.criticaltools.wbs+xml', + 'wbxml' => 'application/vnd.wap.wbxml', + 'wcm' => 'application/vnd.ms-works', + 'wdb' => 'application/vnd.ms-works', + 'wdp' => 'image/vnd.ms-photo', + 'weba' => 'audio/webm', + 'webapp' => 'application/x-web-app-manifest+json', + 'webm' => 'video/webm', + 'webmanifest' => 'application/manifest+json', + 'webp' => 'image/webp', + 'wg' => 'application/vnd.pmi.widget', + 'wgsl' => 'text/wgsl', + 'wgt' => 'application/widget', + 'wif' => 'application/watcherinfo+xml', + 'wks' => 'application/vnd.ms-works', + 'wm' => 'video/x-ms-wm', + 'wma' => 'audio/x-ms-wma', + 'wmd' => 'application/x-ms-wmd', + 'wmf' => 'image/wmf', + 'wml' => 'text/vnd.wap.wml', + 'wmlc' => 'application/vnd.wap.wmlc', + 'wmls' => 'text/vnd.wap.wmlscript', + 'wmlsc' => 'application/vnd.wap.wmlscriptc', + 'wmv' => 'video/x-ms-wmv', + 'wmx' => 'video/x-ms-wmx', + 'wmz' => 'application/x-msmetafile', + 'woff' => 'font/woff', + 'woff2' => 'font/woff2', + 'wpd' => 'application/vnd.wordperfect', + 'wpl' => 'application/vnd.ms-wpl', + 'wps' => 'application/vnd.ms-works', + 'wqd' => 'application/vnd.wqd', + 'wri' => 'application/x-mswrite', + 'wrl' => 'model/vrml', + 'wsc' => 'message/vnd.wfa.wsc', + 'wsdl' => 'application/wsdl+xml', + 'wspolicy' => 'application/wspolicy+xml', + 'wtb' => 'application/vnd.webturbo', + 'wvx' => 'video/x-ms-wvx', + 'x32' => 'application/x-authorware-bin', + 'x3d' => 'model/x3d+xml', + 'x3db' => 'model/x3d+fastinfoset', + 'x3dbz' => 'model/x3d+binary', + 'x3dv' => 'model/x3d-vrml', + 'x3dvz' => 'model/x3d+vrml', + 'x3dz' => 'model/x3d+xml', + 'x_b' => 'model/vnd.parasolid.transmit.binary', + 'x_t' => 'model/vnd.parasolid.transmit.text', + 'xaml' => 'application/xaml+xml', + 'xap' => 'application/x-silverlight-app', + 'xar' => 'application/vnd.xara', + 'xav' => 'application/xcap-att+xml', + 'xbap' => 'application/x-ms-xbap', + 'xbd' => 'application/vnd.fujixerox.docuworks.binder', + 'xbm' => 'image/x-xbitmap', + 'xca' => 'application/xcap-caps+xml', + 'xcs' => 'application/calendar+xml', + 'xdf' => 'application/xcap-diff+xml', + 'xdm' => 'application/vnd.syncml.dm+xml', + 'xdp' => 'application/vnd.adobe.xdp+xml', + 'xdssc' => 'application/dssc+xml', + 'xdw' => 'application/vnd.fujixerox.docuworks', + 'xel' => 'application/xcap-el+xml', + 'xenc' => 'application/xenc+xml', + 'xer' => 'application/patch-ops-error+xml', + 'xfdf' => 'application/xfdf', + 'xfdl' => 'application/vnd.xfdl', + 'xht' => 'application/xhtml+xml', + 'xhtm' => 'application/vnd.pwg-xhtml-print+xml', + 'xhtml' => 'application/xhtml+xml', + 'xhvml' => 'application/xv+xml', + 'xif' => 'image/vnd.xiff', + 'xla' => 'application/vnd.ms-excel', + 'xlam' => 'application/vnd.ms-excel.addin.macroenabled.12', + 'xlc' => 'application/vnd.ms-excel', + 'xlf' => 'application/xliff+xml', + 'xlm' => 'application/vnd.ms-excel', + 'xls' => 'application/vnd.ms-excel', + 'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroenabled.12', + 'xlsm' => 'application/vnd.ms-excel.sheet.macroenabled.12', + 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'xlt' => 'application/vnd.ms-excel', + 'xltm' => 'application/vnd.ms-excel.template.macroenabled.12', + 'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', + 'xlw' => 'application/vnd.ms-excel', + 'xm' => 'audio/xm', + 'xml' => 'text/xml', + 'xns' => 'application/xcap-ns+xml', + 'xo' => 'application/vnd.olpc-sugar', + 'xop' => 'application/xop+xml', + 'xpi' => 'application/x-xpinstall', + 'xpl' => 'application/xproc+xml', + 'xpm' => 'image/x-xpixmap', + 'xpr' => 'application/vnd.is-xpr', + 'xps' => 'application/vnd.ms-xpsdocument', + 'xpw' => 'application/vnd.intercon.formnet', + 'xpx' => 'application/vnd.intercon.formnet', + 'xsd' => 'application/xml', + 'xsf' => 'application/prs.xsf+xml', + 'xsl' => 'application/xslt+xml', + 'xslt' => 'application/xslt+xml', + 'xsm' => 'application/vnd.syncml+xml', + 'xspf' => 'application/xspf+xml', + 'xul' => 'application/vnd.mozilla.xul+xml', + 'xvm' => 'application/xv+xml', + 'xvml' => 'application/xv+xml', + 'xwd' => 'image/x-xwindowdump', + 'xyz' => 'chemical/x-xyz', + 'xz' => 'application/x-xz', + 'yaml' => 'text/yaml', + 'yang' => 'application/yang', + 'yin' => 'application/yin+xml', + 'yml' => 'text/yaml', + 'ymp' => 'text/x-suse-ymp', + 'z1' => 'application/x-zmachine', + 'z2' => 'application/x-zmachine', + 'z3' => 'application/x-zmachine', + 'z4' => 'application/x-zmachine', + 'z5' => 'application/x-zmachine', + 'z6' => 'application/x-zmachine', + 'z7' => 'application/x-zmachine', + 'z8' => 'application/x-zmachine', + 'zaz' => 'application/vnd.zzazz.deck+xml', + 'zip' => 'application/zip', + 'zir' => 'application/vnd.zul', + 'zirz' => 'application/vnd.zul', + 'zmm' => 'application/vnd.handheld-entertainment+xml', +]; \ No newline at end of file diff --git a/tests/ExceptionStub.php b/tests/ExceptionStub.php new file mode 100644 index 0000000..5f3886e --- /dev/null +++ b/tests/ExceptionStub.php @@ -0,0 +1,21 @@ +messages = $messages; + return $this; + } + + public function get_messages() { + return $this->messages; + } +} diff --git a/tests/Integration/SampleIntegrationTest.php b/tests/Integration/SampleIntegrationTest.php deleted file mode 100644 index 4f3b832..0000000 --- a/tests/Integration/SampleIntegrationTest.php +++ /dev/null @@ -1,12 +0,0 @@ -assertTrue( function_exists( 'get_bloginfo' ) ); - } -} diff --git a/tests/Unit/Core/DateTimeFormatTest.php b/tests/Unit/Core/DateTimeFormatTest.php new file mode 100644 index 0000000..bd16a2d --- /dev/null +++ b/tests/Unit/Core/DateTimeFormatTest.php @@ -0,0 +1,116 @@ + '2023-12-25', + 'd/m/Y' => '25/12/2023', + 'm-d-Y' => '12-25-2023', + 'Y.m.d' => '2023.12.25', + 'd-M-Y' => '25-Dec-2023', + ]; + + foreach ( $formats as $format => $value ) { + $request = new WP_REST_Request( 'POST', '/test' ); + $request->set_param( 'date_field', $value ); + + $rules = [ + 'date_field' => "date:{$format}", + ]; + + $validation = new Validation( $request, $rules ); + $this->assertTrue( $validation->passes(), "Failed asserting that {$value} is a valid date for format {$format}" ); + } + } + + /** + * Test date time formats. + */ + public function test_date_time_formats() { + $formats = [ + 'Y-m-d H:i:s' => '2023-12-25 14:30:00', + 'Y-m-d H:i' => '2023-12-25 14:30', + 'd/m/Y g:i A' => '25/12/2023 2:30 PM', + 'Y-m-d\TH:i:s' => '2023-12-25T14:30:00', + 'Y-m-d\TH:i:sP' => '2023-12-25T14:30:00+01:00', + 'H:i' => '14:30', + ]; + + foreach ( $formats as $format => $value ) { + $request = new WP_REST_Request( 'POST', '/test' ); + $request->set_param( 'datetime_field', $value ); + + $rules = [ + 'datetime_field' => "date:{$format}", + ]; + + $validation = new Validation( $request, $rules ); + $this->assertTrue( $validation->passes(), "Failed asserting that {$value} is a valid datetime for format {$format}" ); + } + } + + /** + * Test before and after rules with different formats. + */ + public function test_before_and_after_with_custom_formats() { + // Test with Y-m-d H:i + $request = new WP_REST_Request( 'POST', '/test' ); + $request->set_param( 'start_time', '2023-12-25 10:00' ); + $request->set_param( 'end_time', '2023-12-25 11:00' ); + + $rules = [ + 'start_time' => 'date:Y-m-d H:i', + 'end_time' => 'date:Y-m-d H:i|after:start_time,Y-m-d H:i', + ]; + + $validation = new Validation( $request, $rules ); + $this->assertTrue( $validation->passes(), 'Failed after:start_time with Y-m-d H:i' ); + + // Test with invalid order + $request->set_param( 'end_time', '2023-12-25 09:00' ); + $validation = new Validation( $request, $rules ); + $this->assertTrue( $validation->fails(), 'Failed asserting that 09:00 is not after 10:00' ); + } + + /** + * Test date_equals rule with custom format. + */ + public function test_date_equals_with_custom_format() { + $request = new WP_REST_Request( 'POST', '/test' ); + $request->set_param( 'date_1', '25/12/2023' ); + $request->set_param( 'date_2', '25/12/2023' ); + + $rules = [ + 'date_1' => 'date:d/m/Y', + 'date_2' => 'date_equals:date_1,d/m/Y', + ]; + + $validation = new Validation( $request, $rules ); + $this->assertTrue( $validation->passes() ); + } + + /** + * Regression test for flawed DateTime fallback logic. + */ + public function test_date_rule_with_fallback_flaw() { + $request = new WP_REST_Request( 'POST', '/test' ); + $today = date( 'Y-m-d' ); + $request->set_param( 'start_date', $today ); + + $rules = [ + 'start_date' => "after:$today", + ]; + + $validation = new Validation( $request, $rules ); + $this->assertTrue( $validation->fails(), 'A date should not be "after" itself.' ); + } +} diff --git a/tests/Unit/Core/HooksTest.php b/tests/Unit/Core/HooksTest.php new file mode 100644 index 0000000..4561b2f --- /dev/null +++ b/tests/Unit/Core/HooksTest.php @@ -0,0 +1,33 @@ +set_param( 'username', 'admin' ); + + $validation = new Validation( + $request, [ + 'username' => 'required|alpha' + ] + ); + + $hook_executed = false; + + $validation->after( + function ( $validator ) use ( &$hook_executed ) { + $hook_executed = true; + $this->assertInstanceOf( Validation::class, $validator ); + } + ); + + // Trigger validation. after() hooks execute after the main validation loop. + $this->assertTrue( $validation->passes(), 'passes() should return true for a valid payload.' ); + $this->assertTrue( $hook_executed, 'The after() hook was not executed.' ); + } +} diff --git a/tests/Unit/Core/NestedValidationTest.php b/tests/Unit/Core/NestedValidationTest.php new file mode 100644 index 0000000..96ac0aa --- /dev/null +++ b/tests/Unit/Core/NestedValidationTest.php @@ -0,0 +1,114 @@ + [ + 'l2' => [ + 'l3' => [ + 'l4' => [ + 'l5' => [ + 'l6' => [ + 'l7' => [ + 'l8' => [ + 'l9' => [ + 'l10' => 'invalid-email' + ] + ] + ] + ] + ] + ] + ] + ] + ] + ]; + + $request->set_body_params( $data ); + + $rules = [ + 'l1.l2.l3.l4.l5.l6.l7.l8.l9.l10' => 'required|email', + ]; + + $validation = new Validation( $request, $rules ); + + $this->assertTrue( $validation->fails() ); + $errors = $validation->errors(); + + $this->assertArrayHasKey( 'l1.l2.l3.l4.l5.l6.l7.l8.l9.l10', $errors ); + $this->assertContains( 'The l1.l2.l3.l4.l5.l6.l7.l8.l9.l10 must be a valid email address.', $errors['l1.l2.l3.l4.l5.l6.l7.l8.l9.l10'] ); + } + + /** + * Test wildcard validation at multiple levels. + */ + public function test_wildcard_nested_validation() { + $request = new WP_REST_Request( 'POST', '/test' ); + + $data = [ + 'users' => [ + [ + 'posts' => [ + ['id' => 1, 'meta' => ['key' => '']], + ['id' => 2, 'meta' => ['key' => 'val']], + ] + ], + [ + 'posts' => [ + ['id' => 3, 'meta' => ['key' => '']], + ] + ] + ] + ]; + + $request->set_body_params( $data ); + + $rules = [ + 'users.*.posts.*.meta.key' => 'required', + ]; + + $validation = new Validation( $request, $rules ); + $this->assertTrue( $validation->fails() ); + $errors = $validation->errors(); + + // Should have errors for index 0.0 and 1.0 + $this->assertArrayHasKey( 'users.0.posts.0.meta.key', $errors ); + $this->assertArrayHasKey( 'users.1.posts.0.meta.key', $errors ); + $this->assertArrayNotHasKey( 'users.0.posts.1.meta.key', $errors ); + } + + /** + * Test the "same" rule with nested dot notation. + */ + public function test_same_rule_with_nested_dot_notation() { + $request = new WP_REST_Request( 'POST', '/test' ); + $request->set_body_params( + [ + 'user' => [ + 'password' => 'secret', + 'password_confirmation' => 'secret', + ], + ] + ); + + $rules = [ + 'user.password_confirmation' => 'same:user.password', + ]; + + $validation = new Validation( $request, $rules ); + + $this->assertTrue( $validation->passes(), 'The "same" rule should support dot notation for nested fields.' ); + } +} diff --git a/tests/Unit/Core/RuleResolverTest.php b/tests/Unit/Core/RuleResolverTest.php new file mode 100644 index 0000000..38bdd42 --- /dev/null +++ b/tests/Unit/Core/RuleResolverTest.php @@ -0,0 +1,70 @@ +setAccessible( true ); + } + $reflection->setValue( null, [] ); + } + + public function test_it_resolves_stateless_rules_from_cache(): void { + // First resolution should instantiate and cache + $email_rule1 = RuleResolver::resolve( 'email' ); + + // Second resolution should return the exact same object from cache + $email_rule2 = RuleResolver::resolve( 'email' ); + + $this->assertSame( $email_rule1, $email_rule2, 'RuleResolver should cache and return the exact same stateless rule instance.' ); + } + + public function test_it_correctly_hydrates_rule_parameters(): void { + // Resolve a rule with parameters (not cached) + $min_rule = RuleResolver::resolve( 'min', ['5'] ); + + $this->assertInstanceOf( \WpMVC\RequestValidator\Rules\Min::class, $min_rule ); + + $reflection = new ReflectionProperty( $min_rule, 'min' ); + if ( \PHP_VERSION_ID < 80100 ) { + $reflection->setAccessible( true ); + } + $this->assertEquals( '5', $reflection->getValue( $min_rule ) ); + } + + public function test_it_returns_null_for_unregistered_rules(): void { + $rules = RuleResolver::resolve( 'non_existent_rule_123' ); + $this->assertNull( $rules, 'Unregistered rules should return null.' ); + } + + public function test_mac_address_typo_is_fixed(): void { + $mac_rule = RuleResolver::resolve( 'mac_address' ); + $this->assertInstanceOf( \WpMVC\RequestValidator\Rules\MacAddress::class, $mac_rule ); + } + + public function test_it_resolves_multiple_parameters_correctly(): void { + $between_rule = RuleResolver::resolve( 'between', ['1', '10'] ); + + $this->assertInstanceOf( \WpMVC\RequestValidator\Rules\Between::class, $between_rule ); + + $reflection_min = new ReflectionProperty( $between_rule, 'min' ); + if ( \PHP_VERSION_ID < 80100 ) { + $reflection_min->setAccessible( true ); + } + $this->assertEquals( '1', $reflection_min->getValue( $between_rule ) ); + + $reflection_max = new ReflectionProperty( $between_rule, 'max' ); + if ( \PHP_VERSION_ID < 80100 ) { + $reflection_max->setAccessible( true ); + } + $this->assertEquals( '10', $reflection_max->getValue( $between_rule ) ); + } +} diff --git a/tests/Unit/Core/ValidationEngineTest.php b/tests/Unit/Core/ValidationEngineTest.php new file mode 100644 index 0000000..05fb2b2 --- /dev/null +++ b/tests/Unit/Core/ValidationEngineTest.php @@ -0,0 +1,143 @@ +set_param( + 'payload', [ + 'meta' => [ + ['id' => 1, 'name' => 'John'], + ['id' => 2, 'name' => 'Jane'] + ] + ] + ); + + // Validate that each ID must be an integer and exactly 1 digit + $validation = new Validation( + $request, [ + 'payload.meta.*.id' => 'required|integer|digits:1', + 'payload.meta.*.name' => 'required|alpha|max:10' + ] + ); + + $this->assertTrue( $validation->passes(), 'Deep wildcard validation failed on valid payload.' ); + + // Add an invalid payload deep in the structure + $request->set_param( + 'payload', [ + 'meta' => [ + ['id' => '12', 'name' => 'John'], // Invalid: ID digits should be 1 + ] + ] + ); + + $validation2 = new Validation( + $request, [ + 'payload.meta.*.id' => 'required|integer|digits:1', + ] + ); + + $this->assertTrue( $validation2->fails(), 'Deep wildcard failed to reject invalid nested payload.' ); + $this->assertArrayHasKey( 'payload.meta.0.id', $validation2->errors() ); + } + + public function test_rule_type_evaluation_pipe_and_array_syntax() { + $request = new WP_REST_Request(); + $request->set_param( 'title', 'ValidTitle' ); + + // Mixed syntax + $validation = new Validation( + $request, [ + 'title' => ['required', 'alpha', 'max:50'] // Array syntax + ] + ); + + $this->assertTrue( $validation->passes(), 'Array syntax failed.' ); + + $validation2 = new Validation( + $request, [ + 'title' => 'required|alpha|max:50' // Pipe syntax + ] + ); + + $this->assertTrue( $validation2->passes(), 'Pipe syntax failed.' ); + } + + public function test_circuit_breaker_mechanics_bail_and_nullable() { + $request = new WP_REST_Request(); + + // --- nullable --- + $validation_nullable = new Validation( + $request, [ + 'optional_field' => 'nullable|email' // optional_field is missing completely + ] + ); + + $this->assertTrue( $validation_nullable->passes(), 'Nullable failed to circuit-break on missing field.' ); + + $request->set_param( 'optional_field', '' ); // null/empty string + $validation_nullable_empty = new Validation( + $request, [ + 'optional_field' => 'nullable|email' + ] + ); + $this->assertTrue( $validation_nullable_empty->passes(), 'Nullable failed to circuit-break on empty string.' ); + + $request->set_param( 'optional_field', 'invalid-email' ); + $validation_nullable_invalid = new Validation( + $request, [ + 'optional_field' => 'nullable|email' + ] + ); + $this->assertTrue( $validation_nullable_invalid->fails(), 'Nullable allowed invalid formatted data.' ); + + // --- bail --- + $request->set_param( 'title', 'invalid-data' ); // Not an array, not min size + $validation_bail = new Validation( + $request, [ + 'title' => 'required|array|min:5' + ] + ); + + $this->assertTrue( $validation_bail->fails() ); + // Since it's not array, the array validation failed. Wait, bail is for stopping on FIRST failure. + $this->assertCount( 2, $validation_bail->errors()['title'], 'Without bail, multiple errors should be registered.' ); + + $validation_with_bail = new Validation( + $request, [ + 'title' => 'bail|required|array|min:5' + ] + ); + + $this->assertCount( 1, $validation_with_bail->errors()['title'], 'Bail failed to stop validation after first failure.' ); + } + + public function test_internal_method_fallbacks_like_date_validator() { + $request = new WP_REST_Request(); + $request->set_param( 'start_date', '2023-01-01' ); + + // Instead of resolving a class, this falls back to $this->date_validator internally from DateTime trait. + $validation = new Validation( + $request, [ + 'start_date' => 'required|date:Y-m-d' + ] + ); + + $this->assertTrue( $validation->passes(), 'Fallback to trait validator failed on valid data.' ); + + $request->set_param( 'start_date', '01/01/2023' ); // Invalid format + $validation_invalid = new Validation( + $request, [ + 'start_date' => 'required|date:Y-m-d' + ] + ); + + $this->assertTrue( $validation_invalid->fails(), 'Fallback to trait validator failed to reject invalid data.' ); + } +} diff --git a/tests/Unit/Extensibility/CustomRuleExtensionTest.php b/tests/Unit/Extensibility/CustomRuleExtensionTest.php new file mode 100644 index 0000000..5ebb3de --- /dev/null +++ b/tests/Unit/Extensibility/CustomRuleExtensionTest.php @@ -0,0 +1,61 @@ +set_param( 'code', 'LOWERCASE' ); // wait, UPPERCASE is valid, lowercase is invalid + $request->set_param( 'code_valid', 'UPPERCASE' ); + + $validation = new Validation( + $request, [ + 'code' => [new UppercaseRule()], + 'code_valid' => ['required', new UppercaseRule()] + ] + ); + + $this->assertTrue( $validation->passes(), 'Validation should pass with custom rules evaluating to true.' ); + } + + public function test_validation_with_proper_case() { + $request = new WP_REST_Request(); + $request->set_param( 'code', 'lowercase' ); // Invalid + $request->set_param( 'code_valid', 'UPPERCASE' ); // Valid + + $validation = new Validation( + $request, [ + 'code' => [new UppercaseRule()], + 'code_valid' => ['required', new UppercaseRule()] + ] + ); + + $this->assertTrue( $validation->fails() ); + $this->assertArrayHasKey( 'code', $validation->errors() ); + $this->assertArrayNotHasKey( 'code_valid', $validation->errors() ); + $this->assertEquals( 'The field must be uppercase.', $validation->errors()['code'][0] ); + } +} diff --git a/tests/Unit/Integration/FormRequestLifecycleTest.php b/tests/Unit/Integration/FormRequestLifecycleTest.php new file mode 100644 index 0000000..d7932f9 --- /dev/null +++ b/tests/Unit/Integration/FormRequestLifecycleTest.php @@ -0,0 +1,67 @@ +authorize_result; + } + + public function rules(): array { + return [ + 'username' => 'required|alpha' + ]; + } + + public function with_validator( $validator ): void { + $this->with_validator_called = true; + } +} + +class FormRequestLifecycleTest extends TestCase { + public function test_it_authorizes_and_validates_automatically() { + $request = new WP_REST_Request(); + $request->set_param( 'username', 'admin' ); + + $form_request = new StubFormRequest( $request ); + + // Validation should have run in the constructor. + $this->assertTrue( $form_request->with_validator_called, 'with_validator() should be called during instantiation.' ); + $this->assertEquals( 'admin', $form_request->get_param( 'username' ), 'Request should proxy params.' ); + } + + public function test_it_throws_exception_on_validation_failure() { + $this->expectException( Exception::class ); + $this->expectExceptionCode( 422 ); + + $request = new WP_REST_Request(); + $request->set_param( 'username', 'not_alpha_123' ); // Fails alpha rule + + $form_request = new StubFormRequest( $request ); + } + + public function test_it_throws_403_on_authorization_failure() { + $this->expectException( \Exception::class ); + $this->expectExceptionMessage( 'Unauthorized' ); + // Checking if the code is actually set to 403 by FormRequest + + $request = new WP_REST_Request(); + $request->set_param( 'username', 'admin' ); + + // This is tricky, we need to set the property BEFORE the constructor runs if it evaluates inside. + // FormRequest doesn't let us pass it, so we'll mock or override auth in a specific class. + $class = new class($request) extends StubFormRequest { + public function authorize(): bool { + return false; } + }; + } +} diff --git a/tests/Unit/Localization/MessageFormattingTest.php b/tests/Unit/Localization/MessageFormattingTest.php new file mode 100644 index 0000000..f50aacf --- /dev/null +++ b/tests/Unit/Localization/MessageFormattingTest.php @@ -0,0 +1,157 @@ +set_param( 'email_field', 'invalid-email' ); + $request->set_param( 'min_field', 'abc' ); // string length 3, min 5 + + $rules = [ + 'email_field' => 'email', + 'min_field' => 'min:5', + 'required_field' => 'required', + ]; + + $validation = new Validation( $request, $rules ); + $errors = $validation->errors(); + + $this->assertContains( 'The email_field must be a valid email address.', $errors['email_field'] ); + $this->assertContains( 'The min_field must be at least 5 characters.', $errors['min_field'] ); + $this->assertContains( 'The required_field field is required.', $errors['required_field'] ); + } + + public function test_placeholder_replacement_in_complex_rules() { + $request = new WP_REST_Request( 'POST', '/test' ); + $request->set_param( 'digits_field', '123' ); // 3 digits, need 5 + $request->set_param( 'between_field', 10 ); // numeric between 1, 5 + + $rules = [ + 'digits_field' => 'digits:5', + 'between_field' => 'numeric|between:1,5', + ]; + + $validation = new Validation( $request, $rules ); + $errors = $validation->errors(); + + $this->assertContains( 'The digits_field must be 5 digits.', $errors['digits_field'] ); + $this->assertContains( 'The between_field must be between 1 and 5.', $errors['between_field'] ); + } + + public function test_custom_attribute_names_in_messages() { + $request = new WP_REST_Request( 'POST', '/test' ); + + $rules = [ + 'first_name' => 'required', + ]; + $custom_attributes = [ + 'first_name' => 'First Name', + ]; + + $validation = new Validation( $request, $rules, [], $custom_attributes ); + $errors = $validation->errors(); + + $this->assertContains( 'The First Name field is required.', $errors['first_name'] ); + } + + public function test_between_rule_with_types() { + $request = new WP_REST_Request( 'POST', '/test' ); + $request->set_param( 'string_between', 'abc' ); // 3 chars, need 5-10 + $request->set_param( 'numeric_between', 15 ); // need 1-5 + + $rules = [ + 'string_between' => 'between:5,10', + 'numeric_between' => 'numeric|between:1,5', + ]; + + $validation = new Validation( $request, $rules ); + $errors = $validation->errors(); + + $this->assertContains( 'The string_between must be between 5 and 10 characters.', $errors['string_between'] ); + $this->assertContains( 'The numeric_between must be between 1 and 5.', $errors['numeric_between'] ); + } + + public function test_date_rules_formatting() { + $request = new WP_REST_Request( 'POST', '/test' ); + $request->set_param( 'date_field', 'invalid' ); + $request->set_param( 'before_field', '2023-01-01' ); // need before 2022-01-01 + + $rules = [ + 'date_field' => 'date', + 'before_field' => 'before:2022-01-01', + ]; + + $validation = new Validation( $request, $rules ); + $errors = $validation->errors(); + + $this->assertContains( 'The date_field is not a valid date.', $errors['date_field'] ); + $this->assertContains( 'The before_field must be a date before 2022-01-01.', $errors['before_field'] ); + } + + public function test_accepted_and_confirmed_rules() { + $request = new WP_REST_Request( 'POST', '/test' ); + $request->set_param( 'terms', 'no' ); + $request->set_param( 'password', 'secret' ); + $request->set_param( 'password_confirmation', 'wrong' ); + + $rules = [ + 'terms' => 'accepted', + 'password' => 'confirmed', + ]; + + $validation = new Validation( $request, $rules ); + $errors = $validation->errors(); + + $this->assertContains( 'The terms must be accepted.', $errors['terms'] ); + $this->assertContains( 'The password confirmation does not match.', $errors['password'] ); + } + + public function test_custom_message_overrides() { + $request = new WP_REST_Request( 'POST', '/test' ); + $request->set_param( 'email_field', 'not-an-email' ); + $request->set_param( 'numeric_field', 'abc' ); + $request->set_param( 'required_field', '' ); + + $rules = [ + 'email_field' => 'email', + 'numeric_field' => 'numeric|min:10', + 'required_field' => 'required', + ]; + + $messages = [ + 'email_field.email' => 'CUSTOM EMAIL MESSAGE', + 'required' => 'GENERIC REQUIRED MESSAGE', + 'min.numeric' => 'CUSTOM NUMERIC MIN MESSAGE', + ]; + + $validation = new Validation( $request, $rules, $messages ); + $errors = $validation->errors(); + + $this->assertContains( 'CUSTOM EMAIL MESSAGE', $errors['email_field'] ); + $this->assertContains( 'CUSTOM NUMERIC MIN MESSAGE', $errors['numeric_field'] ); + $this->assertContains( 'GENERIC REQUIRED MESSAGE', $errors['required_field'] ); + } + + public function test_fluent_message_override_priority() { + $request = new WP_REST_Request( 'POST', '/test' ); + $request->set_param( 'email_field', 'not-an-email' ); + + $rules = [ + 'email_field' => ( new \WpMVC\RequestValidator\Rules\Email() )->message( 'FLUENT EMAIL MESSAGE' ), + ]; + + $messages = [ + 'email_field.email' => 'ARRAY EMAIL MESSAGE', + ]; + + $validation = new Validation( $request, $rules, $messages ); + $errors = $validation->errors(); + + $this->assertContains( 'FLUENT EMAIL MESSAGE', $errors['email_field'] ); + } +} diff --git a/tests/Unit/Performance/ValidationEngineBenchmarkTest.php b/tests/Unit/Performance/ValidationEngineBenchmarkTest.php new file mode 100644 index 0000000..2d8fef7 --- /dev/null +++ b/tests/Unit/Performance/ValidationEngineBenchmarkTest.php @@ -0,0 +1,42 @@ +set_param( 'field1', 'john@example.com' ); + $request->set_param( 'field2', '12345' ); + + $rules = [ + 'field1' => 'required|email|max:50', + 'field2' => 'required|numeric|digits_between:1,10' + ]; + + // Warm up and record baseline + $v = new Validation( $request, $rules ); + $v->passes(); + + $start_memory = memory_get_usage(); + + for ( $i = 0; $i < 1000; $i++ ) { + $val = new Validation( $request, $rules ); + $val->passes(); + } + + $end_memory = memory_get_usage(); + $diff = $end_memory - $start_memory; + + // Ensure memory usage doesn't grow by more than 2MB for 1000 iterations + // The singleton cache in RuleResolver guarantees objects aren't redundantly recreated + $this->assertLessThan( + 2 * 1024 * 1024, + $diff, + 'Validation engine has a significant memory leak. Leak size: ' . $diff . ' bytes.' + ); + } +} diff --git a/tests/Unit/Rules/Boundary/SizeBoundaryRuleTest.php b/tests/Unit/Rules/Boundary/SizeBoundaryRuleTest.php new file mode 100644 index 0000000..519d10a --- /dev/null +++ b/tests/Unit/Rules/Boundary/SizeBoundaryRuleTest.php @@ -0,0 +1,73 @@ +set_param( 'test', '12345' ); + $this->assertTrue( ( new Validation( $request, ['test' => 'min:5'] ) )->passes() ); + + $request->set_param( 'test', '1234' ); + $this->assertTrue( ( new Validation( $request, ['test' => 'min:5'] ) )->fails() ); + + // Numeric value + $request->set_param( 'test', 10 ); + $this->assertTrue( ( new Validation( $request, ['test' => 'numeric|min:10'] ) )->passes() ); + + $request->set_param( 'test', 5 ); + $this->assertTrue( ( new Validation( $request, ['test' => 'numeric|min:10'] ) )->fails() ); + } + + public function test_between_rule() { + $request = new WP_REST_Request(); + + $request->set_param( 'test', '123456' ); + $this->assertTrue( ( new Validation( $request, ['test' => 'between:5,10'] ) )->passes() ); + + $request->set_param( 'test', '1234' ); + $this->assertTrue( ( new Validation( $request, ['test' => 'between:5,10'] ) )->fails() ); + + $request->set_param( 'test', [1,2,3,4,5,6] ); + $this->assertTrue( ( new Validation( $request, ['test' => 'array|between:5,10'] ) )->passes() ); + + $request->set_param( 'test', [1,2,3] ); + $this->assertTrue( ( new Validation( $request, ['test' => 'array|between:5,10'] ) )->fails() ); + } + + public function test_digits_rule() { + $request = new WP_REST_Request(); + + $request->set_param( 'test', '123' ); + $this->assertTrue( ( new Validation( $request, ['test' => 'digits:3'] ) )->passes() ); + + $request->set_param( 'test', '12' ); + $this->assertTrue( ( new Validation( $request, ['test' => 'digits:3'] ) )->fails() ); + } + + public function test_digits_between_rule() { + $request = new WP_REST_Request(); + + $request->set_param( 'test', '123' ); + $this->assertTrue( ( new Validation( $request, ['test' => 'digits_between:2,4'] ) )->passes() ); + + $request->set_param( 'test', '1' ); + $this->assertTrue( ( new Validation( $request, ['test' => 'digits_between:2,4'] ) )->fails() ); + } + + /** + * Test that min rule treats non-numeric values as strings for length validation. + */ + public function test_min_rule_without_numeric_is_treated_as_string_length() { + $request = new WP_REST_Request(); + $request->set_param( 'age', '123' ); + $validation = new Validation( $request, ['age' => 'min:5'] ); + $this->assertTrue( $validation->fails(), 'Min rule without numeric should check string length.' ); + } +} diff --git a/tests/Unit/Rules/Dependency/ConditionalRuleTest.php b/tests/Unit/Rules/Dependency/ConditionalRuleTest.php new file mode 100644 index 0000000..4dda5cd --- /dev/null +++ b/tests/Unit/Rules/Dependency/ConditionalRuleTest.php @@ -0,0 +1,90 @@ +set_param( 'type', 'admin' ); + + // required_if:type,admin + $validation = new Validation( + $request, [ + 'password' => 'required_if:type,admin' + ] + ); + + $this->assertTrue( $validation->fails(), 'Required validation skipped when condition was met.' ); + + // provide the field + $request->set_param( 'password', 'secret' ); + $validation2 = new Validation( + $request, [ + 'password' => 'required_if:type,admin' + ] + ); + $this->assertTrue( $validation2->passes(), 'Required validation failed when field was provided.' ); + + // change condition so it's not required + $request->set_param( 'type', 'guest' ); + $request->set_param( 'password', '' ); + $validation3 = new Validation( + $request, [ + 'password' => 'required_if:type,admin' + ] + ); + $this->assertTrue( $validation3->passes(), 'Required validation ran even when condition was NOT met.' ); + } + + public function test_prohibited_unless_rule() { + $request = new WP_REST_Request(); + $request->set_param( 'type', 'guest' ); + $request->set_param( 'access_code', '123' ); + + // prohibited_unless:type,admin + // means access_code is prohibited UNLESS type is admin + // Since type is guest, it is prohibited! + $validation = new Validation( + $request, [ + 'access_code' => 'prohibited_unless:type,admin' + ] + ); + + $this->assertTrue( $validation->fails(), 'Prohibited rule allowed field when condition was NOT met.' ); + + // Provide the correct admin type, access_code should now be allowed (ignored) + $request->set_param( 'type', 'admin' ); + $validation2 = new Validation( + $request, [ + 'access_code' => 'prohibited_unless:type,admin' + ] + ); + $this->assertTrue( $validation2->passes(), 'Prohibited rule rejected field even though condition was met.' ); + } + + /** + * Test parameter resolution in RuleResolver specifically for required_if. + */ + public function test_required_if_resolution_with_string_value() { + $request = new WP_REST_Request( 'POST', '/test' ); + $request->set_body_params( + [ + 'type' => 'admin', + 'role' => '', + ] + ); + + $rules = [ + 'role' => 'required_if:type,admin', + ]; + + $validation = new Validation( $request, $rules ); + + $this->assertTrue( $validation->fails(), 'required_if should fail when the condition is met but field is empty.' ); + } +} diff --git a/tests/Unit/Rules/Format/NetworkFormatRuleTest.php b/tests/Unit/Rules/Format/NetworkFormatRuleTest.php new file mode 100644 index 0000000..0a287d2 --- /dev/null +++ b/tests/Unit/Rules/Format/NetworkFormatRuleTest.php @@ -0,0 +1,34 @@ +assertEquals( $is_ip, $ip_rule->passes( 'test', $value ) ); + + $ipv4_rule = RuleResolver::resolve( 'ipv4' ); + $this->assertEquals( $is_ipv4, $ipv4_rule->passes( 'test', $value ) ); + + $ipv6_rule = RuleResolver::resolve( 'ipv6' ); + $this->assertEquals( $is_ipv6, $ipv6_rule->passes( 'test', $value ) ); + } + + public function ipDataProvider() { + return [ + ['192.168.1.1', true, true, false], + ['10.0.0.0', true, true, false], + ['2001:0db8:85a3:0000:0000:8a2e:0370:7334', true, false, true], + ['::1', true, false, true], + ['not_an_ip', false, false, false], + ['256.256.256.256', false, false, false], + ['1200::AB00:1234::2552:7777:1313', false, false, false] // Double :: + ]; + } +} diff --git a/tests/Unit/Rules/Format/StringFormatRuleTest.php b/tests/Unit/Rules/Format/StringFormatRuleTest.php new file mode 100644 index 0000000..e2c92d8 --- /dev/null +++ b/tests/Unit/Rules/Format/StringFormatRuleTest.php @@ -0,0 +1,113 @@ +assertEquals( $expected, $rule->passes( 'test', $value ) ); + } + + public function emailDataProvider() { + return [ + ['test@example.com', true], + ['invalid-email', false], + ['test@sub.domain.com', true], + ['@missinguser.com', false] + ]; + } + + /** + * @dataProvider urlDataProvider + */ + public function test_url_rule( $value, $expected ) { + $rule = RuleResolver::resolve( 'url' ); + $this->assertEquals( $expected, $rule->passes( 'test', $value ) ); + } + + public function urlDataProvider() { + return [ + ['https://google.com', true], + ['http://example.com/path?query=1', true], + ['invalid-url', false], + ['ftp://domain.com', true], // filter_var validates ftp + ]; + } + + /** + * @dataProvider macAddressDataProvider + */ + public function test_mac_address_rule( $value, $expected ) { + $rule = RuleResolver::resolve( 'mac_address' ); + $this->assertEquals( $expected, $rule->passes( 'test', $value ) ); + } + + public function macAddressDataProvider() { + return [ + ['00:1A:2B:3C:4D:5E', true], + ['00-1A-2B-3C-4D-5E', true], + ['invalid-mac', false], + ['00:1A:2B:3C:4D:5Z', false] // Invalid hex + ]; + } + + /** + * @dataProvider alphaDataProvider + */ + public function test_alpha_rule( $value, $expected ) { + $rule = RuleResolver::resolve( 'alpha' ); + $this->assertEquals( $expected, $rule->passes( 'test', $value ) ); + } + + public function alphaDataProvider() { + return [ + ['OnlyLetters', true], + ['Letters123', false], + ['With Spaces', false], + ['special@', false] + ]; + } + + /** + * @dataProvider alphaDashDataProvider + */ + public function test_alpha_dash_rule( $value, $expected ) { + $rule = RuleResolver::resolve( 'alpha_dash' ); + $this->assertEquals( $expected, $rule->passes( 'test', $value ) ); + } + + public function alphaDashDataProvider() { + return [ + ['Letters-And_Dash', true], + ['letters123', true], + ['With Spaces', false], + ['special@', false] + ]; + } + + public function test_regex_rule() { + $rule = RuleResolver::resolve( 'regex', ['/^[A-Z]+$/'] ); + $this->assertTrue( $rule->passes( 'test', 'UPPERCASE' ) ); + $this->assertFalse( $rule->passes( 'test', 'lowercase' ) ); + + $not_regex = RuleResolver::resolve( 'not_regex', ['/^[A-Z]+$/'] ); + $this->assertFalse( $not_regex->passes( 'test', 'UPPERCASE' ) ); + $this->assertTrue( $not_regex->passes( 'test', 'lowercase' ) ); + } + + /** + * Test that rules like StartsWith handle non-string values gracefully. + */ + public function test_starts_with_handles_non_string_values() { + $rule = RuleResolver::resolve( 'starts_with', ['api'] ); + $this->assertFalse( $rule->passes( 'test', ['api', 'web'] ) ); + } +} diff --git a/tests/Unit/Rules/Type/PrimitiveTypeRuleTest.php b/tests/Unit/Rules/Type/PrimitiveTypeRuleTest.php new file mode 100644 index 0000000..eb1c350 --- /dev/null +++ b/tests/Unit/Rules/Type/PrimitiveTypeRuleTest.php @@ -0,0 +1,47 @@ +assertEquals( $expected, $rule->passes( 'test', $value ) ); + } + + public function typeDataProvider() { + return [ + ['numeric', '123', true], + ['numeric', 123.45, true], + ['numeric', 'abc', false], + + ['integer', '123', true], + ['integer', 123, true], + ['integer', 123.45, false], + + ['boolean', true, true], + ['boolean', false, true], + ['boolean', 1, true], + ['boolean', 0, true], + ['boolean', '1', true], + ['boolean', '0', true], + ['boolean', 'true', true], + ['boolean', 'false', true], + ['boolean', 'on', false], // Unlike larval we're strictly checking boolean filter wait - let's see. PHP filter checks on? + + ['array', [], true], + ['array', ['a'], true], + ['array', 'string', false], + + ['json', '{"key":"value"}', true], + ['json', '[1, 2, 3]', true], + ['json', 'not json', false], + ['json', '{"key":}', false], + ]; + } +} diff --git a/tests/Unit/SampleTest.php b/tests/Unit/SampleTest.php deleted file mode 100644 index 94228e1..0000000 --- a/tests/Unit/SampleTest.php +++ /dev/null @@ -1,12 +0,0 @@ -assertTrue( true ); - } -} diff --git a/tests/Unit/Security/XssPreventionTest.php b/tests/Unit/Security/XssPreventionTest.php new file mode 100644 index 0000000..5860036 --- /dev/null +++ b/tests/Unit/Security/XssPreventionTest.php @@ -0,0 +1,39 @@ +set_param( 'payload', '' ); + + $validation = new Validation( + $request, [ + 'payload' => 'alpha' + ] + ); + + $this->assertTrue( $validation->fails(), 'Alpha rule incorrectly allowed HTML/JS syntax.' ); + $this->assertArrayHasKey( 'payload', $validation->errors() ); + } + + public function test_it_rejects_xss_in_emails() { + $request = new WP_REST_Request(); + + $request->set_param( 'email', '">@example.com' ); + + $validation = new Validation( + $request, [ + 'email' => 'email' + ] + ); + + $this->assertTrue( $validation->fails(), 'Email rule incorrectly allowed XSS payload.' ); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index c686acb..62c0039 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,4 +1,5 @@