diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3f7036b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +/.git +/.settings +README.* +LICENSE +CHANGELOG.* +/docs +*.md +/.buildpath +/.project +/.gitignore +/.dockerignore +/temp-test +/build.sh \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1b0acdf --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/modules/* +!/modules/README.md +/storage/* +!/storage/README.md +/temp-test +/composer.lock +/vendor diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..74ae122 --- /dev/null +++ b/.htaccess @@ -0,0 +1,41 @@ + + SetEnv TZ Europe/Moscow + +ServerSignature Off +AddDefaultCharset UTF-8 +Options -Indexes +DirectoryIndex index.php + +# Bad Rquest +#ErrorDocument 400 /400.html +# Authorization Required +#ErrorDocument 401 /401.html +# Forbidden +ErrorDocument 403 /error.php?code=403 +# Not found +ErrorDocument 404 /error.php?code=404 +# Method Not Allowed +ErrorDocument 405 /error.php?code=405 +# Request Timed Out +#ErrorDocument 408 /408.html +# Request URI Too Long +#ErrorDocument 414 /414.html +# Internal Server Error +ErrorDocument 500 /error.php?code=500 +# Not Implemented +ErrorDocument 501 /error.php?code=501 +# Bad Gateway +#ErrorDocument 502 /502.html +# Service Unavailable +#ErrorDocument 503 /503.html +# Gateway Timeout +#ErrorDocument 504 /504.html + +IndexIgnore * + +## Mod_rewrite in use. + +RewriteEngine On +RewriteCond %{REQUEST_FILENAME} !-f +RewriteRule . /index.php [L] + diff --git a/Autoloader.php b/Autoloader.php new file mode 100644 index 0000000..735f858 --- /dev/null +++ b/Autoloader.php @@ -0,0 +1,24 @@ + \ No newline at end of file diff --git a/Dockerfile.sample b/Dockerfile.sample new file mode 100644 index 0000000..4f4f87e --- /dev/null +++ b/Dockerfile.sample @@ -0,0 +1,36 @@ +FROM mxfox.ru/chimera/fox-web-basic:latest AS prepare + +RUN apt-get update \ + && apt-get install curl -y \ + && cd /tmp \ + && curl -sS https://getcomposer.org/installer -o composer-setup.php \ + && php composer-setup.php --install-dir=/usr/local/bin --filename=composer \ + && DEBIAN_FRONTEND=noninteractive TZ=Europe/Moscow apt-get -y install tzdata \ + && cd /var/www/html + +COPY . /var/www/html + +RUN composer install + +RUN find . -name "*~" -prune -exec rm -rf '{}' \; \ + && find . -name "*.bak" -prune -exec rm -rf '{}' \; \ + && find . -name "*.old" -prune -exec rm -rf '{}' \; \ + && find . -name ".git" -prune -exec rm -rf '{}' \; \ + && find . -name ".settings" -prune -exec rm -rf '{}' \; \ + && find . -name ".buildpath" -prune -exec rm -rf '{}' \; \ + && find . -name ".project" -prune -exec rm -rf '{}' \; \ + && find . -name "README.*" -prune -exec rm -rf '{}' \; \ + && find . -name "*.md" -prune -exec rm -rf '{}' \; \ + && find . -name "composer.*" -prune -exec rm -rf '{}' \; \ + && find . -name ".travis*" -prune -exec rm -rf '{}' \; \ + && find . -name "installed.json" -prune -exec rm -rf '{}' \; + +RUN find . -type d ! -path './.git/**' ! -path './static/**' ! -path "./static" ! -path ./*/modules/*/static* -exec bash -c 'test -f {}/.htaccess && echo -n "[ SKIP ] " || (cp ./docker-build/.htaccess {} && echo -n "[ ADD ] ") && echo {}/.htaccess' \; + +RUN rm -f composer.* \ + && rm -rf docker-build + + +FROM mxfox.ru/chimera/fox-web-basic:latest as build +COPY --from=prepare /var/www/html /var/www/html +COPY docker-build/rootfs / diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..61d1860 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ +GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. \ No newline at end of file diff --git a/README.md b/README.md index e69de29..edf412f 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,9 @@ +# Chimera Fox Platform Mark2 + +Полностью новая версия платформы Chimera Fox. + +Предыдущие версии будут поддерживаться только в качестве обновлений безопасности. + +Обратная совместимость с предыдущими версиями отсутствует - это основная причина выхода новой платформы - совместимость со старыми версиями занимает слишком много ресурсов на поддержку, а фактически она уже не нужна. + +*Актуальные модули старой версии будут портированы на Mark2* \ No newline at end of file diff --git a/api/.htaccess b/api/.htaccess new file mode 100644 index 0000000..5f39d91 --- /dev/null +++ b/api/.htaccess @@ -0,0 +1,5 @@ +RewriteEngine On +RewriteRule '^v2/' v2.php [END] +RewriteRule '^v2$' v2.php [END] +RewriteRule . - [END,R=404] + diff --git a/api/404.html b/api/404.html new file mode 100644 index 0000000..563324f --- /dev/null +++ b/api/404.html @@ -0,0 +1 @@ +0000404 \ No newline at end of file diff --git a/api/v2.php b/api/v2.php new file mode 100644 index 0000000..dbe6164 --- /dev/null +++ b/api/v2.php @@ -0,0 +1,112 @@ +module !== 'api') { + throw new foxException("Invalid request",400); + } + + $request->shift(); + + if ($request->module !== "v2") { + throw new foxException("Invalid API version",400); + } + $request->shift(); + $modules=moduleInfo::getAll(); + if (!array_key_exists(request::get()->module, $modules)) { + if (request::get()->authOK) { + throw new foxException("Invalid module",404); + } else { + throw new foxException("Unauthorized",401); + } + } + + if ($modules[request::get()->module]->authRequired && !request::get()->authOK) { + throw new foxException("Unauthorized",401); + } + + $modNS=$modules[request::get()->module]->namespace; + request::get()->shift(); + $className=$modNS."\\".request::get()->module; + if (!class_exists($className)) { + throw new foxException("Not found",404); + } + + if(!is_a($className, fox\externalCallable::class,true)) { + throw new foxException("Not found",404); + } + + ob_clean(); + $apiMethod=fox\common::clearInput($request->method,"A-Z"); + $apiFunction=fox\common::clearInput($request->function,"a-zA-Z0-9"); + $apiXFunction=empty($request->parameters[0])?NULL:fox\common::clearInput($request->parameters[0],"a-zA-Z0-9"); + + $apiCallMethod="API_".$apiMethod."_".$apiFunction; + $apiXCallMethod="APIX_".$apiMethod."_".$apiXFunction; + $apiZCallMethod="API_".$apiMethod; + + if (method_exists($className, $apiCallMethod)) { + $rv=$className::$apiCallMethod($request); + } else if (($apiXFunction!==null) && method_exists($className, $apiXCallMethod)) { + $rv=$className::$apiXCallMethod($request); + } else if (method_exists($className, $apiZCallMethod)) { + $rv=$className::$apiZCallMethod($request); + } else if (method_exists($className, "APICall")) { + $rv=$className::apiCall(request::get()); + } else { + throw new foxException("Method not allowed", 405); + } + + foxRequestResult::throw("200", "OK", $rv); + +} catch (fox\foxRequestResult $e) { + ob_clean(); + header('HTTP/1.0 '.$e->getCode().' '.$e->getMessage(), true, $e->getCode()); + if ($e->retVal===null) { + print json_encode(["status"=>$e->getMessage()]); + } else { + print json_encode($e->retVal); + } + exit; +} catch (fox\foxException $e) { + if (($e->getCode()>=400 && $e->getCode()<500) || ($e->getCode() == 501) || ($e->getCode() >= 600 && $e->getCode()<900)) { + trigger_error($e->getStatus().": ".$e->getCode().": ".$e->getMessage()." in ".$e->getFile()." at line ".$e->getLine(), E_USER_WARNING); + print(json_encode(["error"=>["code"=>$e->getCode(),"message"=>$e->getMessage(), "xCode"=>$e->getXCode()]])); + if ($e->getCode()>=400 && $e->getCode()<502) { + header('HTTP/1.0 '.$e->getCode().' '.$e->getMessage(), true, $e->getCode()); + } else { + header('HTTP/1.0 501 '.$e->getMessage(), true, $e->getCode()); + } + } else { + trigger_error($e->getStatus().": ".$e->getCode().": ".$e->getMessage()." in ".$e->getFile()." at line ".$e->getLine(), E_USER_WARNING); + print(json_encode(["error"=>["errCode"=>500,"message"=>"Internal server error","xCode"=>$e->getXCode()]])); + header('HTTP/1.0 500 Internal server error', true, 500); + } + exit; +} catch (Exception $e) { + trigger_error($e->getCode().": ".$e->getMessage()." in ".$e->getFile()." at line ".$e->getLine(), E_USER_WARNING); + print(json_encode(["error"=>["errCode"=>500,"message"=>"Internal server error", "xCode"=>"ERR"]])); + header('HTTP/1.0 500 Internal server error', true, 500); + throw($e); + exit; +} +exit; + +?> \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..575639a --- /dev/null +++ b/build.sh @@ -0,0 +1,4 @@ +#!/bin/bash +# Not implemented yet +source config +docker build . -t x diff --git a/cli/.htaccess b/cli/.htaccess new file mode 100644 index 0000000..93169e4 --- /dev/null +++ b/cli/.htaccess @@ -0,0 +1,2 @@ +Order deny,allow +Deny from all diff --git a/cli/initialize.php b/cli/initialize.php new file mode 100755 index 0000000..697182b --- /dev/null +++ b/cli/initialize.php @@ -0,0 +1,77 @@ +#!/usr/bin/php +name, modules::pseudoModules) && !$mod->getInstances()) { + print "Install module ".$mod->name."..."; + $mod->save(); + print "OK\n"; + } + } +//} + +if (company::getCount()==0) { + + $c = new fox\company(); + $c->name="Default company"; + $c->qName="Company"; + print "Create company ".$c->name."..."; + $c->save(); + print "OK\n"; +} + +// create user +if (user::getCount()==0 && userGroup::getCount()==0) { + $u = new fox\user(); + $u->fullName="Administrator"; + $u->login=$initUser; + $u->setPassword($initPass); + $u->authType="internal"; + print "Create user ".$u->login."..."; + $u->save(); + + print "OK\n"; + + $ug = new fox\userGroup(); + $ug->name="Administrators"; + $ug->addAccessRule("isRoot"); + print "Create usergroup ".$ug->name."..."; + $ug->save(); + print "OK\n"; + print "Join user into ".$u->login." group ".$ug->name."..."; + $ug->join($u); + print "OK\n"; +} + + + + + +?> \ No newline at end of file diff --git a/cli/migration.php b/cli/migration.php new file mode 100755 index 0000000..5c6b7b4 --- /dev/null +++ b/cli/migration.php @@ -0,0 +1,28 @@ +#!/usr/bin/php +name="core"; + $module->instanceOf="core"; + $module->namespace="fox"; +} + +fox\sql::doMigration($module,__DIR__."/../core/fox"); + +?> \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..f2df9ba --- /dev/null +++ b/composer.json @@ -0,0 +1,7 @@ +{ + "require" : { + "aws/aws-sdk-php" : "^3.208", + "html2text/html2text" : "^4.3", + "phpmailer/phpmailer" : "~6.5.3" + } +} \ No newline at end of file diff --git a/core/.htaccess b/core/.htaccess new file mode 100644 index 0000000..e69de29 diff --git a/core/fox/.htaccess b/core/fox/.htaccess new file mode 100644 index 0000000..93169e4 --- /dev/null +++ b/core/fox/.htaccess @@ -0,0 +1,2 @@ +Order deny,allow +Deny from all diff --git a/core/fox/UID.php b/core/fox/UID.php new file mode 100644 index 0000000..09f51d0 --- /dev/null +++ b/core/fox/UID.php @@ -0,0 +1,133 @@ + [ + "type" => "VARCHAR(255)", + "nullable" => false, + "index" => "INDEX" + ], + "class" => [ + "type" => "VARCHAR(255)", + "nullable" => false + ] + ]; + + public function __get($key) + { + if (! empty($this->id) && $this->__loaded == false) { + $this->fill($this->id); + } + + return parent::__get($key); + } + + public function __fromString($val) + { + if (! static::check($val)) { + throw new Exception("Invalid UID format"); + } + $uid = substr(static::clear($val), 0, 9); + $this->id = $uid - $this->getOffset(); + } + + public static function clear($code) + { + return preg_replace('![^0-9]+!', '', $code); + } + + public function print() + { + return (substr($this->__toString(), 0, 4) . "-" . substr($this->__toString(), 4, 4) . "-" . substr($this->__toString(), 8, 2)); + } + + public static function check($code) + { + $code = static::clear($code); + if (strlen($code) != 10) { + return false; + } + return (substr($code, 9, 1) == self::checksum($code)); + } + + protected static function checksum($code) + { + $sum1 = substr($code, 1, 1) + substr($code, 3, 1) + substr($code, 5, 1) + substr($code, 7, 1); + $sum1 = $sum1 * 3; + $sum2 = substr($code, 0, 1) + substr($code, 2, 1) + substr($code, 4, 1) + substr($code, 6, 1) + substr($code, 8, 1); + $sum = $sum1 + $sum2; + $ceil = ceil(($sum / 10)) * 10; + $delta = $ceil - $sum; + return $delta; + } + + public static $sqlTable = "tblRegistry"; + + protected function getOffset() + { + $offset = config::get("UIDOffset"); + if (! is_numeric($offset)) { + $offset = 0; + } elseif ($offset > 90) { + $offset = 90; + } + $offset = 100000000 + ($offset * 1000000); + return $offset; + } + + public function __toString(): string + { + if (empty($this->id)) { + return ""; + } else { + $code = $this->getOffset() + $this->id; + return $code . static::checksum($code); + } + } + + public function isNull(): bool + { + return ($this->id === null); + } + + public function issue($instance, $class) + { + $this->instance = $instance; + $this->class = $class; + $this->save(); + } + + public static function qIssue($instance, $class) { + $uid = new static(); + $uid->issue($instance, $class); + return $uid; + } + + public function jsonSerialize() { + return $this->__toString(); + } +} +?> \ No newline at end of file diff --git a/core/fox/auth.php b/core/fox/auth.php new file mode 100644 index 0000000..039f46a --- /dev/null +++ b/core/fox/auth.php @@ -0,0 +1,31 @@ +quickExec1Line("select * from `" . user::$sqlTable . "` where `login` = '" . common::clearInput($login) . "' and `secret` = '" . xcrypt::hash($password) . "'"); + if ($res) { + $u = new user($res); + return $u; + } else { + return false; + } + } +} + +?> \ No newline at end of file diff --git a/core/fox/auth/.htaccess b/core/fox/auth/.htaccess new file mode 100644 index 0000000..93169e4 --- /dev/null +++ b/core/fox/auth/.htaccess @@ -0,0 +1,2 @@ +Order deny,allow +Deny from all diff --git a/core/fox/auth/login.php b/core/fox/auth/login.php new file mode 100644 index 0000000..40d8c26 --- /dev/null +++ b/core/fox/auth/login.php @@ -0,0 +1,56 @@ +method) { + case "POST": + if ($request->authOK) { + return; + } + if (! (gettype($request->requestBody) == "object" && property_exists($request->requestBody, "login") && property_exists($request->requestBody, "password"))) { + throw new foxException("Bad request", 400); + } + + $type = "API"; + if (property_exists($request->requestBody, "type")) { + $type = $request->requestBody->type; + } + + if ($u = auth::doAuth($request->requestBody->login, $request->requestBody->password)) { + $t = authToken::issue($u, $type); + return [ + "token" => $t->token, + "expire" => $t->expireStamp->isNull() ? "Never" : $t->expireStamp + ]; + } else { + throw new foxException("Authorization failed", 401); + } + break; + default: + throw new foxException("Bad request", 400); + } + } +} + +?> \ No newline at end of file diff --git a/core/fox/auth/oauth.php b/core/fox/auth/oauth.php new file mode 100644 index 0000000..0df4164 --- /dev/null +++ b/core/fox/auth/oauth.php @@ -0,0 +1,71 @@ +method) { + case "GET": + try { + $profile = new oAuthProfile(common::clearInput($request->function,"0-9")); + } catch (\Exception $e) { + if ($e->getCode()==691) { + throw new foxException("Not found",404); + } else { + throw $e; + } + } + if ($profile->deleted || !$profile->enabled) { + throw new foxException("Not found",404); + } + return [ + "id"=>$profile->id, + "name"=>$profile->name, + "url"=>$profile->getClient(config::get("SITEPREFIX")."/auth/oauth")->getAuthURL(), + "icon"=>$profile->getClient(config::get("SITEPREFIX"))->getAuthIcon(), + ]; + break; + + case "POST": + $profile = oAuthProfile::getByHash(common::clearInput($request->requestBody->hash)); + $oac = $profile->getClient(config::get("SITEPREFIX")."/auth/oauth"); + $xTokens=$oac->getTokenByCode(common::clearInput($request->requestBody->code)); + $userInfo=$oac->getUserInfo(); + $userRefId=$profile->id.":".$userInfo->sub; + $u=user::getByRefID("oauth",$userRefId); + + if (!$u) { + foxException::throw("ERR", "User not registered",401,"UNR"); + } + + $t = authToken::issue($u, "WEB"); + return [ + "token" => $t->token, + "expire" => $t->expireStamp->isNull() ? "Never" : $t->expireStamp + ]; + + return $u; + break; + } + } +} \ No newline at end of file diff --git a/core/fox/auth/register.php b/core/fox/auth/register.php new file mode 100644 index 0000000..268cd65 --- /dev/null +++ b/core/fox/auth/register.php @@ -0,0 +1,190 @@ +requestBody->email,"0-9A-Za-z_.@-"); + $regCode = common::clearInput($request->requestBody->regCode,"0-9"); + + $authType = explode("_", $request->requestBody->authType)[0]; + $ic=null; + if (!empty($regCode)) { + $ic=userInvitation::getByCode($regCode); + if (!$ic || ($ic->expireStamp->stamp>time())) { + $ic=null; + } + } else { + $ic =userInvitation::getByEMail($eMail); + } + + if ($authType=="oauth") { + $profile = oAuthProfile::getByHash(common::clearInput($request->requestBody->oAuthHash)); + $oac = $profile->getClient(config::get("SITEPREFIX")."/auth/oauth"); + $oac->getTokenByCode(common::clearInput($request->requestBody->oAuthCode)); + $userInfo=$oac->getUserInfo(); + $userRefId=$profile->id.":".$userInfo->sub; + $u=user::getByRefID("oauth",$userRefId); + + if (!$u) { + + $u = new user(); + // $u->login=uniqid(); + $u->eMail=$eMail; + $u->authType="oauth"; + $u->authRefId=$userRefId; + $u->fullName=$userInfo->name; + $u->save(); + } + + } elseif ($authType=="password") { + $login = common::clearInput($request->requestBody->login,"0-9A-Za-z_.-"); + $passwd = $request->requestBody->password; + if (user::getByLogin($login)) { + foxException::throw("ERR","Login already registered","409","LAR"); + } + + if (preg_match("/^[0-9]/",$login)) { + foxException::throw("ERR","Invalid login format num","406","ILF"); + } + + if (strlen($login) < 5) { + foxException::throw("ERR","Invalid login format len","406","ILF"); + } + + if (strlen($passwd) < static::minPasswordLength) { + foxException::throw("ERR","Invalid password format len","406","IPF"); + } + + $u=new user(); + $u->login=$login; + $u->fullName=common::clearInput($request->requestBody->fullName,"0-9A-Za-zА-Яа-я ._-"); + $u->authType="internal"; + $u->eMail=$eMail; + $u->setPassword($passwd); + $u->save(); + } + + if ($ic) { + if ($ic && ($ic->expireStamp->stamp<=time())) { + foreach ($ic->joinGroupsId as $grid) { + $group = new userGroup($grid); + $group->join($u); + } + } + if (!$ic->allowMultiUse) { $ic->delete(); } + } + + try { + $u->sendEMailConfirmation(); + } catch (\Exception $e) { + trigger_error($e->getMessage()); + } + + $t = authToken::issue($u, "WEB"); + return [ + "token" => $t->token, + "expire" => $t->expireStamp->isNull() ? "Never" : $t->expireStamp + ]; + } + + public static function API_POST_recovery(request $request) { + $eMail = common::clearInput($request->requestBody->email,"0-9A-Za-z_.@-"); + if (!common::validateEMail($eMail)) { foxException::throw("ERR","Invalid eMail format",406,"IMF");} + $u=user::getByEmail($eMail); + if (!$u || $u->authType!=='internal' || !$u->eMailConfirmed) { foxException::throw("ERR","Not found",404,"URNF");} + $u->sendPasswordRecovery(); + } + + public static function API_POST_validateRecovery(request $request) { + $code = common::clearInput($request->requestBody->code,"0-9"); + $eMail = common::clearInput($request->requestBody->email,"0-9A-Za-z_.@-"); + if (!common::validateEMail($eMail)) { foxException::throw("ERR","Invalid eMail format",406,"IMF");} + $u=user::getByEmail($eMail); + if (!$u || $u->authType!=='internal' || !$u->eMailConfirmed) { foxException::throw("ERR","Not found",404,"URNF");} + if ($u->validateRecoveryCode($code)) { + return; + } else { + foxException::throw("ERR", "Validation failed", 400,"IVCC"); + } + } + + public static function API_POST_setNewPassword(request $request) { + $code = common::clearInput($request->requestBody->code,"0-9"); + $eMail = common::clearInput($request->requestBody->email,"0-9A-Za-z_.@-"); + $passwd=$request->requestBody->newPasswd; + if (strlen($passwd) < static::minPasswordLength) { + foxException::throw("ERR","Invalid password format len","406","IPF"); + } + if (!common::validateEMail($eMail)) { foxException::throw("ERR","Invalid eMail format",406,"IMF");} + $u=user::getByEmail($eMail); + if (!$u || $u->authType!=='internal' || !$u->eMailConfirmed) { foxException::throw("ERR","Not found",404,"URNF");} + if ($u->validateRecoveryCode($code,true)) { + + $u->setPassword($passwd); + $u->save(); + + + $t = authToken::issue($u, "WEB"); + return [ + "token" => $t->token, + "expire" => $t->expireStamp->isNull() ? "Never" : $t->expireStamp + ]; + } else { + foxException::throw("ERR", "Validation failed", 400,"IVCC"); + } + } + + protected static function validate(request $request) { + $eMail = common::clearInput($request->requestBody->email,"0-9A-Za-z_.@-"); + $code = common::clearInput($request->requestBody->regCode,"0-9"); + $ic=null; + if (!empty($code)) { + $ic=userInvitation::getByCode($code); + + if (!$ic || (!$ic->expireStamp->isNull() && $ic->expireStamp->stamp \ No newline at end of file diff --git a/core/fox/auth/session.php b/core/fox/auth/session.php new file mode 100644 index 0000000..b4ba632 --- /dev/null +++ b/core/fox/auth/session.php @@ -0,0 +1,64 @@ +method) { + case "DELETE": + if (! ($request->authOK)) { + throw new foxException("Bad request", 501); + } + + $request->token->delete(); + return; + case "GET": + if ($request->authOK) { + $modules=[]; + $i = 0; + foreach (modules::listInstalled() as $mod) { + if (array_search("menu", $mod->features) !== false && $request->user->checkAccess($mod->globalAccessKey, $mod->name) && ! empty($mod->menuItem)) { + $i ++; + $modules[($mod->modPriority * 100) + $i] = [ + "name" => $mod->name, + "instanceOf" => $mod->instanceOf, + "menu" => $mod->menuItem, + "globalAccesKey" => $mod->globalAccessKey, + "languages" => $mod->languages + ]; + } + } + + return [ + "updated" => time(), + "user" => $request->token->user, + "acls" => $request->user->getAccessRules(), + "modules" => $modules + ]; + } + ; + throw new foxException("Unauthorized", 401); + break; + } + } +} + +?> \ No newline at end of file diff --git a/core/fox/authToken.php b/core/fox/authToken.php new file mode 100644 index 0000000..0c7bb56 --- /dev/null +++ b/core/fox/authToken.php @@ -0,0 +1,252 @@ + [ + "name" => "REST API", + "defaultTTL" => 10, + "allowLogin" => false, + "allowRenew" => true, + "renewTTL" => 30 + ], + "WEB" => [ + "name" => "WWW", + "defaultTTL" => 32, + "allowLogin" => true, + "allowRenew" => true, + "renewTTL" => 30 + ], + "APP" => [ + "name" => "Mobile APP", + "defaultTTL" => 10, + "allowLogin" => true, + "allowRenew" => true, + "renewTTL" => 30 + ] + ]; + + public string $type = "API"; + + public static $allowDeleteFromDB = true; + + public static $sqlTable = "tblAuthTokens"; + + public static $sqlColumns = [ + "userId" => [ + "type" => "INT", + "index" => "INDEX", + "nullable" => false + ], + "token1" => [ + "type" => "CHAR(64)", + "index" => "UNIQUE", + "nullable" => true + ], + "token2" => [ + "type" => "CHAR(64)", + "nullable" => false + ], + "token2b" => [ + "type" => "CHAR(64)", + "nullable" => true + ] + ]; + + public function __xConstruct() + { + $this->issueStamp = new time(time()); + $this->expireStamp = new time(); + $this->renewStamp = new time(); + } + + public static function dropExpired() + { + $sql = sql::getConnection(); + $sql->quickExec("delete from `" . static::$sqlTable . "` where `expireStamp` is not NULL and `expireStamp` < NOW()"); + } + + public static function getByToken(string $token) + { + if (strlen($token) != 128) { + return null; + } + $token1 = substr($token, 0, 64); + $token2 = substr($token, 64, 64); + + $tx = new static(); + $sql = $tx->getSql(); + $sql->quickExec("START TRANSACTION"); + $row = $sql->quickExec1Line($tx->__sqlSelectTemplate . " where `i`.`token1` = '" . $token1 . "' AND (`expireStamp` is NULL OR `expireStamp` >= NOW()) FOR UPDATE"); + + $t = new static($row, $sql); + + $renewTTL = config::get("TOKEN_RENEW_" . $t->type) === null ? static::tokenTypes[$t->type]["renewTTL"] : config::get("TOKEN_RENEW_" . $t->type); + + $rv=null; + if (time() > $t->expireStamp->stamp) { + // expired, delete + // $t->delete(); + trigger_error("Token expired"); + } else if (time() < $t->renewStamp->stamp && time() < $t->expireStamp->stamp && $token1 == $t->token1 && ($token2 == $t->token2 || $token2 == $t->token2b)) { + // token OK (not expired, match 2A or 2B + $rv= $t; + } else if (time() > $t->renewStamp->stamp && time() < $t->expireStamp->stamp && $t->token1 == $token1 && $t->token2 == $token2) { + // renew expired, 2A matched - renew + trigger_error("Token renew started"); + $t->renew(); + header("X-Fox-Token-Renew: " . $t->token); + trigger_error("Token renew completed"); + $rv= $t; + } else if (time() < $t->expireStamp->stamp && time() > ($t->renewStamp->stamp + $renewTTL / 2) && $t->token1 == $token1 && $t->token2 != $token2 && $t->token2b == $token2) { + // RenewExpired, renew+renewTTL/2 - not expired, 2A failed, 2B matched - not update, void conflict, OK + trigger_error("Token2B used!"); + $rv= $t; + } else if ($t->token1 == $token1 && $t->token2 != $token2 && $token2 != $t->token2b) { + // token2 failed - token are compromised + trigger_error("Token #".$t->id." are compromised!"); + // $t->delete(); + } else { + + } + + $sql->quickExec("COMMIT"); + + if ($rv===null) { + trigger_error("Token:: time: ".time()."; Expire: ".$t->expireStamp->stamp."dT(".(time() - $t->expireStamp->stamp)."); Renew: ".$t->renewStamp->stamp."dT(".(time() - $t->renewStamp->stamp).")"); + } + + return $rv; + } + + public static function issue(user $user, $type = null, ?int $expireInDays = null) + { + static::dropExpired(); + $t = new authToken(); + $t->userId = $user->id; + $type = strtoupper($type); + if (array_key_exists($type, static::tokenTypes)) { + $t->type = $type; + } else { + throw new foxException("Invalid token type"); + } + + if ($expireInDays === null) { + $expireInDays = config::get("TOKEN_TTL_" . $type) === null ? static::tokenTypes[$type]["defaultTTL"] : config::get("TOKEN_TTL_" . $type); + } + + $renewTTL = config::get("TOKEN_RENEW_" . $type) === null ? static::tokenTypes[$type]["renewTTL"] : config::get("TOKEN_RENEW_" . $type); + + $t->expireStamp = empty($expireInDays) ? (new time()) : (new time(time() + ($expireInDays * 86400))); + $t->renewStamp = empty($renewTTL) ? (new time()) : (new time(time() + ($renewTTL * 60))); + ; + + for ($i = 0; $i < 32; $i ++) { + $token = substr(preg_replace("/[-_+=\\/]/", "", base64_encode(random_bytes(64))), 0, 64); + if (static::getByToken($token) === null) { + break; + } + } + + if ($i >= 32) { + throw new foxException("Unable to find token in $i iterations"); + } + if ($i > 0) { + trigger_error("Token found in $i iterations"); + } + $t->token1 = $token; + $t->token2 = substr(preg_replace("/[-_+=\\/]/", "", base64_encode(random_bytes(64))), 0, 64); + $t->save(); + return $t; + } + + public function renew() + { + $expireAllowRenew = config::get("TOKEN_ALLOW_RENEW_" . $this->type) === null ? static::tokenTypes[$this->type]["allowRenew"] : (config::get("TOKEN_ALLOW_RENEW_" . $this->type) == "true"); + + $renewTTL = config::get("TOKEN_RENEW_" . $this->type) === null ? static::tokenTypes[$this->type]["renewTTL"] : config::get("TOKEN_RENEW_" . $this->type); + + $this->renewStamp = empty($renewTTL) ? (new time()) : (new time(time() + ($renewTTL * 60))); + ; + $this->token2b = $this->token2; + $this->token2 = substr(preg_replace("/[-_+=\\/]/", "", base64_encode(random_bytes(64))), 0, 64); + + if ($expireAllowRenew) { + $expireInDays = config::get("TOKEN_TTL_" . $this->type) === null ? static::tokenTypes[$this->type]["defaultTTL"] : config::get("TOKEN_TTL_" . $this->type); + $this->expireStamp = empty($expireInDays) ? (new time()) : (new time(time() + ($expireInDays * 86400))); + } else { + if ($this->expireStamp < $this->renewStamp) { + $this->renewStamp = $this->expireStamp; + } + } + $this->save(); + return $this->token; + } + + public function __get($key) + { + switch ($key) { + case "token": + return $this->token1 . $this->token2; + + case "user": + if (empty($this->__user) && ! empty($this->userId)) { + $this->__user = new user($this->userId); + } + return $this->__user; + default: + return parent::__get($key); + } + } +} +?> \ No newline at end of file diff --git a/core/fox/baseClass.php b/core/fox/baseClass.php new file mode 100644 index 0000000..bac1134 --- /dev/null +++ b/core/fox/baseClass.php @@ -0,0 +1,586 @@ + $conf) { + if ($conf["type"] !== "SKIP") { + $rv[$key] = static::$sqlColumns[$key]; + } + } + + foreach ($this as $key => $val) { + if (array_key_exists($key, static::$sqlColumns)) { + continue; + } + if (preg_match("/^[^_][^_].*/", $key) && (array_search($key, array_merge(static::$excludeProps, static::$excludePropsBase)) === false)) { + $type = null; + $idx = null; + $null = null; + switch (gettype($val)) { + case "NULL": + if ($key == static::$sqlIdx) { + $type = "INT"; + $idx = "AI"; + $null = false; + } else { + throw new \Exception("Invalid type conversion for key $key at " . get_class($this)); + } + break; + case "array": + $type = "TEXT"; + break; + case "integer": + $type = "INT"; + break; + case "string": + $type = "VARCHAR(255)"; + break; + case "boolean": + $type = "INT"; + break; + + case "object": + if (! empty($val::$SQLType)) { + $type = $val::$SQLType; + } elseif (($val instanceof stringExportable)) { + $type = "VARCHAR(255)"; + } elseif ($val instanceof \JsonSerializable) { + $type = "VARCHAR(255)"; + } else { + throw new \Exception("Invalid type conversion for key $key at " . get_class($this)); + } + break; + default: + throw new \Exception("Invalid type conversion for key $key at " . get_class($this)); + break; + } + $rv[$key]["type"] = $type; + if ($idx) { + $rv[$key]["index"] = $idx; + } + if ($null !== null) { + $rv[$key]["nullable"] = $null; + } + if ($key == static::$sqlIdx) { + $rv[$key]["first"] = true; + } else { + $rv[$key]["first"] = false; + } + } + } + return $rv; + } + + protected function checkSql() + { + if ($this->sql === null) { + $this->sql = sql::getConnection(); + } + return; + } + + protected function __xConstruct() + {} + + public function __construct($id = null, ?namespace\sql $sql = null, $prefix = null, $settings = null) + { + # How to call from child template: + # parent::__construct($id, $sql, $prefix, $settings); + $this->__settings = $settings; + if (empty($this::$sqlSelectTemplate) and ! empty($this::$sqlTable)) { + $this->__sqlSelectTemplate = "select * from `" . $this::$sqlTable . "` as `i`"; + } else { + $this->__sqlSelectTemplate = $this::$sqlSelectTemplate; + } + + if (isset($sql)) { + $this->sql = &$sql; + } + $this->fillPrefix = $prefix; + + $this->__xId = $id; + + if ($this->__xConstruct() === false) { + // stop autoload content if __xConstruct return FALSE; + return; + } + + switch (gettype($id)) { + case "array": + $this->fillFromRow($id); + break; + case "string": + if ($this instanceof stringImportable) { + $this->__fromString($id); + } elseif (is_numeric($id)) { + $this->fill($id); + } elseif ($x = json_decode($id)) { + $this->fillFromRow($x); + } else { + throw new \Exception("Invalid input format", 597); + } + break; + case "integer": + $this->fill($id); + break; + case "NULL": + break; + default: + throw new \Exception("Invalid type " . gettype($id) . " for " . get_class($this) . "->__construct", 591); + break; + } + } + + protected function fill($id) + { + if (! empty($this->__sqlSelectTemplate)) { + $this->checkSql(); + $row = $this->sql->quickExec1Line($this->__sqlSelectTemplate . " where `i`." . $this::$sqlIdx . " = '" . $id . "'"); + if (! empty($row)) { + $this->fillFromRow($row); + } else { + throw new \Exception("Record with " . (static::$sqlIdx) . " " . $id . " not found in " . get_class($this), 691); + } + } else { + throw new \Exception("Fill by ID not implemented in " . get_class($this), 592); + } + } + + protected function fillFromRow($row) + { + foreach ($row as $key => $val) { + if (! empty($this->fillPrefix)) { + if (! preg_match("/^" . $this->fillPrefix . "/", $key)) { + continue; + } + $key = preg_replace("/^" . $this->fillPrefix . "/", "", $key); + } + + if (property_exists($this, $key) || property_exists($this, "__" . $key)) { + if (property_exists($this, "__" . $key)) { + $key = "__" . $key; + } + + if (gettype($this->{$key}) == 'boolean') { + $this->{$key} = $val == 1; + } elseif ((($this->{$key}) instanceof jsonImportable) || (($this->{$key}) instanceof stringImportable)) { + $typeof = get_class(($this->{$key})); + $this->{$key} = new $typeof($val); + } elseif (gettype($this->{$key}) == "array") { + if (gettype($val) == "string") { + $this->{$key} = (array) json_decode($val, true); + } elseif (gettype($val) == "array") { + $this->{$key} = $val; + } elseif ($val === null) { + $this->{$key} = []; + } elseif (gettype($val) == "object") { + $this->{$key} = (array) $val; + } else { + throw new Exception("Invalid type " . gettype($val) . " for " . $key . " in " . get_class($this)); + } + } else { + $this->{$key} = $val; + } + } + } + } + + public function save() + { + if (! $this->validateSave()) { + return false; + } + $this->checkSql(); + + if (property_exists($this, static::$sqlIdx) && ($this->{static::$sqlIdx} == null)) { + return $this->create(); + } else { + + $class = get_class($this); + if (is_numeric($this->{static::$sqlIdx})) { + $ref = new $class((int) $this->{static::$sqlIdx}); + } else { + $ref = new $class($this->{static::$sqlIdx}); + } + + $this->changelog = ""; + foreach ($this as $key => $val) { + + if ((array_search($key, array_merge(static::$excludeProps, static::$excludePropsBase)) === false) && $ref->{$key} != $this->{$key}) { + $stringRef = (is_bool($ref->{$key}) || ! (is_object($ref->{$key}) || is_array($ref->{$key}))); + $stringVal = (is_bool($val) || ! (is_object($val) || is_array($val))); + + $this->changelog .= "key: " . $key . " changed from " . ($stringRef ? (is_bool($ref->{$key}) ? ($ref->{$key} ? "true" : "false") : $ref->{$key}) : "<" . gettype($ref->{$key}) . ">") . " to " . ($stringVal ? (is_bool($val) ? ($val ? "true" : "false") : $val) : "<" . gettype($val) . ">") . ";\n "; + } + } + + if (empty($this->changelog)) { + return true; + } + return $this->update(); + } + } + + public function delete() + { + if (property_exists($this, static::$sqlIdx) && ($this->{static::$sqlIdx} == null)) { + return false; + } + if (static::$allowDeleteFromDB) { + if ($this->validateDelete()) { + $this->checkSql(); + $this->sql->quickExec("DELETE FROM `" . static::$sqlTable . "` where " . static::$sqlIdx . " = '" . $this->{static::$sqlIdx} . "'"); + if (! (empty(static::$deletedFieldName))) { + $this->{static::$deletedFieldName} = true; + } + $this->{static::$sqlIdx} = null; + } else { + throw new \Exception("ValidateDelete failed"); + } + } elseif (! (empty(static::$deletedFieldName))) { + $this->checkSql(); + $this->sql->quickExec("UPDATE `" . static::$sqlTable . "` set `" . static::$deletedFieldName . "`='1' where " . $this::$sqlIdx . " = '" . $this->{static::$sqlIdx} . "'"); + $this->{static::$deletedFieldName} = true; + return true; + } else { + throw new \Exception("DELETE not implemented in " . get_class($this), 592); + } + } + + protected function validateDelete() + { + return true; + } + + protected function update() + { + if (! empty($this::$sqlTable)) { + $this->sql->prepareUpdate($this::$sqlTable); + } + + if (empty($this::$sqlTable) || ! $this->updateAddParams()) { + throw new \Exception("Method update not implemented in " . get_class($this), 593); + } + $this->sql->paramClose($this::$sqlIdx . " = '" . $this->{static::$sqlIdx} . "'"); + $this->sql->quickExecute(); + return false; + } + + protected function create() + { + if (! empty($this::$sqlTable)) { + $this->sql->prepareInsert($this::$sqlTable); + } + + if (empty($this::$sqlTable) || ! $this->createAddParams()) { + throw new \Exception("Method create not implemented in " . get_class($this), 594); + } + + $this->sql->paramClose(); + $this->sql->quickExecute(); + if (property_exists($this, static::$sqlIdx)) { + $this->{static::$sqlIdx} = $this->sql->getInsertId(); + if (is_numeric($this->{static::$sqlIdx})) { + $this->fill((int) $this->{static::$sqlIdx}); + } else { + $this->fill($this->{static::$sqlIdx}); + } + } + return true; + } + + protected function updateAddParams() + { + return $this->addParams(); + } + + protected function createAddParams() + { + return $this->addParams(); + } + + protected function addParams() + { + $this->checkSql(); + + foreach ($this->getSqlSchema() as $key => $conf) { + if ($key == static::$sqlIdx) { + continue; + } + + if (property_exists($this, $key)) { + $val = $this->{$key}; + } elseif (property_exists($this, "__" . $key)) { + $val = $this->{"__" . $key}; + } else { + $val = null; + } + + if (is_array($val)) { + $sqlVal = json_encode($val); + $sqlNull = empty($val); + } elseif (is_object($val) && ! ($val instanceof stringExportable)) { + if ($val instanceof \JsonSerializable) { + $sqlVal = json_encode($val); + $sqlNull = empty($val); + } else { + throw new Exception("Oups... $key is not jsonSerialiazable"); + } + } elseif (is_object($val) && ($val instanceof stringExportable)) { + if ($val->isNull()) { + $sqlVal = null; + $sqlNull = true; + } else { + $sqlVal = (string) $val; + $sqlNull = ($val === null); + } + } elseif (is_bool($val)) { + $sqlVal = $val ? 1 : 0; + $sqlNull = false; + } else { + $sqlVal = (string) $val; + $sqlNull = ($val === null); + } + + if ($sqlNull && array_key_exists("nullable", $conf) && $conf["nullable"] === false) { + throw new Exception("Field $key can't be null in " . get_class($this)); + } + + $this->sql->paramAdd($key, $sqlVal, $sqlNull); + } + + return true; + } + + protected function validateSave() + { + return true; + } + + public function __get($key) + { + switch ($key) { + case "sqlSelectTemplate": + return $this->__sqlSelectTemplate; + break; + case "sql": + $this->checkSql(); + return $this->sql; + case "changelog": + return $this->changelog; + break; + default: + if (property_exists($this, $key)) { + if ($this->{$key} instanceof stringExportable && !($this->{$key} instanceof \JsonSerializable)) { + return (string) $this->{$key}; + } else { + return $this->{$key}; + } + } else { + throw new \Exception("property $key not availiable for read in class " . get_class($this), 595); + break; + } + } + } + + public function getSql() : sql + { + $this->checkSql(); + return $this->sql; + } + + public static function getCount($where = null) + { + $s = new static(); + if (empty($s::$sqlTable)) { + throw new \Exception("Method getTotalCount not implemented in " . get_class($s), 691); + } + + $sql = $s->getSql(); + $res = $sql->quickExec1Line("select count(" . (empty(static::$sqlIdx) ? "*" : static::$sqlIdx) . ") as `cnt` from `" . static::$sqlTable . "`" . (empty($where) ? "" : " where $where")); + return $res["cnt"]; + } + + public function __set($key, $val) + { + switch ($key) { + case "settings": + $this->__settings = $val; + break; + default: + throw new \Exception("property $key not availiable for write in class " . get_class($this), 596); + break; + } + } + + public function __debugInfo() + { + $rv = []; + foreach ($this as $key => $value) { + if (array_search($key, array_merge(static::$excludeProps, static::$excludePropsBase)) === false && ! preg_match("!^_!", $key)) { + if ($value instanceof stringExportable) { + if ($value->isNull()) { + $rv[$key] = null; + } else { + $rv[$key] = (string) $value; + } + } else { + $rv[$key] = $value; + } + } + } + return $rv; + } + + public function export() + { + $rv = []; + foreach ($this as $key => $value) { + if (array_search($key, array_merge(static::$excludeProps, static::$excludePropsBase)) === false && ! preg_match("!^_!", $key)) { + if (($this->__get($key)) instanceof \JsonSerializable) { + $rv[$key] = $this->__get($key); + } elseif (($this->__get($key)) instanceof stringExportable) { + if (($this->__get($key)->isNull())) { + $rv[$key] = null; + } else { + $rv[$key] = (string) ($this->__get($key)); + } + } else { + $rv[$key] = $this->__get($key); + } + } + } + return $rv; + } + + public function jsonSerialize() + { + $rv = $this->export(); + $rv["_type"] = get_class($this); + return $rv; + } + + protected static function xSearch($where, $pattern, ?array $options, sql $sql) { + + return ["where"=>$where, "join"=>null]; + } + + public static function search($pattern=null, $pageSize=null, $page=1, $options=[]) { + if (static::$sqlTable == null) { + throw new \Exception("Search not implemented for ".static::class); + } + $ref=new static(); + $sql = $ref->getSql(); + + $where=""; + if (!empty($pattern)) { + foreach (static::$sqlColumns as $key=>$val) { + if (!empty($val["search"])) { + switch (strtolower($val["search"])) { + case "strict": + $where.=(empty($where)?"":" OR ")."`$key`='".common::clearInput($pattern)."'"; + break; + + case "like": + $where.=(empty($where)?"":" OR ")."`$key` like '%".common::clearInput($pattern)."%'"; + break; + + case "start": + $where.=(empty($where)?"":" OR ")."`$key` like '%".common::clearInput($pattern)."'"; + break; + + case "invCode": + $where.=(empty($where)?"":" OR ")."`$key`='".UID::clear($pattern)."'"; + break; + + default: + continue 2; + } + + } + } + } + + if (static::$deletedFieldName && empty($options["showDeleted"])) { + $where = (empty($where)?"":"(".$where.") AND ")."`".static::$deletedFieldName."` = 0"; + } + + if ($pageSize!==null) { + if ($page<1) { $page=1;} + $limit = "LIMIT ".($pageSize*($page-1)).", ".$pageSize; + } else { + $limit=""; + } + + $xRes=static::xSearch($where, $pattern, $options, $sql); + $where = $xRes["where"]; + $join=$xRes["join"]; + + $sqlQueryString=$ref->sqlSelectTemplate.(empty($join)?"":" ".$join).(empty($where)?"":" WHERE ".$where).(empty($limit)?"":" ".$limit); + + $res=$sql->quickExec($sqlQueryString); + $rv=[]; + while ($row=mysqli_fetch_assoc($res)) { + $rv[]=new static($row); + } + return $rv; + } +} \ No newline at end of file diff --git a/core/fox/baseModule.php b/core/fox/baseModule.php new file mode 100644 index 0000000..faef24f --- /dev/null +++ b/core/fox/baseModule.php @@ -0,0 +1,103 @@ +title = static::$title; + $mi->modVersion = static::$version; + $mi->name = $mi->namespace = substr(static::class, 0, strrpos(static::class, '\\')); + $mi->features = static::$features; + $mi->isTemplate = true; + $mi->singleInstanceOnly = ! static::$allowAlias; + $mi->authRequired = static::$authRequred; + $mi->ACLRules = static::$ACLRules; + $mi->menuItem = static::$menuItem; + $mi->globalAccessKey = static::$globalAccessKey; + $mi->languages = static::$languages; + $mi->configKeys=static::$configKeys; + + return $mi; + } + + public static function doMigration() { + if (static::$allowAlias) { + throw new \Exception("Embedded migration not allowed for multi-instance modules"); + } + + $modInfo = static::getModInfo(); + $instances = $modInfo->getInstances(); + + foreach ($instances as $module) { + \fox\sql::doMigration($module); + } + + } +} diff --git a/core/fox/cache.php b/core/fox/cache.php new file mode 100644 index 0000000..920ab13 --- /dev/null +++ b/core/fox/cache.php @@ -0,0 +1,124 @@ +connCheck()) { + return $mcd; + } else { + return false; + } + } + + public function __construct($host = null, $port = null) + { + if (! class_exists("Memcached")) { + $this->mcd = null; + return; + } + + if (empty($host) && ! empty(config::get("cacheHost"))) { + $this->mcd = new \Memcached(); + $host = config::get("cacheHost"); + if (empty($port)) { + $port = config::get("cachePort"); + } + ; + if (empty($port)) { + $port = 11211; + } + } + + if (gettype($host) == 'array') { + $this->mcd = new \Memcached(); + $this->mcd->addServers($host); + } elseif (gettype($host) == "string") { + $this->mcd = new \Memcached(); + $this->mcd->addServer($host, $port); + } else { + $this->mcd = null; + return; + } + $this->prefix = str_pad(strtolower(dechex(crc32(config::get("sitePrefix")))), 8, "0", STR_PAD_LEFT); + } + + protected function pConnCheck() + { + if (! $this->connCheck()) { + throw new \Exception("MEMCACHED Not connected!"); + } + } + + public function connCheck() + { + if (empty($this->mcd)) { + return false; + } + return $this->mcd->getVersion() !== false; + } + + public function set($key, $val, $TTL = 300,$encrypt=false) + { + try { + $this->pConnCheck(); + } catch (\Exception $e) { + trigger_error($e->getMessage()); + return false; + } + if ($encrypt) { + $this->mcd->set($this->prefix . "." . $key, xcrypt::encrypt(json_encode($val)), $TTL); + } else { + $this->mcd->set($this->prefix . "." . $key, json_encode($val), $TTL); + } + } + + public function get($key, $array = false) + { + try { + $this->pConnCheck(); + } catch (\Exception $e) { + trigger_error($e->getMessage()); + return null; + } + + + $xval=$this->mcd->get($this->prefix . "." . $key); + if ($xval==null || $xval=="null") { return null; } + $rv= json_decode($this->mcd->get($this->prefix . "." . $key), $array); + if ($rv !== null) { return $rv; } + $rv= json_decode(xcrypt::decrypt($this->mcd->get($this->prefix . "." . $key)), $array); + return $rv; + } + + public function del($key) { + try { + $this->pConnCheck(); + } catch (\Exception $e) { + trigger_error($e->getMessage()); + return false; + } + + $this->mcd->delete($this->prefix . "." . $key); + + } +} + +?> \ No newline at end of file diff --git a/core/fox/common.php b/core/fox/common.php new file mode 100644 index 0000000..2236eb6 --- /dev/null +++ b/core/fox/common.php @@ -0,0 +1,209 @@ +', '>', $txt); + $txt = str_replace("\n", "
", $txt); + + return $txt; + } + + // получить значение с get или post + static function getVal($name, $regex = '', $skipQuotes = null, $allowEmptyString = true) + { + if ((! isset($_POST[$name])) && (! isset($_GET[$name]))) { + return null; + } + if ($regex != "") { + if (isset($_POST[$name])) { + $val = preg_replace('![^' . $regex . ']+!', '', $_POST[$name]); + } else { + $val = ""; + } + if ($val == "") { + if (isset($_GET[$name])) { + $val = preg_replace('![^' . $regex . ']+!', '', $_GET[$name]); + } + ; + } + } else { + + if (! isset($skipQuotes)) { + if (isset($_POST[$name])) { + $val = preg_replace("![\'\"]+!", '\"', $_POST[$name]); + } else { + $val = ""; + } + if ($val == "") { + if (isset($_GET[$name])) { + $val = preg_replace("![\'\"]+!", '\"', $_GET[$name]); + } + } + } else { + if (isset($_POST[$name])) { + $val = $_POST[$name]; + } else { + $val = $_GET[$name]; + } + } + } + if (! $allowEmptyString && $val == "") { + $val = null; + } + + return $val; + } + + static function dropcslash($val) + { + $val = preg_replace("!\\\([\'\"])+!", "$1", $val); + return $val; + } + + static function validateEMail($str) { + return preg_match("/[0-9A-Za-z_.-]*@[0-9A-Za-z_.-]/", $str); + } + + /** + * проверяем, что функция mb_ucfirst не объявлена + * и включено расширение mbstring (Multibyte String Functions) + */ + static function mbx_ucfirst($str, $encoding = 'UTF-8') + { + $str = mb_ereg_replace('^[\ ]+', '', $str); + $str = mb_strtoupper(mb_substr($str, 0, 1, $encoding), $encoding) . mb_substr($str, 1, mb_strlen($str), $encoding); + return $str; + } + + static function getGUIDc() + { + mt_srand((double) microtime() * 10000); // optional for php 4.2.0 and up. + $charid = strtoupper(md5(uniqid(rand(), true))); + $hyphen = chr(45); // "-" + $uuid = substr($charid, 0, 8) . $hyphen . substr($charid, 8, 4) . $hyphen . substr($charid, 12, 4) . $hyphen . substr($charid, 16, 4) . $hyphen . substr($charid, 20, 12); + + return $uuid; + } + + static function getGUID() + { + $cUuid = getGUIDc(); + $uuid = chr(123) . // "{" + $cUuid . chr(125); // "}" + return $uuid; + } + + static function fullname2qname($first, $mid, $last) + { + return $last . " " . mb_substr($first, 0, 1) . ". " . mb_substr($mid, 0, 1) . "."; + } + + static function text2html($src) + { + $src = preg_replace("/[\n]/", "
", $src); + // $src=preg_replace("/[\cr\<\>]/"," ",$src); + return $src; + } + + static function genPasswd($number, $arr = null) + { + if (! isset($arr)) { + $arr = array( + 'a', + 'b', + 'c', + 'd', + 'e', + 'f', + 'g', + 'h', + 'i', + 'j', + 'k', + 'l', + 'm', + 'n', + 'o', + 'p', + 'r', + 's', + 't', + 'u', + 'v', + 'x', + 'y', + 'z', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '0' + ); + } + // Генерируем пароль + $pass = ""; + for ($i = 0; $i < $number; $i ++) { + // Вычисляем случайный индекс массива + $index = rand(0, count($arr) - 1); + $pass .= $arr[$index]; + } + return $pass; + } + + static function clearInput($val, $regex = "") + { + if (empty($regex)) { + $regex = "[0-9a-zA-Z_.@-]"; + } + return preg_replace('![^' . $regex . ']+!', '', $val); + } + + static function replaceMessageFromArray($text, $ref, $template='\$\{([^}]*)\}', $delimiter=".") { + $match=[]; + preg_match_all('/'.$template.'/', $text, $match); + + foreach ($match[1] as $idx=>$val) { + $text=str_replace($match[0][$idx], static::getArrayKeyByPath($ref,explode($delimiter,$val)), $text); + } + return $text; + } + + static function getArrayKeyByPath($arr, $path) { + if (gettype($path)=="NULL") { + return "undefined"; + } elseif (gettype($path)=="string") { $path=[$path]; } + + while ($key = array_shift($path)) { + if (array_key_exists($key, (array)$arr)) { + $arr=((array)$arr)[$key]; + } else { + return "undefined"; + } + } + return $arr; + } +} +?> \ No newline at end of file diff --git a/core/fox/company.php b/core/fox/company.php new file mode 100644 index 0000000..fa5be09 --- /dev/null +++ b/core/fox/company.php @@ -0,0 +1,66 @@ + [ + "type" => "VARCHAR(255)", + "nullable" => false + ], + "qName" => [ + "type" => "VARCHAR(255)", + "nullable" => false + ], + "description" => [ + "type" => "VARCHAR(255)" + ], + "type" => [ + "type" => "VARCHAR(255)", + "nullable" => false + ] + ]; + + public function __xConstruct() + { + $this->invCode = new UID(); + } + + protected function validateSave() + { + if ($this->invCode->isNull()) { + $this->invCode->issue("core", get_class($this)); + } + return true; + } +} +?> \ No newline at end of file diff --git a/core/fox/config.php b/core/fox/config.php new file mode 100644 index 0000000..bb3f616 --- /dev/null +++ b/core/fox/config.php @@ -0,0 +1,151 @@ +[ + "type"=>"INT", + "index"=>"AI", + ], + "module" => [ + "type" => "VARCHAR(128)", + "index" => "INDEX" + ], + "key" => [ + "type" => "VARCHAR(128)", + "index" => "INDEX" + ], + "value" => [ + "type" => "VARCHAR(128)" + ] + ]; + + // stop-list - never search this keys in SQL + private const envLockedKeys = [ + "FOX_SQLSERVER", + "FOX_SQLUSER", + "FOX_SQLPASSWD", + "FOX_SQLDB", + "FOX_CACHEHOST", + "FOX_CACHEPORT", + "FOX_TITLE", + "FOX_SITEPREFIX", + "FOX_MASTERSECRET", + "FOX_UIDOFFSET", + "FOX_S3_ENDPOINT", + "FOX_S3_LOGIN", + "FOX_S3_SECRET", + "FOX_S3_REGION", + "FOX_INIT_PASSWORD", + "FOX_INIT_USERNAME", + "FOX_TOKEN_TTL_WEB", + "FOX_TOKEN_TTL_API", + "FOX_TOKEN_TTL_APP", + "FOX_TOKEN_ALLOW_RENEW_WEB", + "FOX_TOKEN_ALLOW_RENEW_API", + "FOX_TOKEN_ALLOW_RENEW_APP", + "FOX_TOKEN_RENEW_WEB", + "FOX_TOKEN_RENEW_API", + "FOX_TOKEN_RENEW_APP", + "FOX_DEFAULT_THEME", + "FOX_DEFAULT_PAGESIZE", + "FOX_DEFAULT_LANGUAGE", + "FOX_DEFAULT_MODULE", + "FOX_SESSION_RENEW_SEC", + "FOX_ALLOW_REGISTER" + ]; + + static function get($key, $module = "core") + { + + // try serach in ENV + if (getenv("FOX_" . strtoupper($key)) !== false) { + return getenv("FOX_" . strtoupper($key)); + } + + // check stop-list + if (array_search("FOX_" . strtoupper($key), static::envLockedKeys) !== false) { + return null; + } + + if (static::get("SQLSERVER")===null) { + throw new Exception("Error: SQL config not found"); + } + + $conf = static::getAll($module); + if (array_key_exists($key, $conf)) { + return $conf[$key]; + } else { + return null; + } + } + + static function getAll($module = "core", $forceDB = false, $sql = null) + { + $cache = new cache(); + $conf = $cache->get("config." . $module); + + if ($forceDB || $conf === null) { + if (empty($sql)) { + $sql = new sql(); + } + $res = $sql->quickExec("select `key`,`value` from `tblSettings` where `module` = '$module'"); + $conf = []; + while ($row = mysqli_fetch_assoc($res)) { + $conf[$row["key"]] = $row["value"]; + } + $cache->set("config." . $module, $conf); + } + return (array) $conf; + } + + static function set($key, $value, $module) + { + $sql = new sql(); + if (static::get($key, $module) !== null) { + $sql->prepareUpdate("tblSettings"); + $sql->paramAddUpdate("value", $value); + $sql->paramClose(" `module` = '" . $module . "' and `key` = '" . $key . "'"); + $sql->execute(); + } else { + $sql->prepareInsert("tblSettings"); + $sql->paramAddInsert("module", $module); + $sql->paramAddInsert("key", $key); + $sql->paramAddInsert("value", $value); + $sql->paramClose(); + $sql->execute(); + } + static::getAll($module, true, $sql); + } + + static function del($key, $module) + { + $sql = new sql(); + $sql->quickExec("delete from `tblSettings` where `module` = '$module' and `key`='$key'"); + static::getAll($module, true, $sql); + } + + static function delAll($module) + { + $sql = new sql(); + $sql->quickExec("delete from `tblSettings` where `module` = '$module'"); + static::getAll($module, true, $sql); + } +} +?> \ No newline at end of file diff --git a/core/fox/confirmCode.php b/core/fox/confirmCode.php new file mode 100644 index 0000000..8f431a3 --- /dev/null +++ b/core/fox/confirmCode.php @@ -0,0 +1,89 @@ + [ + "type" => "CHAR(4)", + "nullable" => false, + "index"=>"INDEX" + ], + "instance" => [ + "type" => "VARCHAR(255)", + "nullable" => false, + ], + "class" => [ + "type" => "VARCHAR(255)", + "nullable" => false, + ], + "operation" => [ + "type" => "VARCHAR(255)", + "nullable" => false, + ], + "reference" => [ + "type" => "VARCHAR(255)", + "nullable" => false, + ], + "hash" => [ + "type" => "VARCHAR(255)", + "nullable" => false, + ], + ]; + + protected function __xConstruct() + { + $this->issueStamp=new time(); + $this->expireStamp=new time(); + } + + protected function getHash() { + return xcrypt::hash(json_encode([ + $this->instance, + $this->class, + $this->operation, + $this->reference, + $this->payload, + ])); + } + + public function fillByHash() { + if (empty($this->hash)) { $this->hash=$this->getHash();} + $this->checkSql(); + if ($row=$this->sql->quickExec1Line($this->__sqlSelectTemplate." WHERE `hash`='".$this->hash."'")) { + $this->fillFromRow($row); + return true; + } else { + return false; + } + } + + protected function validateSave() + { + if ($this->issueStamp->isNull()) {$this->issueStamp=time::current();}; + if ($this->expireStamp->isNull()) {$this->expireStamp=time::current()->addSec(static::defaultTTL);} + if ($this->code==null) { $this->code=(common::genPasswd(4,[0,1,2,3,4,5,6,7,8,9]));} + if (empty($this->instance) || empty($this->class) || empty($this->operation) || empty($this->reference)) { + throw new foxException("Empty refences not allowed here","400"); + } + if (empty($this->hash)) {$this->hash = $this->getHash(); } + + return !$this->fillByHash(); + } +} + +?> \ No newline at end of file diff --git a/core/fox/cronDb.php b/core/fox/cronDb.php new file mode 100644 index 0000000..8c213c3 --- /dev/null +++ b/core/fox/cronDb.php @@ -0,0 +1,84 @@ +dbfile)); + + $this->s=new SQLite3($this->dbfile); + $this->s->busyTimeout(100000); + } catch (\Exception $e) { + $this->__destruct(); + throw $e; + } + if ($init) { $this->initialize();} + } + + public function __destruct() { + $this->s->close(); + } + + public function initialize() { + + try { + $this->s->exec("drop table if exists `tasks`"); + + $this->s->exec("create table if not exists `tasks` ( + `pid` INTEGER PRIMARY KEY NOT NULL, + `hash` text default null, + `startStamp` int default 0, + `expireStamp` int default 0, + `method` text default null + )"); + } catch (\Exception $e) { + $this->__destruct(); + throw $e; + } + } + + public function getRunningTasks() { + $r=$this->s->query("select * from `tasks`"); + if (empty($r)) { + trigger_error("Query failed select * from `tasks`"); + return null; + } + $rv=[]; + while ($row = $r->fetchArray(SQLITE3_ASSOC)) { + array_push($rv, $row);; + } + + return $rv; + } + + public function addTask ($pid, $hash, $method, $TTL) { + $st=$this->s->prepare('insert into `tasks` (pid,hash,startStamp, method,expireStamp) values (:pid, :hash,:startStamp,:method,:expireStamp) on conflict(pid) do update set hash=:hash,startStamp=:startStamp, expireStamp=:expireStamp, method=:method'); + $st->bindValue(':pid', $pid,SQLITE3_INTEGER); + $st->bindValue(':hash', $hash,SQLITE3_TEXT); + $st->bindValue(':startStamp', time(),SQLITE3_INTEGER); + $st->bindValue(':expireStamp', time()+($TTL),SQLITE3_INTEGER); + $st->bindValue(':method', $method,SQLITE3_TEXT); + $st->execute(); + } + + public function delTask($pid) { + return $this->s->exec("delete from `tasks` where `pid`='".$pid."'"); + } + +} +?> \ No newline at end of file diff --git a/core/fox/dbStoredBase.php b/core/fox/dbStoredBase.php new file mode 100644 index 0000000..d7071dc --- /dev/null +++ b/core/fox/dbStoredBase.php @@ -0,0 +1,38 @@ + \ No newline at end of file diff --git a/core/fox/errorPage.php b/core/fox/errorPage.php new file mode 100644 index 0000000..45770da --- /dev/null +++ b/core/fox/errorPage.php @@ -0,0 +1,58 @@ + 'Unauthorized', + 402 => 'Unauthorized`', + 403 => "Forbidden", + 404 => 'Not found', + 500 => 'Internal server error' + ]; + + public static function show($error_code = null, $error_desc = null, $light = null, $json = false) + { + ob_clean(); + $error_code_clean = explode(".", $error_code)[0]; + if (empty($error_code)) { + $error_code = "404"; + } + if (empty($error_desc)) { + $error_desc = static::defaultErrorDesc[$error_code_clean]; + } + + if ($light) { + if ($json) { + print json_encode([ + "status" => "ERR", + "message" => $error_code . ' ' . $error_desc + ]); + } else { + print 'Error: ' . $error_code . ' ' . $error_desc; + } + header('HTTP/1.0 ' . $error_code . ' ' . $error_desc, true, $error_code_clean); + exit(); + } + + $mod_name = config::get("defaultLoginModule"); + if (! $mod_name) { + $mod_name = "core"; + } + + static::show($error_code, $error_desc, true); + exit(); + } +} +?> \ No newline at end of file diff --git a/core/fox/externalCallable.php b/core/fox/externalCallable.php new file mode 100644 index 0000000..726c95f --- /dev/null +++ b/core/fox/externalCallable.php @@ -0,0 +1,25 @@ +_(request $request); + * Example: + * public static function API_POST_members(request $request); + * public static function APICall(request $request); + * + */ + +} +?> \ No newline at end of file diff --git a/core/fox/file.php b/core/fox/file.php new file mode 100644 index 0000000..4baa1d1 --- /dev/null +++ b/core/fox/file.php @@ -0,0 +1,57 @@ +expireStamp=new time(); + } + + public static $sqlColumns = [ + "fileName" => [ + "type" => "VARCHAR(255)", + ], + "module" => [ + "type" => "VARCHAR(255)", + "index" => "INDEX" + ], + "class" => [ + "type" => "VARCHAR(255)", + "index" => "INDEX" + ], + "ownerId" => [ + "type" => "INT", + "index" => "INDEX", + "nullable"=>true + ], + "expireStamp" => [ + "type" => "DATETIME", + "index" => "INDEX", + "nullable"=>true + ], + ]; + +} + + +?> \ No newline at end of file diff --git a/core/fox/fileConverter.php b/core/fox/fileConverter.php new file mode 100644 index 0000000..e16a534 --- /dev/null +++ b/core/fox/fileConverter.php @@ -0,0 +1,95 @@ + $type + ]; + $files = [ + "userfile" => $src + ]; + + // данные для отправки + foreach ($postData as $key => $val) { + $content .= '--' . $boundary . "\n"; + $content .= 'Content-Disposition: form-data; name="' . $key . '"' . "\n\n" . $val . "\n"; + } + + // файлы для отправки + foreach ($files as $key => $file) { + $content .= '--' . $boundary . "\n"; + $content .= 'Content-Disposition: form-data; name="' . $key . '"; filename="' . basename($file) . '"' . "\n"; + $content .= 'Content-Type: ' . "text/xml" . "\n"; + $content .= 'Content-Transfer-Encoding: binary' . "\n\n"; + $content .= file_get_contents($file) . "\n"; + } + + // завершаем контент + $content .= "--$boundary--\n"; + + $params = array( + 'http' => array( + 'method' => 'POST', + 'content' => $content, + 'header' => array( + 'Content-Type: multipart/form-data; boundary=' . $boundary + ) + ) + ); + + $context = stream_context_create($params); + + if ($remote = fopen($url, 'rb', false, $context)) { + $response = @stream_get_contents($remote); + } else { + throw new \Exception("Converter failed!"); + } + + $res = json_decode($response); + if ($res->status == 'OK') { + $raw = file_get_contents($url . $res->result); + if (empty($dst)) { + return $raw; + } else { + file_put_contents($dst, $raw); + return true; + } + } else { + throw new \ErrorException("Converter failed: " . $res->message); + } + } +} \ No newline at end of file diff --git a/core/fox/foxException.php b/core/fox/foxException.php new file mode 100644 index 0000000..dd329fc --- /dev/null +++ b/core/fox/foxException.php @@ -0,0 +1,59 @@ +setStatus($status); + $e->setXCode($xCode); + throw $e; + } + + public function setStatus($status) + { + $this->status = $status; + } + + public function setXCode($xCode) + { + $this->xCode=$xCode; + } + + public function getStatus() + { + return $this->status; + } + + public function getXCode() + { + return empty($this->xCode)?$this->code:$this->xCode; + } + +} + +?> \ No newline at end of file diff --git a/core/fox/foxRequestResult.php b/core/fox/foxRequestResult.php new file mode 100644 index 0000000..5bd2027 --- /dev/null +++ b/core/fox/foxRequestResult.php @@ -0,0 +1,30 @@ +code = $code; + $e->message = $message; + $e->retVal = $retVal; + throw $e; + } +} + +?> \ No newline at end of file diff --git a/core/fox/jsonImportable.php b/core/fox/jsonImportable.php new file mode 100644 index 0000000..d422613 --- /dev/null +++ b/core/fox/jsonImportable.php @@ -0,0 +1,18 @@ + \ No newline at end of file diff --git a/core/fox/lang/ru.php b/core/fox/lang/ru.php new file mode 100644 index 0000000..d3ac282 --- /dev/null +++ b/core/fox/lang/ru.php @@ -0,0 +1,73 @@ +Приглашение пользователя на \${svcName} +
+Добрый день!
+Вас пригласили для регистрации на сайте \${svcName}
+
+ +Для регистрации регистрации укажите код \${regCodePrint} в форме регистрации по адресу \${sitePrefix}/auth/register
+или перейдите по ссылке
+
+Если регистрация не требуется либо письмо было отправлено по ошибке - просто проигнориуйте его. +
+ +Отвечать на это письмо не нужно, так как оно отправлено автоматически. +
+
+С уважением,
+Команда \${svcName} + +
+"; + const eMailConfirmMessageTitle="Подтверждение адреса почты для \${svcName}"; + const eMailConfirmMessage=" +
+Добрый день!
+ +Для подтверждения адреса электронной почты укажите код \${confCodePrint} в форме подтверждения по адресу \${sitePrefix}/core/userEmailConfirm
+или перейдите по ссылке
+
+Если Вы не запрашивали подтверждения адреса либо письмо отправлено по ошибке - просто проигнориуйте его. +
+ +Отвечать на это письмо не нужно, так как оно отправлено автоматически. +
+
+С уважением,
+Команда \${svcName} + +
+"; + + const accessRecoverMessageTitle="Восстановление доступа \${svcName}"; + const accessRecoverMessage=" +
+Добрый день!
+ +Для восстановления доступа укажите код \${confCodePrint} в форме восстановления по адресу \${sitePrefix}/auth/recover
+или перейдите по ссылке
+
+Если Вы не запрашивали восстановление доступа - просто проигнориуйте его либо сообщите нам. +
+ +Отвечать на это письмо не нужно, так как оно отправлено автоматически. +
+
+С уважением,
+Команда \${svcName} + +
+"; +} + + +?> \ No newline at end of file diff --git a/core/fox/langPack.php b/core/fox/langPack.php new file mode 100644 index 0000000..05fde8b --- /dev/null +++ b/core/fox/langPack.php @@ -0,0 +1,40 @@ +languages) === false ) { + $lang = $mod->languages[0]; + } + + + $langClass=$mod->namespace."\\lang\\".$lang; + if (!class_exists($langClass)) { + $langClass=$mod->namespace."\\lang_".$lang; + } + + if (!class_exists($langClass)) { + throw new foxException("Class ".$langClass." not found!"); + } + try { + return constant($langClass."::".$ref[1]); + } catch (\Exception $e) { + return null; + } + } +} +?> \ No newline at end of file diff --git a/core/fox/mailAccount.php b/core/fox/mailAccount.php new file mode 100644 index 0000000..934c4f3 --- /dev/null +++ b/core/fox/mailAccount.php @@ -0,0 +1,104 @@ +["type"=>"VARCHAR(255)","nullable"=>false], + "rxServer"=>["type"=>"VARCHAR(255)", "nullable"=>true], + "rxProto"=>["type"=>"VARCHAR(255)", "nullable"=>true], + "txServer"=>["type"=>"VARCHAR(255)", "nullable"=>true], + "txProto"=>["type"=>"VARCHAR(255)", "nullable"=>true], + "login"=>["type"=>"VARCHAR(255)", "nullable"=>false], + "password"=>["type"=>"TEXT", "nullable"=>true], + "module"=>["type"=>"VARCHAR(255)", "nullable"=>true], + "rxFolder"=>["type"=>"VARCHAR(255)", "nullable"=>true], + "rxArchiveFolder"=>["type"=>"VARCHAR(255)", "nullable"=>true], + + ]; + + public function __get($key) { + switch ($key) { + case "password": return xcrypt::decrypt($this->__password); + case "rxLogin": return $this->login; + case "rxPassword": return xcrypt::decrypt($this->__password); + case "rxLogin": return $this->login; + case "rxPassword": return xcrypt::decrypt($this->__password); + default: return parent::__get($key); + } + } + + public function __set($key, $val) { + switch ($key) { + case "password": $this->__password = xcrypt::encrypt($val); break; + default: parent::__set($key, $val); + } + } + + public function connect() { + return new mailClient($this); + } + + protected function validateSave() { + if (empty($this->login) || empty($this->__password) || empty($this->address)) { return false;} + + return true; + } + + + public static function getDefaultAccount(&$sql=null) { + $ref = new static(); + $sql = $ref->getSql(); + $rv = $sql->quickExec1Line("select * from `tblMailAccounts` where `default` = 1 limit 1"); + if ($rv) { + return new static($rv); + } else {return null;} + } + +} \ No newline at end of file diff --git a/core/fox/mailAddress.php b/core/fox/mailAddress.php new file mode 100644 index 0000000..61fe364 --- /dev/null +++ b/core/fox/mailAddress.php @@ -0,0 +1,74 @@ +__set("name", $name); + $this->__set("address", $address); + } else { + $res=[]; + $name = preg_replace('!^[\"\ \t]*|[\"\ \t]*$!', '', $name); + if(preg_match("/([^\<\>]*) \<([^\<\>]*)\>/", $name, $res)) { + $this->address=$res[2]; + $this->name=$res[1]; + } elseif (preg_match("/\<([^\<\>]*)\>/", $name, $res)) { + $this->name=$this->address=$res[1]; + } elseif(common::validateEMail($name)) { + $this->name=$this->address=$name; + } else { + throw new \Exception("Invalid input value '$name'"); + } + } + + $this->name = preg_replace('!^[\"]*|[\"]*$!', '', $this->name); + } + + + public function __get($key) { + switch($key) { + case "name": + return $this->name; + case "address": + return $this->address; + case "full": + return ''.$this->name." <".$this->address.">"; + } + } + + public function __set($key,$val) { + switch ($key) { + case "name": + $this->name = $val; + break; + case "address": + if (common::validateEMail($val)) { + $this->address=$val; + } else { + throw new foxException("Invalid EMail '$val'"); + } + break; + } + } + + public function __debugInfo() { + + return ["full"=>$this->__get("full")]; + } +} +?> \ No newline at end of file diff --git a/core/fox/mailAttachment.php b/core/fox/mailAttachment.php new file mode 100644 index 0000000..0d62a3a --- /dev/null +++ b/core/fox/mailAttachment.php @@ -0,0 +1,68 @@ +fillFromMessage($filename, $disposition, $p_key, $encoding, $message); + return $obj; + } + + public function fillFromMessage($filename, $disposition, $p_key, $encoding, mailMessage $message) { + $this->filename = $filename; + $this->disposition=$disposition; + $this->p_key=$p_key; + $this->encoding=$encoding; + $this->message=$message; + } + + public static function createFromPart(&$part, $pkey, &$message) { + + $att = new static(); + $att->message=$message; + $att->p_key=$pkey; + $att->filename = ($part["dparameters"]["0"]->value); + $att->disposition=$part["disposition"]; + $att->encoding=$part["encoding"]; + + + if(strtolower(substr($part["dparameters"]["0"]->value,0,7)) == "utf-8''") + { + $att->filename = urldecode(substr($att->filename,7)); + } elseif (strtolower(substr($part["dparameters"]["0"]->value,0,10)) == "=?utf-8?b?") { + $att->filename=mb_decode_mimeheader($att->filename); + } + + return $att; + } + + public function writeAttachment() { + + $f_data = imap_fetchbody ($this->message->conn, $this->message->refNum, $this->p_key,FT_PEEK); + if ($this->encoding == 3) + { + $f_data = base64_decode($f_data); + } elseif ($this->encoding == 4) { + $f_data=quoted_printable_decode($f_data); + } + + $this->write($f_data); + + } + + + + +} \ No newline at end of file diff --git a/core/fox/mailBlocklist.php b/core/fox/mailBlocklist.php new file mode 100644 index 0000000..7240072 --- /dev/null +++ b/core/fox/mailBlocklist.php @@ -0,0 +1,82 @@ + [ + "type" => "VARCHAR(255)", + "nullable" => true + ], + "entryStamp"=>[ + "type"=>"DATETIME" + ] + ]; + + public static function getByAddress($eMail) { + $ref=new static(); + $sql=$ref->getSql(); + if($res=$sql->quickExec1Line($ref->sqlSelectTemplate." WHERE `address`='".$eMail."'")) { + return new static($res); + } else { + return false; + } + } + + protected function __xConstruct() { + $this->entryStamp=new time(); + } + + protected function validateSave() + { + if ($this->entryStamp->isNull()) { + $this->entryStamp=time::current(); + } + return true; + } + + public static function API_PUT(request $request) { + if (! $request->user->checkAccess("adminMailBlocklist", "core")) { + throw new foxException("Forbidden", 403); + } + + $eMail=common::clearInput($request->requestBody->address,"0-9A-Za-z_@.-"); + + if (!common::validateEMail($eMail)) { + foxException::throw("ERR", "Invalid address format", 400,"IAF"); + } + + if ($bl = static::getByAddress($eMail)) { + return; + } + + $bl=new static(); + $bl->address=$eMail; + $bl->save(); + } + + public static function API_DELETE(request $request) { + if (! $request->user->checkAccess("adminMailBlocklist", "core")) { + throw new foxException("Forbidden", 403); + } + + $eMail=common::clearInput($request->requestBody->address,"0-9A-Za-z_@.-"); + + if (!common::validateEMail($eMail)) { + foxException::throw("ERR", "Invalid address format", 400,"IAF"); + } + + if ($bl = static::getByAddress($eMail)) { + $bl->delete(); + foxRequestResult::throw("200", "Deleted"); + } else { + foxException::throw("WARN", "Not found", 404,"ANF"); + } + } +} +?> \ No newline at end of file diff --git a/core/fox/mailClient.php b/core/fox/mailClient.php new file mode 100644 index 0000000..42e6b3a --- /dev/null +++ b/core/fox/mailClient.php @@ -0,0 +1,258 @@ +acct = $a; + } + + + public function getList($folder=null, $criteria="UNSEEN", $range=null) { + if (strtolower($this->acct->rxProto) != 'imap') { throw new \Exception("Protocol '".$this->acct->rxProto."' not implemented yet");} + + if (empty($folder)) { + $folder=$this->acct->rxFolder; + if (empty($folder)) {$folder = 'INBOX';} + } + $this->conn = (\imap_open("{".$this->acct->rxServer.":".$this->acct->rxPort."/".strtolower($this->acct->rxProto).(($this->acct->rxSSL==false)?"/novalidate-cert":"/ssl")."}$folder",$this->acct->rxLogin,$this->acct->rxPassword)); + + if (!$this->conn) + { + throw new \Exception("Connection to '".$this->acct->rxServer."' failed"); + } + + $this->stat = (array)imap_mailboxmsginfo($this->conn); + + $this->list = imap_search($this->conn, $criteria, SE_UID); + $this->messages=[]; + } + + public function getMessages() { + $this->messages = []; + if (empty($this->list)) { return; } + foreach ($this->list as $idx) { + array_push($this->messages, $this->getMessage($idx)); + } + } + + public function getMessage($uid) { + $message = new mailMessage(); + + $num = imap_msgno ( $this->conn , $uid ); + $msg = imap_fetch_overview ( $this->conn , $num)[0]; + + if (property_exists($msg, "in_reply_to")) { + try { + $message->addRecipient(self::decodeHeader($msg->to)); + } catch (\Exception $e) {} + } + + $message->account = $this->acct; + $message->addSender(self::decodeHeader($msg->from)); + $message->messageId = $msg->message_id; + $message->udate = $msg->udate; + $message->subject = self::decodeHeader(imap_utf8($msg->subject)); + $message->refNum=$num; + $message->conn=$this->conn; + $message->direction='RX'; + + if (property_exists($msg, "in_reply_to")) { $message->inReplyTo=$msg->in_reply_to;}; + if (property_exists($msg, "references")) { $message->references =$msg->references;} + + $pheaders = imap_rfc822_parse_headers(imap_fetchheader($this->conn, $num)); + $struct = imap_fetchstructure ($this->conn, $num); + $parts_found = null; + $this->search_parts($struct,null,$parts_found); + + try { + if (property_exists($pheaders, "fromaddress")) { foreach (explode(",", $pheaders->fromaddress) as $item) { $message->addSender(self::decodeHeader($item)); }; } + } catch(\Exception $e) {} + + try { + if (property_exists($pheaders, "toaddress")) { foreach (explode(",", $pheaders->toaddress) as $item) { $message->addRecipient(self::decodeHeader($item)); }; } + } catch(\Exception $e) {} + try { + if (property_exists($pheaders, "ccaddress")) { foreach (explode(",", $pheaders->ccaddress) as $item) { $message->addCC(self::decodeHeader($item)); }; } + } catch(\Exception $e) {} + + if (isset($parts_found["type"]["PLAIN"])) + { + $text = imap_fetchbody ($this->conn, $num, $parts_found["type"]["PLAIN"],FT_PEEK ); + $encoding = $parts_found[$parts_found["type"]["PLAIN"]]["encoding"]; + $text_encoding = $parts_found[$parts_found["type"]["PLAIN"]]["text-encoding"]; + if ($encoding == 3) + { + $text = base64_decode($text); + } elseif ($encoding == 4) { + $text=quoted_printable_decode($text); + } + + if ($text_encoding == 'default') + { + $msg_plain_text = $text; + } else { + $msg_plain_text = iconv($text_encoding, "utf-8", $text); + } + + $message->bodyPlain = $msg_plain_text; + } + + if (isset($parts_found["type"]["HTML"])) + { + $encoding = $parts_found[$parts_found["type"]["HTML"]]["encoding"]; + $html_text=imap_fetchbody ($this->conn, $num, $parts_found["type"]["HTML"],FT_PEEK ); + if ($encoding == 3) + { + $html_text = base64_decode($html_text); + } elseif ($encoding == 4) { + $html_text=quoted_printable_decode($html_text); + } + + + $message->bodyHTML = $html_text; + } + + + foreach ($parts_found as $pkey=>$part) + { + if ($pkey != 'type') { + if (isset($part["disposition"]) && (($part["disposition"] == 'attachment') || (($part["dparameters"]["0"]->value != '')))) + { + $att = mailAttachment::createFromPart($part, $pkey, $message); + $message->addAttachment($att); + } + } + } + + //imap_setflag_full($this->conn, $message->refNum, "\Seen"); + + return $message; + + } + + protected function search_parts($struct, $path=null, &$parts_found=null) + { + if (!isset($path)) + { + $path=''; + $parts_found = null; + $parts_found["type"]["HTML"] = null; + $parts_found["type"]["PLAIN"] = null; + + } + $retval = $this->parce_struct($struct); + $path_rv = $path; + if ($path_rv == '') { $path_rv = 1;} + $parts_found[$path_rv] = $retval; + + + + if (($retval["type"] == 0) && ($retval["subtype"] == "PLAIN")) + { + $parts_found["type"]["PLAIN"] = $path_rv; + } elseif (($retval["type"] == 0) && ($retval["subtype"] == "HTML")) + { + $parts_found["type"]["HTML"] = $path_rv; + } + + if ($retval["parts_count"] > 0) + { + foreach ($struct->parts as $pkey=>$part) + { + $path_t = $path; + if ($path_t != '') {$path_t .= '.';}; + // $path_t=$path.".".($pkey+1); + $path_t.=($pkey+1); + $this->search_parts($part, $path_t, $parts_found); + } + } + return $retval; + } + + protected function parce_struct($struct, $path="") + { + $retval["type"] = $struct->type; + $retval["encoding"] = $struct->encoding; + $retval["subtype"] = ($struct->ifsubtype==1)?$struct->subtype:"-1"; + $retval["disposition"] = ($struct->ifdisposition==1)?$struct->disposition:null; + $retval["dparameters"] = ($struct->ifdparameters==1)?$struct->dparameters:null; + $retval["parts_count"] = ($retval["type"] == TYPEMULTIPART)?count($struct->parts):0; + $retval["text-encoding"] = "default"; + if (isset($struct->parameters)) + { + foreach ($struct->parameters as $p_stk) + { + if ($p_stk->attribute=="charset") + { + $retval["text-encoding"] = $p_stk->value; + } + } + } + + // $retval["parts"] = $struct->parts; + return $retval; + } + + /* workaround to make most of headers to parse properly */ + protected static function decodeHeader($hdr, $cset = 'UTF8') + { + // Copied nearly intact from PEAR's Mail_mimeDecode. + $hdr = preg_replace('/(=\?[^?]+\?(q|b)\?[^?]*\?=)(\s)+=\?/i', '\1=?', $hdr); + $m = array(); + if(is_array($hdr)) + $hdr = $hdr[0]; + while(preg_match('/(=\?([^?]+)\?(q|b)\?([^?]*)\?=)/i', $hdr, $m)) + { + $encoded = $m[1]; + $charset = strtoupper($m[2]); + $encoding = strtolower($m[3]); + $text = $m[4]; + + switch($encoding) + { + case 'b': + $text = base64_decode($text); + break; + case 'q': + $text = str_replace('_', ' ', $text); + preg_match_all('/=([a-f0-9]{2})/i', $text, $m); + foreach($m[1] as $value) + $text = str_replace('=' . $value, chr(hexdec($value)), $text); + break; + } + if($charset !== $cset) + $text = self::charconv($charset, $cset, $text); + $hdr = str_replace($encoded, $text, $hdr); + } + return $hdr; + } + + /* workaround to make most of headers to parse properly */ + protected function charconv($enc_from, $enc_to, $text) + { + if(function_exists('iconv')) + return iconv($enc_from, $enc_to, $text); + elseif(function_exists('recode_string')) + return recode_string("$enc_from..$enc_to", $text); + elseif(function_exists('mb_convert_encoding')) + return mb_convert_encoding($text, $enc_to, $enc_from); + return $text; + } +} + + +?> \ No newline at end of file diff --git a/core/fox/mailMessage.php b/core/fox/mailMessage.php new file mode 100644 index 0000000..74104c1 --- /dev/null +++ b/core/fox/mailMessage.php @@ -0,0 +1,370 @@ +["type"=>"INT","nullable"=>false], + "messageId"=>["type"=>"VARCHAR(255)", "nullable"=>true], + "direction"=>["type"=>"CHAR(2)", "nullable"=>false], + "date"=>["type"=>"DATETIME", "nullable"=>true], + "createDate"=>["type"=>"DATETIME", "nullable"=>true], + "inReptyTo"=>["type"=>"VARCHAR(255)", "nullable"=>true], + "subject"=>["type"=>"VARCHAR(255)", "nullable"=>true], + "bodyHTML"=>["type"=>"TEXT", "nullable"=>true], + "bodyPlain"=>["type"=>"TEXT", "nullable"=>true], + "txProto"=>["type"=>"VARCHAR(255)", "nullable"=>true], + "conn"=>["type"=>"SKIP"], + "refNum"=>["type"=>"SKIP"], + ]; + + protected function addAddressToIdx(array &$idx, $val) { + $addr=null; + + if ($val instanceof mailAddress) { $addr = $val; } + elseif ($val instanceof user) { + if (!common::validateEMail($val->eMail)) { throw new \Exception("Invalid user eMail. Failed.", 1509);}; + if ($val->eMailConfirmed) { + $addr = new mailAddress($val->fullName, $val->eMail); + } else { + trigger_error("Address for user ".$val->id."not added - not eMailConfirmed"); + } + } + else { + $addr = new mailAddress($val); + } + if (!$addr) { return false; } + if (mailBlocklist::getByAddress($addr->address)) { + trigger_error("Address ".$addr->address." blockListed."); + return false; + } + foreach ($idx as $rcpt) { + if ($rcpt->address == $addr->address) { return; } + } + + array_push($idx,$addr); + } + + protected function getAttachments() { + if (empty($this->__attachments) && !empty($this->__attachmentsID)) { + foreach ($this->__attachmentsID as $attId) { + $att = new mailAttachment($attId); + $att->message=$this; + array_push($this->__attachments,$att); + } + } + return $this->__attachments; + } + + public function addRecipient($rcptTo) { + $this->addAddressToIdx($this->rcptTo, $rcptTo); + } + + public function addSender($mailFrom) { + $this->addAddressToIdx($this->mailFrom, $mailFrom); + } + + public function addCC($mailFrom) { + $this->addAddressToIdx($this->cc, $mailFrom); + } + + public function addBCC($mailFrom) { + $this->addAddressToIdx($this->bcc, $mailFrom); + } + + public function addAttachment(mailAttachment $att) { + array_push($this->attachments, $att); + } + + public function __get($key) { + switch ($key) { + case "bodyPlain": + if (empty($this->__bodyPlain)) { + $this->__bodyPlain = (new Html2Text($this->bodyHTML))->getText(); + }; + return $this->__bodyPlain; + case "bodyHTML": + if (empty($this->__bodyHTML)) { + return base64_decode($this->__bodyPlain); + } else { + return base64_decode($this->__bodyHTML); + } + case "attachments": + return $this->getAttachments(); + break; + case "isHTML": + return (!empty($this->bodyHTML)); + break; + case "account": + if (empty($this->__account) and !empty($this->__accountId)) { + $this->__account = new mailAccount($this->__accountId); + } + return $this->__account; + + case "subject": + return base64_decode($this->__subject); + default: return parent::__get($key); + } + } + + public function __set($key, $val) { + switch ($key) { + case "account": + if (empty($val)) { + $this->__account=null; + $this->__accountId=null; + } elseif ($val instanceof mailAccount) { + $this->__account = $val; + $this->__accountId=$val->id; + } elseif (gettype($val) == 'string' || gettype($val) == 'integer') { + $this->__account = new mailAccount($val); + $this->__accountId = $this->__account->id; + } + break; + case "references": + $ref = explode(" ", $val); + $this->refIds=[]; + foreach ($ref as $ref_item) { + $ref_item=preg_replace("!^\<|\>$!", '', $ref_item); + array_push($this->refIds, $ref_item); + } + break; + + case "inReplyTo": + $this->inReptyTo=preg_replace("!^\<|\>$!", '', $val); + break; + + case "messageId": + $this->messageId=preg_replace("!^\<|\>$!", '', $val); + break; + case "subject": + $this->__subject=base64_encode($val); + break; + + case "direction": + $dir = strtoupper($val); + if ($dir=='RX' || $dir=='TX') { + $this->direction=$dir; + } else { + throw new \Exception("Invalid direction"); + } + break; + case "mailFrom": + $this->mailFrom=[]; + if (!empty($val)) { + $this->addSender($val); + } + break; + + case "bodyPlain": + $this->__bodyPlain=base64_encode($val); + break; + case "bodyHTML": + $this->__bodyHTML=base64_encode($val); + break; + + default: parent::__set($key, $val); + } + } + + public function createMessageID($uid=null) { + if (empty($uid)) ($uid="XXXX-0000-00"); + $this->messageId=common::getGUIDc()."-CHIMERA-".$uid."-FOX-".time(); + } + + protected function serializeAddresses(&$arr) { + $rv=[]; + foreach ($arr as $addr) { + array_push($rv, $addr->full); + } + return $rv; + } + + protected function deSerealizeAddresses($arr) { + $rv=[]; + foreach ($arr as $addr) { + array_push($rv, new mailAddress($addr)); + } + return $rv; + } + + protected function validateSave() { + if (empty($this->accountId)) { throw new \Exception("AccountID can't be empty"); } + if (empty($this->direction) || ($this->direction !== 'RX' && $this->direction !== 'TX')) { throw new \Exception("direction must be in [RX:TX]");} + return true; + } + + protected function create() { + $this->attachmentsID=[]; + foreach ($this->attachments as $att) { + $att->writeAttachment(); + + array_push($this->attachmentsID, $att->id); + } + parent::create(); + if (isset($this->conn)) { imap_setflag_full($this->conn, $this->refNum, "\Seen");} + } + + + public function preSave() { + if ($this->direction=='TX') { + if (empty($this->__accountId)) { + $this->__account=mailAccount::getDefaultAccount($this->sql); + if (empty($this->__account)) { + throw new \Exception("Default mail account absent!"); + } + $this->__accountId=$this->__account->id; + } + if (empty($this->mailFrom)) { $this->addSender($this->__account->address);} + if (empty($this->messageId)) { $this->createMessageID();} + if (empty($this->date)) { $this->date=time::current();}; + + } + + } + + public function save() { + $this->preSave(); + parent::save(); + + } + + public function send($save=false) { + + if (empty($this->direction)) { $this->direction="TX";} + if ($this->direction != 'TX') { + throw new \Exception("Unable to send message with ".$this->direction." type!"); + } + $this->preSave(); + + $this->__get("account"); + + if ($this->account->txProto!='smtp' || $this->account->txServer == null) { + throw new \Exception("This account can't send messages"); + return false; + } + $mail = new PHPMailer(true); + $mail->CharSet = PHPMailer::CHARSET_UTF8; + $mail->ContentType=PHPMailer::CHARSET_UTF8; + try { + $mail->isSMTP(); // Send using SMTP + $mail->Host = $this->account->txServer; // Set the SMTP server to send through + $mail->SMTPAuth = !empty($this->account->login); // Enable SMTP authentication + $mail->Username = $this->account->login; // SMTP username + $mail->Password = $this->account->password; // SMTP password + $mail->SMTPOptions = ['ssl'=> ['allow_self_signed' => true]]; + if ($this->account->txSSL) { + $mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS; // Enable TLS encryption; `PHPMailer::ENCRYPTION_SMTPS` encouraged + } else { + $mail->SMTPSecure = false; + if ($this->account->txPort==587) { + $mail->SMTPAutoTLS=true; + } else { + $mail->SMTPAutoTLS=false; + } + } + $mail->Port = $this->account->txPort; // TCP port to connect to, use 465 for `PHPMailer::ENCRYPTION_SMTPS` above + + if (empty($this->rcptTo)) { + trigger_error("Empty recipients list, send aborted"); + return false; + } + //Recipients + $mail->setFrom($this->mailFrom[0]->address, $this->mailFrom[0]->name); + foreach ($this->rcptTo as $addr) { + $mail->addAddress($addr->address, '=?UTF-8?B?'.base64_encode($addr->name).'?='); + } + + foreach ($this->cc as $addr) { + $mail->addCC($addr->address, $addr->name); + } + + foreach ($this->bcc as $addr) { + $mail->addBCC($addr->address, $addr->name); + } + + $this->getAttachments(); + + foreach ($this->__get("attachments") as $att) { + $mail->addAttachment($att->getPath(), $att->filename); // Add attachments + } + + // Content + $mail->isHTML($this->isHTML); // Set email format to HTML + $mail->Subject = '=?UTF-8?B?'.base64_encode($this->subject).'?='; + $mail->Subject = $this->subject; + $mail->Body = $this->bodyHTML; + $mail->AltBody = $this->bodyPlain; + $mail->addCustomHeader("Auto-Submitted", "auto-replied"); + $mail->send(); + + + + } catch (mException $e) { + throw new \Exception("Message could not be sent. Mailer Error: {$mail->ErrorInfo}"); + } + + } + +} +?> \ No newline at end of file diff --git a/core/fox/meta/.htaccess b/core/fox/meta/.htaccess new file mode 100644 index 0000000..93169e4 --- /dev/null +++ b/core/fox/meta/.htaccess @@ -0,0 +1,2 @@ +Order deny,allow +Deny from all diff --git a/core/fox/meta/settings.php b/core/fox/meta/settings.php new file mode 100644 index 0000000..2f4ad2d --- /dev/null +++ b/core/fox/meta/settings.php @@ -0,0 +1,53 @@ +enabled) { + $oauth[] = [ + "name"=>$p->name, + "id"=>$p->id, + "icon"=>$p->getClient(null)->getAuthIcon(), + ]; + } + } + + return [ + "title" => config::get("TITLE"), + "sitePrefix" => config::get("SITEPREFIX"), + "theme" => config::get("DEFAULT_THEME") === null ? "chimera" : config::get("DEFAULT_THEME"), + "buildVersion" => "undefined", + "buildDate" => time::current()->dayStart, + "pageSize" => config::get("DEFAULT_PAGESIZE") === null ? "30" : config::get("DEFAULT_PAGESIZE"), + "language" => config::get("DEFAULT_LANGUAGE") === null ? "ru" : config::get("DEFAULT_LANGUAGE"), + "defaultModule" => config::get("DEFAULT_MODULE") === null ? "core" : config::get("DEFAULT_MODULE"), + "sessionRenewInterval" => config::get("SESSION_RENEW_SEC") === null ? "3600" : config::get("SESSION_RENEW_SEC"), + "coreLanguages" => modules::list()["core"]->languages, + "oauthProfiles"=>$oauth, + ]; + } +} +?> \ No newline at end of file diff --git a/core/fox/metadata.php b/core/fox/metadata.php new file mode 100644 index 0000000..d6ab00d --- /dev/null +++ b/core/fox/metadata.php @@ -0,0 +1,98 @@ +[ + "type"=>"INT", + "index"=>"AI", + ], + "module" => [ + "type" => "VARCHAR(128)", + "index" => "INDEX" + ], + "key" => [ + "type" => "VARCHAR(128)", + "index" => "INDEX" + ], + "value" => [ + "type" => "VARCHAR(128)" + ] + ]; + + static function get($key, $module) + { + $conf = static::getAll($module); + if (array_key_exists($key, $conf)) { + return $conf[$key]; + } else { + return null; + } + } + + static function getAll($module, $forceDB = false, $sql = null) + { + $cache = new cache(); + $conf = $cache->get("metadata." . $module); + + if ($forceDB || $conf === null) { + if (empty($sql)) { + $sql = new sql(); + } + $res = $sql->quickExec("select `key`,`value` from `tblMetadata` where `module` = '$module'"); + $conf = []; + while ($row = mysqli_fetch_assoc($res)) { + $conf[$row["key"]] = $row["value"]; + } + $cache->set("metadata." . $module, $conf); + } + return (array) $conf; + } + + static function set($key, $value, $module) + { + $sql = new sql(); + if (static::get($key, $module) !== null) { + $sql->prepareUpdate("tblMetadata"); + $sql->paramAddUpdate("value", $value); + $sql->paramClose(" `module` = '" . $module . "' and `key` = '" . $key . "'"); + $sql->execute(); + } else { + $sql->prepareInsert("tblMetadata"); + $sql->paramAddInsert("module", $module); + $sql->paramAddInsert("key", $key); + $sql->paramAddInsert("value", $value); + $sql->paramClose(); + $sql->execute(); + } + static::getAll($module, true, $sql); + } + + static function del($key, $module) + { + $sql = new sql(); + $sql->quickExec("delete from `tblMetadata` where `module` = '$module' and `key`='$key'"); + static::getAll($module, true, $sql); + } + + static function delAll($module) + { + $sql = new sql(); + $sql->quickExec("delete from `tblMetadata` where `module` = '$module'"); + static::getAll($module, true, $sql); + } +} diff --git a/core/fox/moduleInfo.php b/core/fox/moduleInfo.php new file mode 100644 index 0000000..51caaaf --- /dev/null +++ b/core/fox/moduleInfo.php @@ -0,0 +1,383 @@ + [ + "type" => "INT", + "index" => "INDEX" + ], + "singleInstanceOnly" => [ + "type" => "SKIP" + ], + "isTemplate" => [ + "type" => "SKIP" + ], + "authRequired" => [ + "type" => "SKIP" + ], + "ACLRules" => [ + "type" => "SKIP" + ], + "menuItem" => [ + "type" => "SKIP" + ], + "globalAccessKey" => [ + "type" => "SKIP" + ], + "authRequired" => [ + "type" => "SKIP" + ], + "namespace" => [ + "type" => "SKIP" + ], + "languages" => [ + "type" => "SKIP" + ], + "template" => [ + "type" => "SKIP" + ], + "configKeys" => [ + "type" => "SKIP" + ] + ]; + + + protected static $excludeProps=[]; + + protected function fillFromRow($row) + { + + $rv=parent::fillFromRow($row); + + if (!$this->isTemplate && $this->template) { + $this->ACLRules = (array) $this->template->ACLRules; + $this->menuItem = (array) $this->template->menuItem; + $this->globalAccessKey = $this->template->globalAccessKey; + $this->authRequired = $this->template->authRequired; + $this->namespace = $this->template->namespace; + $this->languages = $this->template->languages; + $this->configKeys= $this->template->configKeys; + } + return $rv; + } + + + + public function __get($key) { + + switch ($key) { + case "template": + if (empty($this->__template)) { + $allMods=modules::list(); + if (array_key_exists($this->instanceOf, $allMods)) { + $this->__template=$allMods[$this->instanceOf]; + } + } + return $this->__template; + default: + return parent::__get($key); + } + } + + public function __set($key, $val) { + switch ($key) { + case "template": + if ($val instanceof moduleInfo) { + $this->__template=$val; + } else { + throw new foxException("Invalid type"); + } + break; + default: + return parent::__set($key, $val); + } + } + + public function __xConstruct() + { + $this->installDate = new time(); + $this->updateDate = new time(); + } + + public function save() + { + if ($this->installDate->isNull()) { + $this->installDate = new time(time()); + } + if ($this->updateDate->isNull()) { + $this->updateDate = new time(time()); + } + if ($this->isTemplate && empty($this->instanceOf)) { + $this->instanceOf = $this->name; + } + + parent::save(); + $this->flushCache(); + } + + public function delete() + { + parent::delete(); + $this->flushCache(); + } + + protected function flushCache() + { + $cache = new cache(); + $cache->set("modules", null); + } + + public static function getAll() + { + $cache = new cache(); + $mods = $cache->get("modules"); + if ($mods !== null) { + $rv = []; + foreach ($mods as $mod) { + $rv[$mod->name] = new self((array) $mod); + } + return $rv; + } + + $mods = modules::list(); + $m = new static(); + $sql = $m->getSql(); + $res = $sql->quickExec($m->__sqlSelectTemplate." order by `modPriority`"); + $rv = []; + while ($row = mysqli_fetch_assoc($res)) { + if (array_key_exists($row["instanceOf"], $mods)) { + $x = new static($row); + + $rv[$row["name"]] = $x; + + } + } + $cache->set("modules", $rv); + return $rv; + } + + public static function getByInstance(string $modInstanceName) : moduleInfo { + $modsInstalled = moduleInfo::getAll(); + if (! array_key_exists($modInstanceName, $modsInstalled)) { + throw new foxException("Module not installed", 404); + } + + return $modsInstalled[$modInstanceName]; + } + + public function getInstances() + { + if (! $this->isTemplate) { + return null; + } + + $this->checkSql(); + $res = $this->sql->quickExec($this->__sqlSelectTemplate . " where `i`.`instanceOf` = '" . $this->name . "'"); + + $rv = []; + while ($row = mysqli_fetch_assoc($res)) { + $rv[] = new self($row); + } + return $rv; + } + + public static function load(string $modName) + {} + + public function export() { + $rv=parent::export(); + if ($this->isTemplate) { + $rv["instances"]=$this->getInstances(); + $rv["instancesCount"]=count($rv["instances"]); + } + + return $rv; + } + + public static function APICall(request $request) { + if (! $request->user->checkAccess("adminModulesInstall", "core")) { + throw new foxException("Forbidden", 403); + } + + switch ($request->method) { + case "GET": + if (! $request->user->checkAccess("adminModulesInstall", "core")) { + throw new foxException("Forbidden", 403); + } + + $modInstanceName = common::clearInput($request->function, "0-9a-zA-Z._-"); + $modsInstalled = static::getAll(); + if (! array_key_exists($modInstanceName, $modsInstalled)) { + throw new foxException("Module not installed", 404); + } + + $mod=$modsInstalled[$modInstanceName]; + if (empty($request->parameters[0])) { + return $mod; + } + + switch ($request->parameters[0]) { + case "features": + $rv=[]; + foreach($mod->template->features as $fx) { + $rv[$fx]=(array_search($fx, $mod->features)===false)?false:true; + } + return $rv; + break; + + case "config": + $rv=[ + "values"=>config::getAll($mod->name), + "keys"=>$mod->configKeys, + ]; + return $rv; + break; + default: + throw new foxException("Method not allowed",405); + + } + break; + + case "DELETE": + if (! $request->user->checkAccess("adminModulesInstall", "core")) { + throw new foxException("Forbidden", 403); + } + + $modInstanceName = common::clearInput($request->function, "0-9a-zA-Z._-"); + $modsInstalled = static::getAll(); + if (! array_key_exists($modInstanceName, $modsInstalled)) { + throw new foxException("Module not installed", 404); + } + + $mod = $modsInstalled[$modInstanceName]; + + if (empty($request->parameters[0])) { + $mod->delete(); + foxRequestResult::throw(200, "Deleted"); + } + + switch ($request->parameters[0]) { + case "features": + $idx = array_search(common::clearInput($request->requestBody->feature),$mod->features); + if ($idx !==false) { + unset($mod->features[$idx]); + $mod->features=array_values($mod->features); + $mod->save(); + foxRequestResult::throw(200, "Deleted"); + } + break; + + case "config": + config::del(common::clearInput($request->requestBody->key), $mod->name); + foxRequestResult::throw(200, "Deleted"); + break; + default: + throw new foxException("Method not allowed",405); + + } + + break; + + case "PUT": + if (! $request->user->checkAccess("adminModulesInstall", "core")) { + throw new foxException("Forbidden", 403); + } + + $modInstanceName = common::clearInput($request->function, "0-9a-zA-Z._-"); + $modsInstalled = static::getAll(); + if (! array_key_exists($modInstanceName, $modsInstalled)) { + throw new foxException("Module not installed", 404); + } + + $mod = $modsInstalled[$modInstanceName]; + + switch ($request->parameters[0]) { + case "features": + $idx = array_search(common::clearInput($request->requestBody->feature),$mod->features); + if ($idx ===false && array_search(common::clearInput($request->requestBody->feature),$mod->template->features) !== false) { + $mod->features[]=common::clearInput($request->requestBody->feature); + $mod->features=array_values($mod->features); + $mod->save(); + foxRequestResult::throw(201, "Created"); + } + foxRequestResult::throw(201, "Created"); + break; + + case "config": + config::set(common::clearInput($request->requestBody->key), $request->requestBody->value, $mod->name); + foxRequestResult::throw(201, "Created"); + break; + default: + throw new foxException("Method not allowed",405); + + } + + break; + + default: + throw new foxException("Method not allowed",405); + } + } +} +?> \ No newline at end of file diff --git a/core/fox/modules.php b/core/fox/modules.php new file mode 100644 index 0000000..c751015 --- /dev/null +++ b/core/fox/modules.php @@ -0,0 +1,249 @@ + [ + "title" => "Core module", + "modVersion" => "4.0.0", + "name" => "core", + "namespace" => "fox", + "features" => [ + "page", + "menu" + ], + "isTemplate" => true, + "singleInstanceOnly" => true, + "authRequired" => true, + "languages" => [ + "ru" + ], + "ACLRules" => [ + "isRoot" => "Superadmin user", + "adminViewModules"=>"Manage modules", + "adminModulesInstall"=>"Install modules", + "adminUsers"=>"Manage users", + "adminUserGroups"=>"Manage userGroups", + ], + "configKeys"=> [ + "converterURL"=>"FoxConverter URL prefix", + ], + "menuItem" => [ + "admin" => [ + "title" => [ + "ru" => "Админка", + "en" => "Admin area" + ], + "function" => null, + "pageKey" => "admin", + "accessRule" => "adminBasicRO", + "items" => [ + [ + "title" => [ + "ru" => "Модули", + "en" => "Modules" + ], + "function" => "modules", + "pageKey" => "adminModules" + ], + [ + "title" => [ + "ru" => "Пользователи", + "en" => "Users" + ], + "function" => "users", + "pageKey" => "adminUsers", + "accessRule" => "adminUsersRO" + ], + [ + "title" => [ + "ru" => "Группы", + "en" => "Groups" + ], + "titleLangIdx" => "adminGroups", + "function" => "groups", + "pageKey" => "adminGrous" + ] + ] + ] + ], + "globalAccessKey" => "allUsers" + ], + "auth" => [ + "title" => "Auth pseudo module", + "modVersion" => "4.0.0", + "name" => "auth", + "namespace" => "fox\\auth", + "features" => [ + "auth" + ], + "isTemplate" => true, + "singleInstanceOnly" => true, + "authRequired" => false, + "ACLRules" => [], + "menuItem" => [], + "globalAccessKey" => "isRoot" + ], + "meta" => [ + "title" => "Metadada pseudo module", + "modVersion" => "4.0.0", + "name" => "meta", + "namespace" => "fox\\meta", + "features" => "", + "isTemplate" => true, + "singleInstanceOnly" => true, + "authRequired" => false, + "ACLRules" => [], + "menuItem" => [], + "globalAccessKey" => "isRoot" + ] + ]; + + public static function list() + { + $rv = []; + + foreach (static::scan() as $modName) { + if (array_key_exists($modName, static::pseudoModules)) { + $modInfo = new moduleInfo(static::pseudoModules[$modName]); + } else { + $modClass = $modName . "\module"; + $modInfo = (new $modClass())::getModInfo(); + } + $rv[$modInfo->name] = $modInfo; + } + + return $rv; + } + + public static function listInstalled() + { + return moduleInfo::getAll(); + } + + public static function scan() + { + $rv = []; + foreach (static::pseudoModules as $key => $val) { + $rv[] = $key; + } + foreach (scandir(__DIR__ . "/../../modules/") as $dir) { + if (preg_match("/^[.]/", $dir)) { + continue; + } + if (! is_dir(__DIR__ . "/../../modules/" . $dir)) { + continue; + } + if (! file_exists(__DIR__ . "/../../modules/" . $dir . "/module.php") && ! file_exists(__DIR__ . "/../../modules/" . $dir . "/Autoloader.php")) { + continue; + } + $rv[] = $dir; + } + return $rv; + } + + // REST API CALLS IMPLEMENTATION + public static function API_PUT_installed(request $request) + { + /** + * Request: + * PUT core/modules/installed + * + * Payload: + * module (string) - name of module + * name (string) (optional) - name of module instance. + * priority (int) (optional) - module priority + * + * Reply: + * 201: Created + * object: Installed module + * 404: Not found - module not found + * 409: Already installed + * 409: Multi-instance not allowed + */ + if (! $request->user->checkAccess("adminModulesInstall", "core")) { + throw new foxException("Forbidden", 403); + } + + $modName = common::clearInput($request->requestBody->module, "0-9a-zA-Z._-"); + $modInstanceName = common::clearInput($request->requestBody->name, "0-9a-zA-Z._-"); + $modPriority= common::clearInput($request->requestBody->priority, "0-9"); + if (empty($modInstanceName)) { + $modInstanceName = $modName; + } + + $modules = static::list(); + if (! array_key_exists($modName, $modules)) { + throw new foxException("Module " . $modName . " not present", 404); + } + + $mod = $modules[$modName]; + + $modsInstalled = moduleInfo::getAll(); + if (array_key_exists($modInstanceName, $modsInstalled)) { + foxException::throw("ERR","Already installed",409,"ALI"); + } + + if ($mod->singleInstanceOnly && count($mod->getInstances()) > 0) { + foxException::throw("ERR","Multi-instances not allowed", 409,"MIN"); + + } + + $mod->instanceOf = $mod->name; + $mod->name = $modInstanceName; + if (!empty($modPriority)) { + $mod->modPriority=$modPriority; + } + $mod->save(); + + foxRequestResult::throw(201, "Created", $mod); + } + + public static function API_GET_list(request $request) + { + + if (! $request->user->checkAccess("adminViewModules", "core")) { + throw new foxException("Forbidden", 403); + } + return static::list(); + } + + public static function API_POST_instances(request $request) + { + + if (! $request->user->checkAccess("adminViewModules", "core")) { + throw new foxException("Forbidden", 403); + } + + $modName = common::clearInput($request->requestBody->module, "0-9a-zA-Z._-"); + $modules = static::list(); + if (! array_key_exists($modName, $modules)) { + throw new foxException("Module " . $modName . " not present", 404); + } + + return $modules[$modName]->getInstances(); + } + + public static function API_GET_installed(request $request) + { + if (! $request->user->checkAccess("adminViewModules", "core")) { + throw new foxException("Forbidden", 403); + } + return static::listInstalled(); + } + +} +?> \ No newline at end of file diff --git a/core/fox/noSqlMigration.php b/core/fox/noSqlMigration.php new file mode 100644 index 0000000..7aa0f2e --- /dev/null +++ b/core/fox/noSqlMigration.php @@ -0,0 +1,19 @@ + \ No newline at end of file diff --git a/core/fox/oAuthClient.php b/core/fox/oAuthClient.php new file mode 100644 index 0000000..6d80672 --- /dev/null +++ b/core/fox/oAuthClient.php @@ -0,0 +1,207 @@ +[ + "authorization_endpoint"=>"https://oauth.vk.com/authorize", + "token_endpoint"=>"https://oauth.vk.com/access_token", + "icon"=>"fab fa-vk", + ], + "yandex"=>[ + "authorization_endpoint"=>"https://oauth.yandex.ru/authorize", + "token_endpoint"=>"https://oauth.yandex.ru/token", + "userinfo_endpoint"=>"https://login.yandex.ru/info", + "icon"=>"fab fa-yandex", + ], + "gitlab"=>[ + "authorization_endpoint"=>'${URL}/oauth/authorize', + "token_endpoint"=>'${URL}/oauth/token', + "userinfo_endpoint"=>'${URL}/oauth/userinfo', + "icon"=>"fab fa-gitlab", + "scope"=>"openid", + ], + "gitea"=>[ + "authorization_endpoint"=>'${URL}/login/oauth/authorize', + "token_endpoint"=>'${URL}/login/oauth/access_token', + "jwks_uri"=>'${URL}/login/oauth/keys', + "userinfo_endpoint"=>'${URL}/login/oauth/userinfo', + "icon"=>"fas fa-coffee", + "scope"=>"openid", + ], + + ]; + + public function __construct($url, $id, $key, $redirect, $scope="openid", $config=null) { + $this->url=$url; + $this->appId=$id; + $this->appKey=$key; + $this->redirectURL=$redirect; + $this->scope=$scope; + + if (gettype($config)=="string") { + if (array_key_exists($config, static::configs)) { + $this->mode=$config; + $ref=[]; + foreach (static::configs[$config] as $key=>$val) { + $ref[$key]=str_replace('${URL}', $this->url, $val); + } + $this->config=(object)$ref; + if (empty($this->scope) && !empty($ref["scope"])) { $this->scope = $ref["scope"]; } + } else { + throw new foxException("Invalid config mode"); + } + + } else { + $this->config=$config; + } + } + + public function getConfig() { + if (empty($this->config)) { + $this->config=json_decode(file_get_contents($this->url."/.well-known/openid-configuration")); + if (empty($this->config)) { + throw new \Exception("Unable to fetch Oauth2 Config"); + } + } + } + + public function getAuthURL() { + $this->getConfig(); + return $this->config->authorization_endpoint."?response_type=code&client_id=$this->appId&redirect_uri=".urlencode($this->redirectURL).(empty($this->scope)?"":"&scope=".$this->scope); + } + + public function getAuthIcon() { + if (property_exists($this->config,"icon")) { + return $this->config->icon; + } else { + return "fa-brands fa-openid"; + } + } + + public function getToken() { + return $this->xTokens; + } + + public function getTokenByCode($code,$mode="generic") { + if (empty($code)) { + throw new foxException("Empty code not allowed"); + } + $this->getConfig(); + switch ($this->mode) { + case "yandex": + $req=new restRequest(null,"POST","grant_type=authorization_code&client_id=".urlencode($this->appId)."&client_secret=".urlencode($this->appKey)."&code=$code"); + break; + case "vk": + $req=new restRequest(null,"GET",[ + "client_id"=>$this->appId, + "client_secret"=>$this->appKey, + "code"=>$code, + "grant_type"=>"authorization_code", + "redirect_uri"=>$this->redirectURL + ]); + break; + default: + $req=new restRequest(null,"POST",[ + "client_id"=>$this->appId, + "client_secret"=>$this->appKey, + "code"=>$code, + "grant_type"=>"authorization_code", + "redirect_uri"=>$this->redirectURL + ]); + break; + } + + $client=new restClient($this->config->token_endpoint); + $res=$client->exec($req); + if ($res->success) { + $this->xTokens=$res->reply; + return $this->xTokens; + } else { + trigger_error(json_encode($res)); + throw new foxException("Invalid token"); + exit; + } + } + + public function getUserInfo($mode="generic") { + $this->getConfig(); + $this->getToken(); + + switch ($this->mode) { + case "vk": + $client = new restClient("https://api.vk.com/method/users.get", $this->xTokens->access_token, "Authorization: Bearer"); + $res2=$client->exec(new restRequest(null,"GET",[ + 'uids' => $this->xTokens->user_id, + 'fields' => 'uid,first_name,last_name', + 'v'=>'5.131', + ])); + break; + default: + $client = new restClient($this->config->userinfo_endpoint, $this->xTokens->access_token, "Authorization: Bearer"); + $res2=$client->exec(new restRequest(null,"GET")); + break; + } + + + + if ($res2->success) { + switch ($this->mode) { + case "yandex": + $rv=(object)[ + "name"=>$res2->reply->real_name, + "sub"=>$res2->reply->id, + "email"=>$res2->reply->default_email, + "groups"=>[], + ]; + break; + case "vk"; + $rv=(object)[ + "name"=>$res2->reply->response[0]->first_name." ".$res2->reply->response[0]->last_name, + "sub"=>$res2->reply->response[0]->id, + "email"=>null, + "groups"=>[], + ]; + break; + + default: + @$rv=(object)[ + "name"=>$res2->reply->name, + "sub"=>$res2->reply->sub, + "email"=>$res2->reply->email, + "groups"=>$res2->reply->groups, + ]; + break; + } + return $rv; + } else { + trigger_error(json_encode($res2)); + throw new foxException("Unable to fetch userinfo"); + } + } + } \ No newline at end of file diff --git a/core/fox/oAuthProfile.php b/core/fox/oAuthProfile.php new file mode 100644 index 0000000..b6e9ff3 --- /dev/null +++ b/core/fox/oAuthProfile.php @@ -0,0 +1,63 @@ +["type"=>"VARCHAR(255)","nullable"=>false], + "url"=>["type"=>"VARCHAR(255)", "nullable"=>true], + "clientId"=>["type"=>"TEXT","nullable"=>false], + "clientKey"=>["type"=>"TEXT","nullable"=>false], + "config"=>["type"=>"TEXT","nullable"=>true], + "enabled"=>["type"=>"INT", "default"=>1], + "deleted"=>["type"=>"INT", "default"=>0], + "hash"=>["type"=>"VARCHAR(255)"], + ]; + + protected function validateSave() + { + if (empty($this->hash)) { + $this->hash=xcrypt::hash(json_encode($this)); + } + return true; + } + + public function getClient($redirectUrl, $scope=null) : oAuthClient { + return new oAuthClient($this->url, $this->clientId, $this->__clientKey, $redirectUrl."/".$this->hash,$scope,$this->config); + } + + public static function getByHash($hash) { + $ref = new static(); + $sql = $ref->getSql(); + $res = $sql->quickExec1Line($ref->sqlSelectTemplate." where `i`.`hash` = '".common::clearInput($hash,"0-9a-z")."'"); + if ($res) { + return new static($res); + } else { + throw new foxException("Invalid hash"); + } + } +} +?> \ No newline at end of file diff --git a/core/fox/oasis.php b/core/fox/oasis.php new file mode 100644 index 0000000..73ed35e --- /dev/null +++ b/core/fox/oasis.php @@ -0,0 +1,426 @@ +tmpath = $tmpath; + } + if (! empty($srcFilePath)) { + $this->loadODF($srcFilePath); + } + } + + public function loadODF($srcFile) + { + $mimeType = file_get_contents("zip://" . $srcFile . "#mimetype"); + if (! $mimeType) { + throw new Exception("Unable to determine file type of " . $srcFile); + } + + switch ($mimeType) { + case "application/vnd.oasis.opendocument.text": + $this->srcFileType = 'odt'; + break; + case "application/vnd.oasis.opendocument.spreadsheet": + $this->srcFileType = 'ods'; + break; + default: + throw new Exception("Unknown file type: $mimeType"); + } + + $this->srcFileName = $srcFile; + + $this->dom = new DOMDocument(); + $this->dom->load("zip://" . $this->srcFileName . "#content.xml"); + } + + public function saveODF($newFileName = null) + { + if (empty($newFileName)) { + + $uuid = common::getGUIDc(); + + if (file_exists($this->tmpath) && ! is_dir($this->tmpath)) { + throw new Exception("TMPatn not a directory!"); + } + + if (! file_exists($this->tmpath)) { + mkdir($this->tmpath); + } + + if (file_exists($this->tmpath . "/" . $uuid . "." . $this->srcFileType)) { + $uuid = common::getGUIDc(); + } + + $newFileName = $this->tmpath . "/" . $uuid . "." . $this->srcFileType; + $this->newFileIsTemporary = true; + } else { + $this->newFileIsTemporary = false; + } + + $this->newFileName = $newFileName; + copy($this->srcFileName, $this->newFileName); + + $this->commit(); + $xml = $this->dom->saveXML(); + // file_put_contents("$tmpath/$guid/content.xml", $xml); + + $zip = new ZipArchive(); + + $res = $zip->open($this->newFileName); + if ($res === TRUE) { + $zip->addFromString('content.xml', $xml); + $zip->close(); + } else { + throw new Exception("Unable to save result"); + } + + return $newFileName; + } + + public function export($type, $newFileName = null) + { + $ods = $this->saveODF(); + return fileConverter::convert($ods, $newFileName, $type); + } + + public function commit() + { + if ($this->srcFileType == 'ods') { + $this->odsCommitParams(); + } + + if (! empty($this->parent) && ! empty($this->parent->documentElement)) { + // Импортируем созданый ранее элемент в текущее дерево + $newnode = $this->dom->importNode($this->parent->documentElement, true); + + $this->oldnode->parentNode->replaceChild($newnode, $this->oldnode); + + $this->parent = null; + $this->oldnode = null; + $this->parent_node = null; + $this->dom2 = null; + } + } + + public function odsCommitParams($erase_notfound_params = true) + { + if ($this->srcFileType != 'ods') { + return false; + } + if (empty($this->odsParams)) { + return true; + } + + $xpath = new DOMXpath($this->dom); + $nodelist = $xpath->query("/office:document-content/office:body/office:spreadsheet/table:table"); + $this->oldnode = $nodelist->item(0); + + $this->parent = new DomDocument(); + $this->parent_node = $this->parent->importNode($this->oldnode); + + foreach ($this->oldnode->childNodes as $t_node) { + if ($t_node->nodeName == 'table:table-row') { + foreach ($t_node->getElementsByTagname("*") as $t2_node) { + $res = null; + if ($t2_node->nodeName == 'text:p' && preg_match("/^$/", $t2_node->nodeValue, $res)) { + if ((array_key_exists($res[1], $this->odsParams))) { + + $this->odsUpdateCellValue($this->odsParams[$res[1]], $t2_node->parentNode); + } elseif ($erase_notfound_params) { + $this->odsUpdateCellValue(null, $t2_node->parentNode); + } + } + } + } + } + $this->odsParams = null; + } + + public function addParam($paramName, $paramValue) + { + if ($this->srcFileType == 'odt') { + + if (is_null($this->parent)) { + $this->prepareODFParam(); + } + // Создаем дочерний элемент в структуре + $child_node = $this->parent->createElement('text:user-field-decl'); + + $attribute = $this->parent->createAttribute("office:value-type"); + $attribute->value = "string"; + $child_node->appendChild($attribute); + + $attribute = $this->parent->createAttribute("office:string-value"); + $attribute->value = $paramValue; + $child_node->appendChild($attribute); + + $attribute = $this->parent->createAttribute("text:name"); + $attribute->value = $paramName; + $child_node->appendChild($attribute); + + $this->parent_node->appendChild($child_node); + + $this->parent->appendChild($this->parent_node); + // закончили создавать дочерний элемент + } elseif ($this->srcFileType == 'ods') { + if (empty($this->odsParams)) { + $this->odsParams = []; + } + ; + $this->odsParams[$paramName] = $paramValue; + } + } + + public function odtTableRowAdd($tag, $row_count) + { + if ($this->srcFileType != 'odt') { + return false; + } + if (is_null($this->dom2)) { + $this->odtPrepareTableRowAdd(); + } + + $nodelist = $this->dom->getElementsByTagname("user-field-get"); + $table_row_node = null; + + foreach ($nodelist as $node) { + + if (! is_null($table_row_node)) { + break; + } + ; + + $tag_class = explode('.', $node->getAttribute('text:name'), 2)[0]; + + if ($tag_class == $tag) { + $n = $node; + while (($n = $n->parentNode) && (is_null($table_row_node))) { + if ($n->nodeName == 'table:table-row') { + $table_row_node = $n; + break; + } + } + } + } + + if (is_null($table_row_node)) { + return - 1; + } + + $table_node = $table_row_node->parentNode; + + $element = $this->dom2->importNode($table_node, false); + + foreach ($table_node->childNodes as $node) { + $nn = $this->dom2->importNode($node, true); + + $nodelistS = $node->getElementsByTagname("user-field-get"); + + $nodeV = false; + + foreach ($nodelistS as $nodeS) { + $tag_class = explode('.', $nodeS->getAttribute('text:name'), 2)[0]; + + if ($tag_class == $tag) { + $nodeV = true; + break; + } + } + + if ($nodeV) { + // print "NODEV ".$nodeS->getAttribute('text:name')."\n"; + for ($i = 0; $i < $row_count; $i ++) { + $element->appendChild($this->odtTableNodeAddIdx($table_row_node, $tag, $i)); + } + } else { + // print "General Node\n"; + $element->appendChild($nn); + } + } + + $newnode = $this->dom->importNode($element, true); + $table_node->parentNode->replaceChild($newnode, $table_node); + return 1; + } + + public function odsInsertData($arr, $marker = '') + { + if ($this->srcFileType != 'ods') { + return false; + } + if (empty($this->parent)) { + $xpath = new DOMXpath($this->dom); + $nodelist = $xpath->query("/office:document-content/office:body/office:spreadsheet/table:table"); + $this->oldnode = $nodelist->item(0); + + $this->parent = new DomDocument(); + // $this->parent_node = $this->parent->createElement('table:table'); + $this->parent_node = $this->parent->importNode($this->oldnode); + } + + $marker_found = false; + foreach ($this->oldnode->childNodes as $t_node) { + if (! $marker_found && $t_node->nodeName == 'table:table-row') { + foreach ($t_node->getElementsByTagname("*") as $t2_node) { + if ($t2_node->nodeName == 'text:p' && $t2_node->nodeValue == $marker) { + $marker_found = true; + $t2_row = $t2_node->parentNode->parentNode; + for ($i = 0; $i < count($arr); $i ++) { + $v_arr = $arr[$i]; + $v_idx = 0; + foreach ($t2_row->childNodes as $t2_cell) { + $v_idx ++; + if ($v_idx - 1 > array_key_last($v_arr)) { + $val = null; + } elseif (array_key_exists($v_idx - 1, $v_arr)) { + $val = $v_arr[$v_idx - 1]; + } else { + $val = null; + } + + $this->odsUpdateCellValue($val, $t2_cell); + } + $this->parent_node->appendChild($this->parent->importNode($t_node, true)); + } + continue (2); + } + } + } + $this->parent_node->appendChild($this->parent->importNode($t_node, true)); + } + $this->parent->appendChild($this->parent_node); + } + + protected function odsUpdateCellValue($val, $t2_cell) + { + switch (gettype($val)) { + case "string": + $val_type = 'string'; + break; + case "integer": + $val_type = 'float'; + break; + case "float": + $val_type = 'float'; + break; + case "double": + $val_type = 'float'; + break; + case "NULL": + $val_type = 'null'; + break; + default: + $val_type = 'string'; + break; + } + + foreach ($t2_cell->childNodes as $t2_child) { + $t2_cell->removeChild($t2_child); + } + + switch ($val_type) { + case "string": + $t2_cell->setAttribute("office:value-type", $val_type); + $t2_cell->setAttribute("calcext:value-type", $val_type); + $t2_cell->appendChild($this->dom->createElement('text:p', $val)); + break; + + case "null": + $t2_cell->removeAttribute("office:value-type"); + $t2_cell->removeAttribute("calcext:value-type"); + $t2_cell->removeAttribute("office:value"); + break; + + case "float": + $t2_cell->setAttribute("office:value-type", $val_type); + $t2_cell->setAttribute("calcext:value-type", $val_type); + $t2_cell->setAttribute("office:value", $val); + break; + } + } + + protected function prepareODFParam() + { + $xpath = new DOMXpath($this->dom); + $nodelist = $xpath->query("/office:document-content/office:body/office:text/text:user-field-decls"); + + $this->oldnode = $nodelist->item(0); + + $this->parent = new DomDocument(); + $this->parent_node = $this->parent->createElement('text:user-field-decls'); + } + + protected function odtPrepareTableRowAdd() + { + $this->dom2 = new DomDocument(); + } + + protected function odtTableNodeAddIdx($row_node, $key, $idx) + { + $d2node = $this->dom2->importNode($row_node, true); + + $nodelist = $d2node->getElementsByTagname("user-field-get"); + + foreach ($nodelist as $node) { + + $tag_class = explode('.', $node->getAttribute('text:name'), 2)[0]; + + if ($tag_class == $key) { + $node->setAttribute('text:name', $node->getAttribute('text:name') . $idx); + } + } + + return $d2node; + } + + public function __destruct() + { + if ($this->newFileIsTemporary && file_exists($this->newFileName)) { + unlink($this->newFileName); + } + } +} + +?> \ No newline at end of file diff --git a/core/fox/objectStorageClient.php b/core/fox/objectStorageClient.php new file mode 100644 index 0000000..37daea5 --- /dev/null +++ b/core/fox/objectStorageClient.php @@ -0,0 +1,32 @@ + \ No newline at end of file diff --git a/core/fox/prometheus.php b/core/fox/prometheus.php new file mode 100644 index 0000000..7114765 --- /dev/null +++ b/core/fox/prometheus.php @@ -0,0 +1,66 @@ +parseString($ref); + } + } + + public function __get($key) { + switch($key) { + case "items": + return $this->__items; + default: + throw new foxException("Invalid READ for ".$key." in class ".__CLASS__); + } + } + + protected function parseString($str) { + // TODO: implement fromString() + $this->__items=[]; + $ref = preg_replace("/\r/", "", $str); + foreach (explode("\n", $ref) as $line) { + try { + $this->__items[]=item::fromString($line); + + } catch (\Exception $e) { + + } + } + + } + + public function __toString(): string + { + // TODO: implement __toString() + } + + public function __fromString($val) + { + $this->__construct($val); + } + + public function isNull(): bool + { + return empty($this->__items); + } +} + diff --git a/core/fox/prometheus/item.php b/core/fox/prometheus/item.php new file mode 100644 index 0000000..6fd3db4 --- /dev/null +++ b/core/fox/prometheus/item.php @@ -0,0 +1,84 @@ +key=$key; + $this->value=$val; + if ($meta) { $this->metaData=$meta; } + } + + public function __toString(): string + { + return $this->key.(empty($this->metaData)?"":"{".$this->encodeMetadata()."}")." ".$this->value; + } + + protected function encodeMetadata() : string { + $rv=""; + foreach ($this->metaData as $key=>$val) { + $rv.=(empty($rv)?"":",").$key."=\"".$val."\""; + } + return $rv; + } + + public static function fromString($val) { + $item=new static(); + $item->__fromString($val); + return $item; + } + + public function __fromString($val) + { + $ref=[]; + if (preg_match("/^([^# ][^ {]*)(\{([^ }]*)\}){0,1} (.*)$/", $val, $ref)) { + $this->key=$ref[1]; + if (is_numeric($ref[4])) { + if ((int)$ref[4]==(float)$ref[4]) { + $this->value=(int)$ref[4]; + } else { + $this->value=(float)$ref[4]; + } + } else { + $this->value=$ref[4]; + } + + if ($ref[3] !== "") { + $this->metaData=[]; + foreach(explode(",", $ref[3]) as $kvp) { + $kvx=[]; + if (preg_match("/([^=]*)=(.*)$/", $kvp,$kvx)) { + $xVal=preg_replace('/^\"/', '', preg_replace('/\"$/','',$kvx[2])); + $this->metaData[$kvx[1]]=$xVal; + } + } + } + + } else { + throw new foxException("Unable to parce string ".$val." in ".__CLASS__."->__fromString"); + } + } + + public function isNull(): bool + { + return ($this->value === null || $this->value === false || $this->value === ""); + } +} +?> \ No newline at end of file diff --git a/core/fox/request.php b/core/fox/request.php new file mode 100644 index 0000000..f79b09f --- /dev/null +++ b/core/fox/request.php @@ -0,0 +1,177 @@ +parce(); + return false; + } + + function parce() + { + $this->method = (empty($_SERVER["REQUEST_METHOD"]) ? "GET" : $_SERVER["REQUEST_METHOD"]); + if (array_key_exists("FOX_REWRITE", $_SERVER) && $_SERVER["FOX_REWRITE"] != "yes") { + $prefix = ($_SERVER["CONTEXT_PREFIX"] . "index.php/"); + } else { + $prefix = ($_SERVER["CONTEXT_PREFIX"]); + } + + $this->clientIp = static::getClientIP(); + + $this->requestBody = json_decode(file_get_contents("php://input")); + + $prefix = preg_replace([ + "![/]+!", + "![\.]+!" + ], [ + "\/", + "\." + ], $prefix); + $req = (preg_replace("/" . $prefix . "/", '', $_SERVER["REQUEST_URI"])); + $req = explode("/", explode("?", $req, 2)[0]); + + if (count($req) > 0) { + if ($req[count($req) - 1] == "") { + array_splice($req, - 1); + } + if ($req[0] == "") { + array_splice($req, 0, 1); + } + } + + $this->module = ((count($req) > 0) ? $req[0] : NULL); + $this->instance=$this->module; + $this->function = ((count($req) > 1) ? $req[1] : NULL); + + if (count($req) > 2) { + array_splice($req, 0, - (count($req) - 2)); + } else { + $req = []; + } + $this->parameters = $req; + + if (array_key_exists("Authorization", apache_request_headers()) || (array_key_exists("Token", $_COOKIE) && preg_match("/^[0-9A-Za-z=+_-]*$/", $_COOKIE["Token"]))) { + $match = []; + + /* + * removed Cookie authorization + * + * + * if ((empty($this->__xId) || $this->__xId=="WEB") && array_key_exists("Token", $_COOKIE) && preg_match("/^[0-9A-Za-z=+_-]*$/", $_COOKIE["Token"])) { + * $this->authMethod="Cookie"; + * $this->__xId="WEB"; + * $this->doTokenAuth($_COOKIE["Token"]); + * + * } else + */ + if (preg_match("/^(Token) ([A-Za-z0-9=+_-]*)$/", apache_request_headers()["Authorization"], $match)) { + $this->authMethod = $match[1]; + switch ($this->authMethod) { + case "Token": + $this->doTokenAuth($match[2]); + break; + } + } else { + $this->authMethod = $this->authVal = null; + } + } + } + + protected function doTokenAuth($val) + { + if (preg_match("/^[0-9A-Za-z=+_-]*$/", $val)) { + $token = authToken::getByToken($val); + if ($token && (empty($this->__xId) || $token->type == $this->__xId)) { + $this->token = $token; + + if ($this->token->user->active) { + $this->authOK = true; + } else { + $this->authOK = false; + } + } + } else { + $this->authMethod = $this->authVal = null; + $this->authOK = false; + } + } + + function shift() + { + $this->instance=$this->module; + $this->module = $this->function; + $this->function = array_shift($this->parameters); + return $this; + } + + public function __get($key) + { + switch ($key) { + case "user": + if (! $this->authOK) { + throw new foxException('Unauthorized', 401); + } + return $this->token->user; + default: + return parent::__get($key); + } + } + + public static function get($type = null) + { + global $__foxRequest; + if (empty($__foxRequest)) { + $__foxRequest = new static($type); + } + return $__foxRequest; + } +} +?> \ No newline at end of file diff --git a/core/fox/restClient.php b/core/fox/restClient.php new file mode 100644 index 0000000..89522de --- /dev/null +++ b/core/fox/restClient.php @@ -0,0 +1,94 @@ +urlPrefix=$urlPrefix; + $this->authToken=$authToken; + $this->authTokenName=$authTokenName; + } + + public function exec(restRequest $request, bool $debug=false) : restResult { + $rv=new restResult(); + + $ch = curl_init(); + if (gettype($request->extraHeaders)=='array') { + $options = array_merge($this->extraHeaders, (gettype($request->extraHeaders)=='array')?$request->extraHeaders:[]); + } else { + $options = $this->extraHeaders; + } + + if ($this->authToken) { array_push($options, $this->authTokenName." ".$this->authToken); }; + + switch (strtoupper($request->method)) { + case "PUT": + curl_setopt($ch, CURLOPT_PUT, 1); + break; + + case "POST": + curl_setopt($ch, CURLOPT_POST, 1); + break; + + default: + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, strtoupper($request->method)); + break; + + } + + + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_HEADER, 0); + + curl_setopt($ch, CURLOPT_HEADERFUNCTION, array($rv, "handleHeaderLine")); + + curl_setopt($ch, CURLOPT_HTTPHEADER, $options); + + $payload=$request->data; + if (gettype($payload) != 'string') { + $payload=json_encode($payload); + } + + $url=$this->urlPrefix.$request->function; + + if (!empty($payload)) { + if ($request->method=="GET") { + $urlSuffix=""; + foreach($request->data as $key=>$val) { + $urlSuffix.=(empty($urlSuffix)?"":"&").urlencode($key)."=".urlencode($val); + } + $url.="?".$urlSuffix; + + } else { + curl_setopt( $ch, CURLOPT_POSTFIELDS, $payload); + } + } + curl_setopt($ch, CURLOPT_URL, $url); + + $rv->rawReply = curl_exec($ch); + $rv->reply=json_decode($rv->rawReply); + if ($debug) { $rv->curlInfo=curl_getinfo($ch); } else { $rv->curlInfo=null; } + $rv->statusCode= curl_getinfo($ch, CURLINFO_RESPONSE_CODE); + $rv->success=(array_search($rv->statusCode, $request->successCodes)!==false); + + curl_close($ch); + return $rv; + + } + +} +?> \ No newline at end of file diff --git a/core/fox/restRequest.php b/core/fox/restRequest.php new file mode 100644 index 0000000..8cc9ae3 --- /dev/null +++ b/core/fox/restRequest.php @@ -0,0 +1,28 @@ +function=$function; } + if (!empty($method)) { $this->method=$method; } + if (!empty($data)) { $this->data=$data; } + if (!empty($xtraHeaders)) { $this->extraHeaders=$xtraHeaders; } + } + public $method="GET"; + public $function=null; + public $data=[]; + public $extraHeaders=[]; + public $successCodes=["200","201"]; +} + +?> \ No newline at end of file diff --git a/core/fox/restResult.php b/core/fox/restResult.php new file mode 100644 index 0000000..8261429 --- /dev/null +++ b/core/fox/restResult.php @@ -0,0 +1,38 @@ +replyHeaders[trim($r[1])] = trim($r[2]); + } elseif (preg_match("/^(HTTP\/.*)/", trim($header_line), $r)) { + $this->replyHeaders["HTTP_STATUS_LINE"] = trim($r[1]); + $this->statusText=trim($r[1]); + } else if (strlen(trim($header_line)) > 0 ){ + array_push($this->replyHeaders, trim($header_line)); + } + return strlen($header_line); + + } +} + +?> \ No newline at end of file diff --git a/core/fox/s3client.php b/core/fox/s3client.php new file mode 100644 index 0000000..c190b5d --- /dev/null +++ b/core/fox/s3client.php @@ -0,0 +1,118 @@ +prefix = $prefix; + } + + if (empty($endpoint)) { + $endpoint = config::get("s3_endpoint"); + $accessKey = config::get("s3_login"); + $secretKey = config::get("s3_secret"); + $regionId = (empty(config::get("s3_region")) ? "ru-1" : config::get("s3_region")); + + if (empty(config::get("s3_prefix"))) { + $this->prefix = ""; + } else { + $this->prefix = config::get("s3_prefix") . "-"; + } + } + + if (empty($endpoint) || empty($accessKey) || empty($secretKey) || empty($regionId)) { + throw new \Exception("Credentials can't be empty!"); + } + + $this->s3 = new \Aws\S3\S3Client([ + 'version' => 'latest', + 'region' => $regionId, + 'endpoint' => $endpoint, + 'use_path_style_endpoint' => true, + 'credentials' => [ + 'key' => $accessKey, + 'secret' => $secretKey + ] + ]); + } + + public function getObject($bucket, $key) + { + $res = $this->s3->getObject([ + "Bucket" => $this->prefix . $bucket, + "Key" => $key + ]); + return (string) $res["Body"]; + } + + public function putObject($bucket, $key, $data) + { + $this->s3->putObject([ + "Bucket" => $this->prefix . $bucket, + "Key" => $key, + "Body" => $data + ]); + } + + public function deleteObject($bucket, $key) + { + $this->s3->deleteObject([ + "Bucket" => $this->prefix . $bucket, + "Key" => $key + ]); + } + + public function listObjects($bucket) + { + return $this->s3->listObjects([ + "Bucket" => $this->prefix . $bucket + ])["Contents"]; + } + + public function exec($method, $args = []) + { + return $this->s3->{$method}($args); + } + + public function createBucket($bucket) + { + return $this->s3->createBucket([ + "Bucket" => $this->prefix . $bucket + ]); + } + + public function deleteBucket($bucket) + { + return $this->s3->deleteBucket([ + "Bucket" => $this->prefix . $bucket + ]); + } +} + +?> \ No newline at end of file diff --git a/core/fox/sql.php b/core/fox/sql.php new file mode 100644 index 0000000..9d1349e --- /dev/null +++ b/core/fox/sql.php @@ -0,0 +1,455 @@ +&$conn) { + $conn->__destruct(); + unset($sqlConnArray[$key]); + } + } + $sqlConnArray=[]; + } + + function __destruct() { + + } + + function __construct($server = null, $db = null, $user = null, $passwd = null) + { + if (! isset($server)) { + $server = config::get("sqlServer"); + $user = config::get("sqlUser"); + $passwd = config::get("sqlPasswd"); + $db = config::get("sqlDB"); + } + + $this->server = $server; + $this->db = $db; + $this->user = $user; + $this->passwd = $passwd; + $this->connected = false; + } + + function connect($server = null, $db = null, $user = null, $passwd = null) + { + if (! $this->connected) { + if (isset($server)) { + $this->server = $server; + $this->db = $db; + $this->user = $user; + $this->passwd = $passwd; + $this->connected = false; + } + + $this->mysqli = @mysqli_connect($this->server, $this->user, $this->passwd, $this->db); + mysqli_set_charset($this->mysqli, "utf8"); + if (! $this->mysqli) { // Если дескриптор равен 0 соединение не установлено + throw new Exception("SQL Connection to $this->server failed"); + exit(); + } + $this->connected = true; + } + return $this->mysqli; + } + + function quickExec($sqlQueryString, &$result = null, $hideError = null) + { + $this->connect(); + if ($this->mysqli->connect_errno) { + if (! isset($hideError)) { + throw new Exception("SQL Connect error"); + } + ; + return null; + } + + $result = $this->mysqli->query($sqlQueryString); + + if (! $result) { + if (! isset($hideError)) { + throw new Exception("SQL Error: ." . $this->mysqli->error); + } + ; + return null; + exit(); + } + return $result; + } + + function quickExec1Line($sqlQueryString, &$result = null, $hideError = null) + { + $this->connect(); + $result = $this->quickExec($sqlQueryString, $result, $hideError); + if (mysqli_num_rows($result) == 0) { + return null; + } + + $retVal = mysqli_fetch_assoc($result); + return $retVal; + } + + // General functions + function prepare() + { + $this->ctr = 0; + $this->bind_names = null; + $this->param_type = null; + $this->stmt = null; + $this->sqlQueryString = null; + $this->sqlQueryStringL = null; + $this->paramClosed = false; + } + + function prepareUpdate($tableName) + { + $this->prepare(); + $this->queryType = "update"; + $this->sqlQueryString = "UPDATE `$tableName` SET"; + } + + function prepareInsert($tableName) + { + $this->prepare(); + $this->queryType = "insert"; + $this->sqlQueryString = "INSERT INTO `$tableName` ("; + } + + function execute() + { + if (! $this->paramClosed && $this->queryType == "insert") { + $this->paramClose(); + } + if (! $this->paramClosed) { + throw new Exception("Params not closed for " . $this->queryType . "!"); + } + + $this->connect(); + if ($this->ctr > 0) { + $this->stmt = mysqli_prepare($this->mysqli, $this->sqlQueryString); + + if (! $this->stmt) { + $err = 'ERR:EXEC 1P' . mysqli_errno($this->mysqli) . ' ' . mysqli_error($this->mysqli); + throw new Exception($err, mysqli_errno($this->mysqli)); + exit(); + } + call_user_func_array(array( + $this->stmt, + 'bind_param' + ), $this->bind_names); + if (! (mysqli_stmt_execute($this->stmt))) { + $err = 'ERR:EXEC 2P' . mysqli_errno($this->mysqli) . ' ' . $this->stmt->error . mysqli_error($this->mysqli); + $errNo = mysqli_errno($this->mysqli); + $this->stmt->close(); + throw new Exception($err, $errNo); + exit(); + } + return true; + } + } + + function quickExecute() + { + if (! $this->execute()) { + if ($this->stmt) { + $this->stmt->close(); + } + throw new \Exception('ERR:EXEC 3P' . mysqli_errno($this->mysqli) . ' ' . mysqli_error($this->mysqli), mysqli_errno($this->mysqli)); + exit(); + } + $this->stmt->close(); + return true; + } + + function getInsertId() + { + return mysqli_insert_id($this->mysqli); + } + + function paramAdd($sqlParamName, $paramValue, $setNull = false) + { + if ($this->queryType == "insert") { + return $this->paramAddInsert($sqlParamName, $paramValue, $setNull); + } elseif ($this->queryType == "update") { + return $this->paramAddUpdate($sqlParamName, $paramValue, $setNull); + } + } + + function paramAddInsert($sqlParamName, $paramValue = null, $setNull = null) + { + $var = $paramValue; + + if (($var !== null) || $setNull) { + if ($setNull) { + $var = null; + } + if ($this->ctr != 0) { + $this->sqlQueryString .= ', '; + $this->sqlQueryStringL .= ', '; + } + $this->sqlQueryString .= "`$sqlParamName` "; + $this->sqlQueryStringL .= "? "; + $this->ctr ++; + if (! isset($this->bind_names)) { + $x = 'XX'; + $bind_name = 'bind' . $this->ctr; + $$bind_name = $x; + $this->bind_names[] = &$$bind_name; + } + $bind_name = 'bind' . $this->ctr; + $$bind_name = $var; + $this->bind_names[] = &$$bind_name; + $this->param_type .= 's'; + } + } + + function paramAddUpdate($sqlParamName, $paramValue = null, $setNull = null) + { + if ($setNull) { + $var = null; + } elseif (isset($paramValue)) { + $var = $paramValue; + } + + if ($var !== null || $setNull) { + if ($setNull) { + $var = null; + } + if ($this->ctr != 0) { + $this->sqlQueryString .= ', '; + } + $this->sqlQueryString .= "`$sqlParamName`=? "; + $this->ctr ++; + if (! isset($this->bind_names)) { + $x = 'XX'; + $bind_name = 'bind' . $this->ctr; + $$bind_name = $x; + $this->bind_names[] = &$$bind_name; + } + $bind_name = 'bind' . $this->ctr; + $$bind_name = $var; + $this->bind_names[] = &$$bind_name; + $this->param_type .= 's'; + } + } + + function paramClose($sqlQueryStringWhere = null) + { + $bind_name = 'bind'; + $$bind_name = $this->param_type; + $this->bind_names[0] = &$$bind_name; + + if ($this->queryType == "insert") { + $this->sqlQueryString = $this->sqlQueryString . ") VALUES (" . $this->sqlQueryStringL . ")"; + } elseif (isset($sqlQueryStringWhere)) { + $this->sqlQueryString .= " where " . $sqlQueryStringWhere; + } + $this->paramClosed = true; + } + + function export($tlist) + { + $retval = ""; + if (gettype($tlist) == "string") { + $tlist = array( + $tlist + ); + } elseif (gettype($tlist) != "array") { + throw new Exception("Incorrect type " . gettype($tlist) . " for tables. Expecting 'string' or 'array'"); + } + + foreach ($tlist as $table) { + $res = $this->quickExec1Line("SHOW CREATE TABLE `$table`"); + if (array_key_exists("Create Table", $res)) { + $t_create = $res["Create Table"]; + $retval .= "CREATE TABLE IF NOT EXISTS $table (zzz int);\n"; + $columns = preg_split("/[\n\r]/", $t_create); + + // Определяем столбцы + foreach ($columns as $col) { + $matches = []; + if (preg_match("/^[ ]*(`(.*)`\ [a-z\(0-9\)]*\ [A-Z _'0-9a-z]*)/", $col, $matches)) { + // var_dump($matches); + if (preg_match("/AUTO_INCREMENT/", $col)) { + $retval .= "ALTER TABLE `$table` ADD COLUMN IF NOT EXISTS " . $matches[1] . " PRIMARY KEY;\n"; + } else { + $retval .= "ALTER TABLE `$table` ADD COLUMN IF NOT EXISTS " . $matches[1] . ";\n"; + } + } + } + $retval .= "ALTER TABLE `$table` DROP COLUMN IF EXISTS zzz;\n"; + + // Определяем индексы + foreach ($columns as $col) { + $matches = []; + if (preg_match("/^[ ]*(([A-Z ]*)KEY\ `(.*)`\ \((.*)\))/", $col, $matches)) { + $retval .= "DROP INDEX IF EXISTS`" . $matches[3] . "` ON `$table`;\n"; + $retval .= "CREATE " . $matches[2] . "INDEX `" . $matches[3] . "` ON `$table` (" . $matches[4] . ");\n"; + } + } + } elseif (array_key_exists("Create View", $res)) { + $retval .= "DROP VIEW IF EXISTS `" . $table . "`;\n"; + $retval .= $res["Create View"] . ";\n"; + } + } + return $retval; + } + + public static function getMigration($folder, $namespace) + { + $res = ""; + foreach (scandir($folder) as $file) { + $r = []; + if (! preg_match("/(.*)\.php$/", $file, $r)) { + continue; + } + $mod = $namespace . "\\" . $r[1]; + if (! is_a($mod, dbStoredBase::class, true)) { + continue; + } + if (is_a($mod, noSqlMigration::class, true)) { + continue; + } + + $m = new $mod(); + if (! empty($m::$sqlTable)) { + $res .= "-- $mod\n"; + $res .= (static::getMigrationForClass($m)); + } + } + return $res; + } + + protected static function getMigrationForClass(dbStoredBase $type) + { + if (empty($type::$sqlTable)) { + throw new Exception("Empty SQLTable not allowed here"); + } + + $res = "CREATE TABLE IF NOT EXISTS `" . $type::$sqlTable . "` (zzz int);\n"; + // columns + foreach ($type->getSqlSchema() as $key => $val) { + $res .= "ALTER TABLE `" . $type::$sqlTable . "` ADD COLUMN IF NOT EXISTS `" . $key . "` " . $val["type"] . ((array_key_exists("nullable", $val) && $val["nullable"] == false) ? " NOT NULL" : ((array_key_exists("default", $val)) ? (" DEFAULT \"" . $val["default"] . "\"") : " DEFAULT NULL")) . ((array_key_exists("index", $val) && $val["index"] == "AI") ? " AUTO_INCREMENT PRIMARY KEY" : "") . ((array_key_exists("first", $val) && $val["first"] == true) ? " FIRST" : "") . ";\n"; + } + + $res .= "ALTER TABLE `" . $type::$sqlTable . "` DROP COLUMN IF EXISTS zzz;\n"; + foreach ($type->getSqlSchema() as $key => $val) { + if (empty($val["index"]) || $val["index"] == "AI") { + continue; + } + $res .= "DROP INDEX IF EXISTS `" . $key . "` ON `" . $type::$sqlTable . "`;\n"; + $res .= "CREATE " . (($val["index"] == "INDEX") ? "" : $val["index"] . " ") . "INDEX `" . $key . "` ON `" . $type::$sqlTable . "` (`" . $key . "`);\n"; + } + + return $res; + } + + function getAffectedRows() + { + return $this->mysqli->affected_rows; + } + + public static function doMigration(moduleInfo $module, $classFolder=null) { + + if (!$module->singleInstanceOnly) { + throw new \Exception("Migration of multi-instance modules not allowed"); + } + + if (empty($classFolder)) { + $classFolder=__DIR__."/../../modules/".$module->instanceOf."/".$module->instanceOf; + } + + $sql = new static(); + if ($sql->quickExec1Line("SHOW TABLES LIKE '__SqlSchemaVersion'")) { + $version = $sql->quickExec1Line("SELECT `version` from `__SqlSchemaVersion` where `module`='".$module->name."'"); + if ($version) { $version=$version["version"];} + } else { + $version=null; + } + + $migration=$sql::getMigration($classFolder, $module->namespace); + + if ($version != hash("md5",$migration)) { + + print "Module '".$module->name."' DB schema version mismatch $version != ".hash("md5",$migration)."\n"; + print "Updating Module '".$module->name."' DB schema..."; + $sql->quickExec("CREATE TABLE IF NOT EXISTS `__SqlSchemaVersion` (`module` VARCHAR(255), `version` VARCHAR(255))"); + foreach (explode("\n", $migration) as $line) { + if (!empty($line) && !preg_match("/^--/", $line)) { + $sql->quickExec($line); + } + } + + $sql->quickExec("DELETE FROM `__SqlSchemaVersion` where `module` = '".$module->name."'"); + $sql->quickExec("INSERT INTO `__SqlSchemaVersion` (`module`, `version`) VALUES ('".$module->name."', '".hash("md5",$migration)."')"); + print "OK\n"; + return true; + + } + } +} + +?> \ No newline at end of file diff --git a/core/fox/stringExportable.php b/core/fox/stringExportable.php new file mode 100644 index 0000000..ffbf764 --- /dev/null +++ b/core/fox/stringExportable.php @@ -0,0 +1,22 @@ + \ No newline at end of file diff --git a/core/fox/stringImportable.php b/core/fox/stringImportable.php new file mode 100644 index 0000000..9f8e960 --- /dev/null +++ b/core/fox/stringImportable.php @@ -0,0 +1,20 @@ + \ No newline at end of file diff --git a/core/fox/time.php b/core/fox/time.php new file mode 100644 index 0000000..8e5f6ee --- /dev/null +++ b/core/fox/time.php @@ -0,0 +1,105 @@ +format = $format; } + if ($time === null || $time == 0) { + $this->stamp = null; + } elseif (is_numeric($time)) { + $this->stamp = $time; + } elseif (gettype($time) == "string") { + $this->stamp = strtotime($time); + } elseif ($time instanceof time) { + $this->stamp = $time->stamp; + } else { + throw new Exception("Invalid input"); + } + } + + public function __fromString($val) + { + $this->__construct($val); + } + + public function __toString(): string + { + date_default_timezone_set('UTC'); + return date($this->format, $this->stamp); + } + + public function print($format=null, $TZ=null) { + if ($TZ !==null) { + date_default_timezone_set($TZ); + } else { + date_default_timezone_set('UTC'); + } + if (empty($format)) { $format=$this->format; } + return date($format, $this->stamp); + } + + public function isNull(): bool + { + return ($this->stamp == null); + } + + public function __get($key) { + switch ($key) { + case "hourStart": + return new static(strtotime(date("Y-m-d H:00:00", $this->stamp))); + case "dayStart": + return new static(strtotime(date("Y-m-d 00:00:00", $this->stamp))); + case "monthStart": + return new static(strtotime(date("Y-m-01 00:00:00", $this->stamp))); + case "yearStart": + return new static(strtotime(date("Y-01-01 00:00:00", $this->stamp))); + + default: + throw new \Exception("Invalid property ".$key." for read in class ".__CLASS__); + } + } + + public function addSec($sec) { + return new static($this->stamp+$sec); + } + + public static function current() : time { + return new static(time()); + } + public function jsonSerialize() + { + return $this->stamp; + } + +} + +?> \ No newline at end of file diff --git a/core/fox/user.php b/core/fox/user.php new file mode 100644 index 0000000..6ed7727 --- /dev/null +++ b/core/fox/user.php @@ -0,0 +1,358 @@ + 30 + ]; + + protected ?company $__company = null; + + + protected static $excludeProps = ["settings","authRefId"]; + + public static $sqlTable = "tblUsers"; + + public static $deletedFieldName = "deleted"; + + public static $sqlColumns = [ + "login" => [ + "type" => "VARCHAR(255)", + "index" => "UNIQUE", + "nullable" => false, + "search"=>"like" + ], + "secret" => [ + "type" => "VARCHAR(255)", + "index" => "INDEX", + "nullable" => true + ], + "invCode" => [ + "type" => "INT", + "index" => "UNIQUE", + "nullable" => false, + "search"=>"invCode" + ], + "authType" => [ + "type" => "VARCHAR(255)", + "index" => "INDEX", + "nullable" => false + ], + "authRefId" => [ + "type" => "TEXT" + ], + "fullName" => [ + "type" => "VARCHAR(255)", + "nullable" => false, + "search"=>"like" + ], + "companyId" => [ + "type" => "INT", + "index" => "INDEX" + ], + "eMail" => [ + "type" => "VARCHAR(255)", + "index" => "INDEX", + "search"=>"like" + ] + ]; + + public function setPassword($passwd) + { + $this->__secret = xcrypt::hash($passwd); + } + + public function checkPassword($passwd) + { + if (empty($this->__secret)) { + return false; + } + return ($this->__secret == xcrypt::hash($passwd)); + } + + protected function validateSave() + { + if ($this->invCode->isNull()) { + $this->invCode->issue("core", get_class($this)); + } + + if (empty($this->login)) { + $this->login=(string)$this->invCode; + } + return true; + } + + public function __xConstruct() + { + $this->invCode = new UID(); + } + + public function getAccessRules() + { + $cache = new cache(); + $ACLS = $cache->get("UserACL." . $this->id, true); + if ($ACLS !== null) { + return $ACLS; + } + + $rv = []; + // merge all ACLs + foreach (userGroup::getForUser($this, false, $this->sql) as $val) { + $rv = array_merge_recursive($rv, $val->accessRules); + } + + // deduplicate + foreach ($rv as &$val) { + $val = array_unique($val); + } + $cache->set("UserACL." . $this->id, $rv); + return $rv; + } + + public function checkAccess(string $rule, string $modInstance) + { + $ACLS = $this->getAccessRules(); + + if ($rule=="allUsers") { + return true; + } + + + if (array_key_exists($modInstance, $ACLS) && (array_search($rule, $ACLS[$modInstance]) !== false)) { + return true; + } + + if (array_key_exists("", $ACLS) && (array_search($rule, $ACLS[""]) !== false)) { + return true; + } + + if (array_key_exists("", $ACLS) && (array_search("isRoot", $ACLS[""]) !== false)) { + return true; + } + + return false; + } + + public function flushACRCache() + { + $cache = new cache(); + $cache->set("UserACL." . $this->id, null); + } + + public function __get($key) + { + switch ($key) { + case "active": + return $this->active && ! $this->deleted; + + default: + return parent::__get($key); + } + } + + public static function getByRefID($authMethod,$userRefId) { + $ref=new static(); + $sql = $ref->getSql(); + $res = $sql->quickExec1Line($ref->sqlSelectTemplate. " where `authType`='".$authMethod."' and `authRefId`='".common::clearInput($userRefId)."'"); + if ($res) { + return new static($res); + } else { + return null; + } + } + + public static function getByEmail($eMail) { + $ref=new static(); + $sql = $ref->getSql(); + $res = $sql->quickExec1Line($ref->sqlSelectTemplate. " where `eMail`='".common::clearInput($eMail,"@0-9A-Za-z._-")."'"); + if ($res) { + return new static($res); + } else { + return null; + } + } + + public static function getByLogin($login) { + $ref=new static(); + $sql = $ref->getSql(); + $res = $sql->quickExec1Line($ref->sqlSelectTemplate. " where `login`='".common::clearInput($login)."'"); + if ($res) { + return new static($res); + } else { + return null; + } + } + + protected function prepareCodeForConfirmation($operation) { + $confCode=new confirmCode(); + $confCode->class=__CLASS__; + $confCode->instance="core"; + $confCode->reference=$this->id; + $confCode->operation=$operation; + $confCode->payload=["eMailAddress"=>$this->eMail]; + return $confCode; + } + + public function sendEMailConfirmation() { + if ($this->eMailConfirmed) { + foxException::throw("WARN", "Already confirmed", 406,"ARCF"); + } + + if (!common::validateEMail($this->eMail)) { + foxException::throw("ERR", "Invalid address format", 406,"WREML"); + } + + $confCode=$this->prepareCodeForConfirmation("eMailConfirmation"); + $confCode->save(); + + $m=new mailMessage(); + $m->addRecipient($this->eMail); + $m->subject=langPack::getAndReplace("core.eMailConfirmMessageTitle"); + $m->bodyHTML=langPack::getAndReplace("core.eMailConfirmMessage",["confCodePrint"=>$confCode->code]); + $m->send(); + } + + public function validateEMailConfirmation($code) { + $confCode=$this->prepareCodeForConfirmation("eMailConfirmation"); + if (!$confCode->fillByHash()) { + foxException::throw("ERR","Invalid code",406,"IVCC"); + } + if ($confCode->code != $code) { + return false; + } + + if ($confCode->expireStamp->stampeMailConfirmed=true; + $this->save(); + $confCode->delete(); + return true; + } + + public function sendPasswordRecovery() { + if ($this->authType!=='internal' || !$this->eMailConfirmed) { throw new foxException("Not acceptable",406);} + $confCode=$this->prepareCodeForConfirmation("passwordRecovery"); + $confCode->save(); + + $m=new mailMessage(); + $m->addRecipient($this); + $m->subject=langPack::getAndReplace("core.accessRecoverMessageTitle"); + $m->bodyHTML=langPack::getAndReplace("core.accessRecoverMessage",["confCodePrint"=>$confCode->code, "eMailEncoded"=>$this->eMail]); + $m->send(); + } + + public function validateRecoveryCode($code,$delete=false) { + if ($this->authType!=='internal' || !$this->eMailConfirmed) { throw new foxException("Not acceptable",406);} + $confCode=$this->prepareCodeForConfirmation("passwordRecovery"); + $confCode->fillByHash(); + if ($confCode->code != $code) { + return false; + } + + if ($confCode->expireStamp->stampdelete();} + return true; + } + + public static function API_GET_list(request $request) + { + if (! $request->user->checkAccess("adminUsers", "core")) { + throw new foxException("Forbidden", 403); + } + return static::search(); + } + + public static function API_POST_search(request $request) { + if (! $request->user->checkAccess("adminUsers", "core")) { + throw new foxException("Forbidden", 403); + } + $pageSize=common::clearInput($request->requestBody->pageSize,"0-9"); + if (empty($pageSize)) { $pageSize=$request->user->settings["pageSize"];} + + return static::search(common::clearInput($request->requestBody->pattern), $pageSize); + + } + + public static function APIX_GET_sendEMailConfirmation(request $request) { + if (! $request->user->checkAccess("adminUsers", "core")) { + throw new foxException("Forbidden", 403); + } + $user = new static(common::clearInput($request->function,"0-9")); + $user->sendEMailConfirmation(); + } + + public static function API_GET_sendEMailConfirmation(request $request) { + $request->user->sendEMailConfirmation(); + } + + public static function API_POST_validateEMailCode(request $request) { + if ($request->user->validateEMailConfirmation(common::clearInput($request->requestBody->code,"0-9"))) { + return; + } else { + foxException::throw("ERR", "Validation failed", 400); + } + } + + public static function APICall(request $request) { + if (!empty($request->function)) { + throw new foxException("Method not allowed",405); + } + + switch ($request->method) { + + default: + throw new foxException("Method not allowed",405); + } + + } + + +} + +?> \ No newline at end of file diff --git a/core/fox/userGroup.php b/core/fox/userGroup.php new file mode 100644 index 0000000..517a543 --- /dev/null +++ b/core/fox/userGroup.php @@ -0,0 +1,254 @@ +][ruleName] + # ex: + # ["core"]["isRoot","ACLx"] + # []["isRoot"] + # + protected array $accessRules = []; + + public static $sqlTable = "tblUserGroups"; + public static $allowDeleteFromDB = true; + + public static $sqlColumns = [ + "name" => [ + "type" => "VARCHAR(255)", + "index" => "INDEX" + ], + "companyId" => [ + "type" => "INT", + "index" => "INDEX" + ] + ]; + + public function getMembers() + { + $this->checkSql(); + $rv = []; + foreach (userGroupMembership::getUsersInGroup($this, $this->sql) as $ugm) { + $rv[$ugm->user->id] = $ugm->user; + } + return $rv; + } + + public static function getForUser(user $user, $isList = false, ?sql $sql = null) + { + if (empty($sql)) { + $sql = new sql(); + } + $ugms = userGroupMembership::getGroupsForUser($user, $sql); + + $rv = []; + + foreach ($ugms as $ugm) { + if ($isList === null || ($ugm->group->isList === $isList)) { + $rv[] = $ugm->group; + } + } + + return $rv; + } + + /* + * options: + * isListOnly - { only - search isList only, no - search with isList = 0, both - search any}, default=no + * accessRule - передается в формате access_rule_name@module. + * !!! Внимание !!! в отличие от проверки прав доступа - здесь поиск по isRoot не осуществляется! + * Если module отсутствует - то считается, что module ='all' + * + * + * + */ + protected static function xSearch($where, $pattern, ?array $options, sql $sql) { + $accessRule=(empty($options["accessRule"])?null:$options["accessRule"]); + $isList=(array_key_exists("isList", $options)?$options["isList"]:false); + $ruleJoin=null; + $ruleWhere=null; + + if ($isList !== false) { + $ruleWhere .= " and `i`.`isList` = " . ($isList == true ? 1 : 0); + } + + if (empty($ruleWhere)) { + $xWhere=$where; + } else { + $xWhere=(empty($where)?$ruleWhere:"(".$where.") AND ".$ruleWhere); + } + + return ["where"=>$xWhere, "join"=>$ruleJoin]; + } + + public function join(user $user) + { + if (empty($this->id)) { + throw new \Exception("Group not saved! Save it first."); + } + if (array_key_exists($user->id, $this->getMembers())) { + return true; + } + + $umm = new userGroupMembership(); + $umm->userId = $user->id; + $umm->groupId = $this->id; + $umm->save(); + $user->flushACRCache(); + return true; + } + + public function left(user $user) + { + if (empty($this->id)) { + throw new \Exception("Group not saved! Save it first."); + } + foreach (userGroupMembership::getUsersInGroup($this, $this->sql) as $ugm) { + if ($ugm->user->id == $user->id) { + $ugm->delete(); + } + } + $user->flushACRCache(); + return true; + } + + public function addAccessRule(string $rule, string $modInstance = "") + { + if (array_key_exists($modInstance, $this->accessRules) && (array_search($rule, $this->accessRules[$modInstance]) !== false)) { + return true; + } + + $this->accessRules[$modInstance][] = $rule; + return true; + } + + + public function dropAccessRule(string $rule, string $modInstance = "") + { + if (array_key_exists($modInstance, $this->accessRules) && (($rid = array_search($rule, $this->accessRules[$modInstance])) !== false)) { + unset($this->accessRules[$modInstance][$rid]); + if (empty($this->accessRules[$modInstance])) { + unset($this->accessRules[$modInstance]); + } + ; + } + } + + public static function API_GET_list(request $request) + { + if (! $request->user->checkAccess("adminUserGroups", "core")) { + throw new foxException("Forbidden", 403); + } + return static::search(); + } + + public static function API_POST_members(request $request) + { + if (! $request->user->checkAccess("adminUserGroups", "core")) { + throw new foxException("Forbidden", 403); + } + return static::getMembers(); + } + + public static function API_DELETE_acl(request $request) + { + if (! $request->user->checkAccess("adminUserGroups", "core")) { + throw new foxException("Forbidden", 403); + } + $group = new static(common::clearInput($request->requestBody->groupId,"0-9")); + $group->dropAccessRule(common::clearInput($request->requestBody->rule), common::clearInput($request->requestBody->module)); + $group->save(); + + foxRequestResult::throw(200, "Deleted"); + + } + + public static function API_PUT_acl(request $request) + { + if (! $request->user->checkAccess("adminUserGroups", "core")) { + throw new foxException("Forbidden", 403); + } + $group = new static(common::clearInput($request->requestBody->groupId,"0-9")); + $group->addAccessRule(common::clearInput($request->requestBody->rule), ($request->requestBody->forAll=="1")?"":common::clearInput($request->requestBody->module)); + $group->save(); + + } + + + public static function APICall(request $request) { + + if (! $request->user->checkAccess("adminUserGroups", "core")) { + throw new foxException("Forbidden", 403); + } + + switch ($request->method) { + case "GET": + if (empty($request->parameters[0])) { + return new static(common::clearInput($request->function)); + } else { + switch ($request->parameters[0]) { + case "acls": + return (new static(common::clearInput($request->function)))->accessRules; + break; + default: + throw new foxException("Method not allowed",405); + } + } + break; + + case "PUT": + $grName=common::clearInput($request->requestBody->name); + $groups = userGroup::search($grName); + foreach ($groups as $group) { + if (trim(strtolower($group->name))==trim(strtolower($grName))) { + foxException::throw("ERR", "Already exists", 409, "GAX"); + } + } + + $group=new userGroup(); + $group->name=$grName; + $group->isList=$request->requestBody->isList==1; + $group->save(); + foxRequestResult::throw(201, "Created",$group); + break; + case "DELETE": + $group = new static(common::clearInput($request->function)); + if ($group->getMembers()) { + foxException::throw("ERR","Group not empty", 400,"NEG"); + } + + $group->delete(); + throw new foxRequestResult("Deleted",200); + break; + default: + throw new foxException("Method not allowed",405); + } + + } +} + +?> \ No newline at end of file diff --git a/core/fox/userGroupMembership.php b/core/fox/userGroupMembership.php new file mode 100644 index 0000000..b0790ea --- /dev/null +++ b/core/fox/userGroupMembership.php @@ -0,0 +1,232 @@ + [ + "type" => "INT", + "index" => "INDEX", + "nullable" => false + ], + "userId" => [ + "type" => "INT", + "index" => "INDEX", + "nullable" => false + ] + ]; + + public static function getUsersInGroup(?userGroup $group = null, ?sql $sql = null) + { + if (empty($sql)) { + $sql = new sql(); + } + + $res = $sql->quickExec("select * from `" . self::$sqlTable . "`" . ($group === null ? "" : " where `groupId` = '" . $group->id . "'")); + $rv = []; + while ($row = mysqli_fetch_assoc($res)) { + $item = new self($row); + if ($item->user !== null) { + $rv[] = $item; + } + } + + return $rv; + } + + public function loadUser() + { + if (empty($this->__user) && ! empty($this->userId)) { + try { + $this->__user = new user($this->userId); + } catch (\Exception $e) { + return null; + } + } + return $this->__user; + } + + public function loadGroup() + { + if (empty($this->__group) && ! empty($this->groupId)) { + $this->__group = new userGroup($this->groupId); + } + return $this->__group; + } + + public static function getGroupsForUser(?user $user = null, ?sql $sql = null) + { + // if user===null -> get all items, else - get only items for user + if (empty($sql)) { + $sql = new sql(); + } + + $res = $sql->quickExec("select * from `" . self::$sqlTable . "`" . ($user === null ? "" : " where `userId` = '" . $user->id . "'")); + $rv = []; + while ($row = mysqli_fetch_assoc($res)) { + $item = new self($row); + if ($item->group !== null) { + $rv[] = $item; + } + } + + return $rv; + } + + + public function __get($key) + { + switch ($key) { + case "user": + return $this->loadUser(); + + case "group": + return $this->loadGroup(); + + default: + return parent::__get($key); + } + } + + public function export() + { + $rv = parent::export(); + if (! empty($this->__user)) { + $rv["user"] = $this->__user; + } + if (! empty($this->__group)) { + $rv["group"] = $this->__group; + } + return $rv; + } + + public static function API_POST_search(request $request) + { + if (! $request->user->checkAccess("adminUserGroups", "core")) { + throw new foxException("Forbidden", 403); + } + + @$page=common::clearInput($request->requestBody->page); + if (empty($page) || !(is_numeric($page))) {$page=0;} + + @$pageSize=common::clearInput($request->requestBody->pageSize); + if (empty($pageSize) || !(is_numeric($pageSize))) {$pageSize=$request->user->settings["pageSize"];} + + if (!empty($request->requestBody->userId)) { + $user = new user($userId=common::clearInput($request->requestBody->userId,"0-9")); + } else { + $user=null; + $userId=null; + } + + if (!empty($request->requestBody->groupId)) { + $group=new userGroup($groupId=common::clearInput($request->requestBody->groupId,"0-9")); + } else { + $group=null; + $groupId=null; + } + + if (!$user && !$group) { + throw new foxException("Invalid request",400); + } + + $res=static::search(null,$pageSize,$page,[ + "user"=>$user, + "group"=>$group, + ]); + + $rv=[]; + foreach ($res as $ugm) { + $rv[]=[ + "ugmId"=>$ugm->id, + "user"=>($userId==$ugm->userId)?$user:$ugm->user, + "group"=>($groupId=$ugm->groupId)?$group:$ugm->group, + ]; + } + + return $rv; + } + + protected static function xSearch($where, $pattern, ?array $options, sql $sql) { + + if (!empty($options["user"])) { + $xWhere="`i`.`userId`='".$options["user"]->id."'"; + } + + if (!empty($options["group"])) { + $xWhere.=(empty($xWhere)?"":" AND ")."`i`.`groupId`='".$options["group"]->id."'"; + } + + return ["where"=>empty($where)?$xWhere:"(".$where.") AND ".$xWhere, "join"=>null]; + } + + public static function APICall(request $request) { + if (! $request->user->checkAccess("adminUserGroups", "core")) { + throw new foxException("Forbidden", 403); + } + + $user=new user(common::clearInput($request->requestBody->userId,"0-9")); + $userGroup=new userGroup(common::clearInput($request->requestBody->groupId,"0-9")); + $userGroupMembership=null; + + foreach (static::getGroupsForUser($user) as $ugm) { + if ($ugm->groupId==$userGroup->id) { + $userGroupMembership=$ugm; + break; + } + } + + switch ($request->method) { + case "PUT": + if ($userGroupMembership!==null) { + foxException::throw("ERR", "Already exists", 409, "UGX"); + } + + $userGroupMembership=new userGroupMembership(); + $userGroupMembership->userId=$user->id; + $userGroupMembership->groupId=$userGroup->id; + $userGroupMembership->save(); + $user->flushACRCache(); + foxRequestResult::throw("201", "Created"); + break; + + case "DELETE": + if ($userGroupMembership==null) { + foxException::throw("ERR", "Not found", 404, "UGN"); + } + $userGroupMembership->delete(); + $user->flushACRCache(); + break; + + default: + throw new foxException("Method not allowed",405); + break; + } + } +} +?> \ No newline at end of file diff --git a/core/fox/userInvitation.php b/core/fox/userInvitation.php new file mode 100644 index 0000000..c50f5ab --- /dev/null +++ b/core/fox/userInvitation.php @@ -0,0 +1,141 @@ + [ + "type" => "CHAR(16)", + "nullable" => false, + "index"=>"UNIQUE" + ], + "eMail" => [ + "type" => "VARCHAR(255)", + "nullable" => true + ], + "expireStamp"=>[ + "type"=>"DATETIME" + ] + ]; + + protected function __xConstruct() { + $this->expireStamp=new time(); + } + + protected function validateSave() { + if (empty($this->regCode)) { + while (true) { + $nc=(common::genPasswd(16,[0,1,2,3,4,5,6,7,8,9])); + if($nc[0] !=0) { + break; + } + } + $this->regCode=$nc; + } + return true; + } + + public function getCodePrint() { + return substr($this->regCode, 0,4)."-".substr($this->regCode, 4,4)."-".substr($this->regCode, 8,4)."-".substr($this->regCode, 12,4); + } + + public function sendEmail() { + if (common::validateEMail($this->eMail)) { + $m=new mailMessage(); + $m->addRecipient($this->eMail); + $m->subject=langPack::getAndReplace("core.eMailInviteMessageTitle"); + $m->bodyHTML=langPack::getAndReplace("core.eMailInviteMessage",["regCodePrint"=>$this->getCodePrint()]); + $m->send(); + } + } + + public static function getByCode($code) { + $code = common::clearInput($code,"0-9"); + if (strlen($code) != 16) { + return false; + } + + $ref=new static(); + $sql = $ref->getSql(); + $res=$sql->quickExec1Line($ref->sqlSelectTemplate." where `regCode`='".$code."'"); + if ($res) { + return new static($res); + } else { + return false; + } + } + + public static function getByEMail($eMail) { + $eMail = common::clearInput($eMail,"@0-9A-Za-z._-"); + if (!common::validateEMail($eMail)) { + return false; + } + + $ref=new static(); + $sql = $ref->getSql(); + $res=$sql->quickExec1Line($ref->sqlSelectTemplate." where `eMail`='".$eMail."'"); + if ($res) { + return new static($res); + } else { + return false; + } + } + + public static function API_PUT(request $request) { + if (! $request->user->checkAccess("adminUsers", "core")) { + throw new foxException("Forbidden", 403); + } + $eMail=common::clearInput($request->requestBody->eMail,"0-9A-Za-z@_.-"); + + if ($inv=static::getByEMail($eMail)) { + return $inv; + } elseif (user::getByEmail($eMail)) { + foxException::throw("ERR", "User already registered", 409,'UAX'); + } + + $inv = new static(); + $inv->eMail=$eMail; + $inv->expireStamp=new time($request->requestBody->expireStamp); + $inv->allowMultiUse=$request->requestBody->allowMultiUse===true || $request->requestBody->allowMultiUse=="true"; + if (!empty($inv->eMail) && !common::validateEMail($inv->eMail)) { foxException::throw("WARN", "Invalid eMail format", 400,"WREML"); } + $inv->save(); + try { + $inv->sendEmail(); + } catch (\Exception $e) { + trigger_error($e->getMessage()); + } + + return $inv; + + } + + public static function API_GET_list(request $request) { + return static::search(); + } + + public static function APIX_GET_reSend(request $request) { + if (! $request->user->checkAccess("adminUsers", "core")) { + throw new foxException("Forbidden", 403); + } + $inv=new static(common::clearInput($request->function)); + $inv->sendEmail(); + } + + public static function API_DELETE(request $request) { + if (! $request->user->checkAccess("adminUsers", "core")) { + throw new foxException("Forbidden", 403); + } + $inv=new static(common::clearInput($request->function)); + $inv->delete(); + } +} + +?> \ No newline at end of file diff --git a/core/fox/xcrypt.php b/core/fox/xcrypt.php new file mode 100644 index 0000000..976561a --- /dev/null +++ b/core/fox/xcrypt.php @@ -0,0 +1,66 @@ +> /var/log/fox/fox-cron.log' + diff --git a/docker-build/rootfs/etc/fox-start.d/100_fox-init.sh b/docker-build/rootfs/etc/fox-start.d/100_fox-init.sh new file mode 100755 index 0000000..a7a63df --- /dev/null +++ b/docker-build/rootfs/etc/fox-start.d/100_fox-init.sh @@ -0,0 +1,28 @@ +#!/bin/bash +XPREFIX=/var/www/html + +if [ ! -e /var/log/fox ] +then + mkdir /var/log/fox +else + echo exists +fi + +chmod a+rw /var/log/fox + + +cd ${XPREFIX}/fox-start.d +echo "Fox init started." +echo "Initialize core." +find * -maxdepth 1 -type f -exec bash -c "echo Run {} && ./{}" \; + +cd ${XPREFIX}/modules +mods=`find * -maxdepth 1 -type d -name fox-start.d | sort` + +for mod in ${mods} +do + echo Module `dirname ${mod}` + cd ${mod} + find * -maxdepth 1 -type f -exec bash -c "echo Run {} && ./{}" \; + cd - > /dev/null +done \ No newline at end of file diff --git a/error.php b/error.php new file mode 100644 index 0000000..a534a8e --- /dev/null +++ b/error.php @@ -0,0 +1,80 @@ +"Bad request", + "401"=>"Unauthorized", + "403"=>"Forbidden", + "404"=>"Not found", + "405"=>"Method Not Allowed", + "500"=>"Internal server error", + "501"=>"Not Implemented", + +]; + +$jsons=[ + "api" +]; + + +$code=$_GET["code"]; + + +if (array_key_exists("FOX_REWRITE", $_SERVER) && $_SERVER["FOX_REWRITE"] != "yes") +{ + $prefix = ($_SERVER["CONTEXT_PREFIX"]."index.php/"); +} else { + $prefix = ($_SERVER["CONTEXT_PREFIX"]); +} + +$prefix = preg_replace(["![/]+!","![\.]+!"], ["\/","\."], $prefix); +$req=(preg_replace("/".$prefix."/", '', $_SERVER["REQUEST_URI"])); +$req = explode("/",explode("?", $req, 2)[0]); + +if (array_search($req[1], $jsons)!==false) { + print '{"error":{"code":'.$code.',"message":"'.(array_key_exists($code, $codes)?$codes[$code]:"Server error").'"}}'; + exit; +} + +?> + + + + + +
+ +
+ +
+"; +if (array_key_exists($code, $codes)) { + print $codes[$code]; +} +?>
+ + + \ No newline at end of file diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..8e6560b Binary files /dev/null and b/favicon.ico differ diff --git a/fox-cron.d/cron.php b/fox-cron.d/cron.php new file mode 100755 index 0000000..ac4feaf --- /dev/null +++ b/fox-cron.d/cron.php @@ -0,0 +1,147 @@ +#!/usr/bin/php +getRunningTasks() as $task) { + + if ($task["expireStamp"] < time()) { + print "Task #".$task["pid"]." for ".$task["method"]." (".substr($task["hash"],0,8).") expired. Kill it.\n"; + if (posix_kill($task["pid"], SIGKILL) ===false) { + $db->delTask($task["pid"]); + } + } +} + +foreach ($modules as $mod) { + if (array_search("cron",$mod->features)!==false) { + $modClass=$mod->name."\module"; + if (property_exists($modClass, "crontab")) { + foreach($modClass::$crontab as $cronItem) { + + $conditionMatch=false; + if (!empty($cronItem["period"])) { + $conditionMatch=(fmod($minutes, $cronItem["period"])==0); + } elseif (!empty($cronItem["regexp"])) { + if (!empty($cronItem["useLocalTZ"]) && $cronItem["useLocalTZ"]==true) { + $conditionMatch= (preg_match("/".$regexp."/", $lcdate)); + } else { + $conditionMatch= (preg_match("/".$regexp."/", $gmdate)); + } + + } + + if ($conditionMatch) { + if (!empty($cronItem["TTL"])) { + $ttl=$cronItem["TTL"]; + } else if (!empty($cronItem["period"])) { + $ttl=$cronItem["period"]*30; + } else { + $ttl=3600; + } + + $cti=[ + "instance"=>$mod->name, + "callback"=>$mod->namespace."\\".$cronItem["method"], + "args"=>array_key_exists("args", $cronItem)?$cronItem["args"]:null, + "TTL"=>$ttl, + ]; + + $cti["hash"]=md5(json_encode($cti)); + + $skipTask=false; + if (empty($cronItem["single"]) || $cronItem["single"]==true) { + foreach ($runningTasks as $task) { + if ($task["hash"]==$cti["hash"]) { + $skipTask=true; + } + } + } + + if (!$skipTask) { + $crontab[]=$cti; + } + + } + } + } + } +} + + +foreach ($crontab as $cronItem) { + $pid=pcntl_fork(); + if ($pid == -1) { + die('could not fork'); + } else if ($pid) { + print("Process #".$pid." started for callback ".$cronItem["callback"]." (".substr($cronItem["hash"],0,8).") with TTL ".$cronItem["TTL"]."\n"); + + $pids[$pid]=$cronItem["TTL"]; + $db->addTask($pid, $cronItem["hash"], $cronItem["callback"],$cronItem["TTL"]); + + } else { + $fork=true; + sql::flushConnections(); + call_user_func($cronItem["callback"],$cronItem["instance"], $cronItem["args"]); + exit; + break; + } + +} + +if ($fork) { exit; } + +$ttlc=0; + +while(($xpid=pcntl_wait($status,WNOHANG)) >=0) { + if ($xpid >0) { + unset($pids[$xpid]); + print "Process #".$xpid." Completed\n"; + $db->delTask($xpid); + } + + foreach($pids as $pid=>&$ttl) { + if ($ttl < $ttlc) { + print "Kill PID ".$pid." by TTL\n"; + var_dump(posix_kill($pid, SIGKILL)); + $ttl+=5; + } + } + sleep(1); + $ttlc++; + +} + + +?> \ No newline at end of file diff --git a/fox-start.d/.htaccess b/fox-start.d/.htaccess new file mode 100644 index 0000000..93169e4 --- /dev/null +++ b/fox-start.d/.htaccess @@ -0,0 +1,2 @@ +Order deny,allow +Deny from all diff --git a/fox-start.d/10_migration.sh b/fox-start.d/10_migration.sh new file mode 100755 index 0000000..3fc4474 --- /dev/null +++ b/fox-start.d/10_migration.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +php /var/www/html/cli/migration.php +php /var/www/html/cli/initialize.php diff --git a/index.php b/index.php new file mode 100644 index 0000000..c0d7cf0 --- /dev/null +++ b/index.php @@ -0,0 +1,16 @@ + + + +Chimera Fox + + + + + + + + + + + + diff --git a/modules/README.md b/modules/README.md new file mode 100644 index 0000000..3ad0c61 --- /dev/null +++ b/modules/README.md @@ -0,0 +1,4 @@ +# Modules Folder +This folder must be empty and don't used for any data except modules subfolders. +Modules must compliant FoxAPImk2. +Modules may be added as mapped-folder for docker containers or prebuilt in it. \ No newline at end of file diff --git a/static/.htaccess b/static/.htaccess new file mode 100644 index 0000000..65fd03a --- /dev/null +++ b/static/.htaccess @@ -0,0 +1,12 @@ + +RewriteEngine On + +RewriteCond %{REQUEST_FILENAME} -f +RewriteRule . - [S=5] + +RewriteRule "^theme/([^/]+)/main.css" "/modules/$1/theme/main.css" [L] +RewriteRule "^theme/([^/]+)/img/([0-9a-zA-Z\.\-\_]*)" "/modules/$1/theme/img/$2" [L] +RewriteRule "^theme/([^/]+)/font/([0-9a-zA-Z\.\-\_]*)" "/modules/$1/theme/font/$2" [L] +RewriteRule "^theme/([^/]+)/css/([0-9a-zA-Z\.\-\_]*\.css)" "/modules/$1/theme/css/$2" [L] + +RewriteRule "^js/([^/]+)/([0-9a-zA-Z\.\-\_]*\.js)" "/modules/$1/js/$2" [L] diff --git a/static/README.md b/static/README.md new file mode 100644 index 0000000..eec75b3 --- /dev/null +++ b/static/README.md @@ -0,0 +1,18 @@ +# Static data +if static files exists here - we well get it directly. eg - js or themes. +If not - we will try search ``(css|js|img)`` folder in module ``(module)`` in path + ``/static/(type)/(module)/(filename)`` + Module struture for static data is: + + `` ./css/*`` - for CSS + + `` ./img/*`` - for images + + `` ./js/*`` - for JavaScripts + + `` ./theme/*`` - themes if module provide it + + # Themes + Theme always contains main.css in root + + Ant it may contain folders ``css``, ``font`` and ``img`` in folder and in module`s ``theme`` folder \ No newline at end of file diff --git a/static/js/core/api.js b/static/js/core/api.js new file mode 100644 index 0000000..43db857 --- /dev/null +++ b/static/js/core/api.js @@ -0,0 +1,272 @@ +import * as UI from './ui.js'; +import { langPack } from './langpack.js'; + +// errDict +export function exec(requestType, method , data, onSuccess,noblank,onError,version) +{ + + if (typeof(requestType)=='object') { + var ref=requestType; + requestType=ref.requestType; + method=ref.method; + data=ref.data; + onSuccess=ref.onSuccess; + noblank=ref.noblank; + onError=ref.onError; + version=ref.version; + var xoss=true; + } else { + var xoss=false; + var ref={}; + } + if (noblank==undefined || noblank==false) { UI.blankerShow(); } + if (requestType==undefined) { requestType="GET" } + if (requestType=="GET") { data=undefined; } + if (version==undefined) { version=2; } + if (!isset(method)) { + throw("Empty method not allowed"); + } + + let headers={}; + let token=localStorage.getItem("token"); + if (token!==null) { + headers.Authorization="Token "+token; + } + + var transationStamp=(new Date()).getTime(); + + return $.ajax({ + url: "/api/v"+String(version)+"/"+method, + data: JSON.stringify(data), + type: requestType, + headers: headers, + + complete: function(data,textStatus,request) { + if (data.getResponseHeader('X-Fox-Token-Renew') !=null) { + session.updateToken(data.getResponseHeader('X-Fox-Token-Renew')); + } + + let rcode=data.status; + let rtext=data.statusText; + let rdata=data.responseText; + try { + var jdata=JSON.parse(rdata); + } catch(err) { + console.log("Unable to parce reply",rdata); + rcode=599; + rtext="Reply parce failure"; + jdata=undefined; + } + + var rv={status: {success: (rcode>=200 && rcode <300), code: rcode, message: rtext} , data: jdata}; + if (rv.status.success) { + UI.blankerHide(); + + let ossrv=undefined; + if (typeof(onSuccess) == 'function') + { + ossrv=onSuccess(rv, rtext); + } + + if (xoss && (ossrv!==false)) { + UI.showInfoDialog(langPack.core.iface.ok0); + } + + + } else { + UI.blankerHide(); + if (typeof(onError) == 'function') + { + onError(rv, rtext); + } else { + if (isset(jdata) && isset(jdata.error) && isset(jdata.error.xCode)) { + var xCode=jdata.error.xCode; + if (isset(ref.errDict) && isset(ref.errDict[xCode])) { + rtext=ref.errDict[jdata.error.xCode]; + } else { + rtext=xCode+" "+rtext; + } + } else { + var xCode=rcode; + rtext=xCode+" "+rtext; + } + + UI.showInfoDialog({message: rtext, title: langPack.core.iface.err0,dialogName: "apiExecStatus"+transationStamp, closeCallback: ref.onFinal}); + } + } + }, + dataType: "html" + }); +} + +export class settings { + static async load() { + if (sessionStorage.getItem("baseSettings")==undefined) { + await exec("GET","meta/settings",{},function(json) { + sessionStorage.setItem("baseSettings",JSON.stringify(json.data)); + },true) + } + } + + + static get(key) { + let settings={}; + let userSettings={}; + if (sessionStorage.getItem("baseSettings")!=undefined) { + settings=JSON.parse(sessionStorage.getItem("baseSettings")); + } + + if (sessionStorage.getItem("userSettings")!=undefined) { + settings=JSON.parse(sessionStorage.getItem("userSettings")); + } + + if (userSettings["key"] != undefined) { + return userSettings[key] + } else { + return settings[key] + } + + + } + +} + +export class auth { + static async login(login, password, callback, meta) { + if (login==undefined || password==undefined) { + throw "Empty credentials not allowed here"; + } + + let payload={}; + if (meta!=undefined) { + payload=meta; + } + + payload.login=login; + payload.password=password; + payload.type="WEB"; + + API.exec("POST", "auth/login",payload, function onSuccess(json) { + session.open(json.data) + if (typeof(callback) == 'function') + { + callback(json.status.code); + } + },false,function(json) { + if (typeof(callback) == 'function') + { + callback(json.status.code); + } + }); + + } + +} + +export class session { + static close() { + localStorage.removeItem("token"); + localStorage.removeItem("tokenExpire"); + sessionStorage.removeItem("session"); + } + + static open(data) { + + localStorage.setItem("token",data.token); + localStorage.setItem("tokenExpire",data.expire.stamp); + sessionStorage.setItem("session",data.session); + + } + + static updateToken(token) { + localStorage.setItem("token",token); + } + + static get(key) { + if (sessionStorage.getItem("session")==undefined) { + return undefined; + } + return JSON.parse(sessionStorage.getItem("session"))[key]; + } + + static getMenu() { + if (session.get("menu") == undefined) { + let menu={}; + $.each(session.get("modules"),function(mkey,mod) { + $.each(mod.menu,function(nkey,nmenu) { + nmenu.xmodkey=mod.name; + menu[mod.name+"_"+nkey]=nmenu; + }); + }); + return menu; + } else { + return session.get("menu"); + } + } + + static getModInstances() { + if (session.get("modInstances") == undefined) { + let instances={}; + $.each(session.get("modules"),function(key,val) { + instances[val.name]=val.instanceOf; + }); + session.set("modInstances",instances); + return instances; + } else { + return session.get("modInstances"); + } + } + + static getModuleByInstance(instance) { + return session.getModInstances()[instance]; + } + + static set(key,val) { + if (sessionStorage.getItem("session")==undefined) { + var xsession={}; + } else { + var xsession=JSON.parse(sessionStorage.getItem("session")); + } + + xsession[key]=val; + sessionStorage.setItem("session",JSON.stringify(xsession)); + } + + static async load(callback) { + let token=localStorage.getItem("token"); + if (token===null) { + // session not found + session.close(); + console.log("Session token not found"); + return; + } + + let stamp=new Date(); + if ((sessionStorage.getItem("session")!=undefined) && (((stamp.getTime()/1000)-this.get("updated"))",{class: "datatable sel"}); + $("",{class: "idx", text: "#"}).appendTo(tx); + $("",{class: "", text: langPack.core.iface.module}).appendTo(tx); + $("",{class: "", text: langPack.core.iface.groups.aclRule}).appendTo(tx); + $("",{class: "button", append: $("",{class: "fas fa-ellipsis-h"})}).appendTo(tx); + tx.appendTo("div.widget.acls"); + + let i=0; + + $.each(json.data, function(mKey, mVal) { + + $.each(mVal, function(aKey, aVal) { + + i++; + let row=$("",{}).bind('contextmenu', aclContextMenuOpen).addClass("contextMenu").attr("xMod",mKey).attr("xRule",aVal); + $("",{class: "idx", text: i}).appendTo(row); + $("",{class: "xModule", text: mKey}).appendTo(row); + $("",{class: "xRule", text: aVal}).appendTo(row); + $("",{class: "button", append: $("",{class: "fas fa-ellipsis-h"})}) + .click(aclContextMenuOpen) + .appendTo(row); + + + $("",{append: row}).appendTo(tx); + + }); + }) + }); +} + +function reloadMembers() { + $("div.widget.members").empty(); + API.exec("POST","core/userGroupMembership/search",{groupId: UI.parceURL().id},function(json) { + + let tx = $("",{class: "datatable sel"}); + $("",{}).bind('contextmenu', usmContextMenuOpen).addClass("contextMenu").attr("userId",ugm.user.id); + $("",{append: row}).appendTo(tx); + }); + + }) +} + +function usmContextMenuOpen(el) { + + let userId=$(el.target).closest("tr").attr("userId"); + let userFullName=$(el.target).closest("tr").find("td.xFullName").text(); + UI.contextMenuOpen(el,[ + {title: langPack.core.iface.delete, onClick: function(el) { + btnUserpDel_click(userId, userFullName); + }}],userFullName); + + return false; +} + +function aclContextMenuOpen(el) { + + var mod=$(el.target).closest("tr").attr("xMod"); + let rule=$(el.target).closest("tr").attr("xRule"); + UI.contextMenuOpen(el,[ + {title: langPack.core.iface.delete, onClick: function(el) { + btnAclDel_click(mod, rule); + }}],mod+":"+rule); + + return false; +} + +function btnUserpDel_click(userId, fullName) { + var buttons={}; + buttons[langPack.core.iface.dialodDelButton]=function() { + $("#dialogInfo").dialog("close"); + API.exec({ + requestType: "DELETE", + method: "core/userGroupMembership", + data: {groupId: UI.parceURL().id, userId: userId}, + onSuccess: function(json) { + reloadMembers(); + }, + errDict: langPack.core.iface.groups.errors, + }); + + }; + + buttons[langPack.core.iface.dialodCloseButton]=function() { $("#dialogInfo").dialog("close"); } + + UI.showInfoDialog({message: langPack.core.iface.groups.deleteUserFromGroupDialog+"
#"+userId+" "+fullName,buttons: buttons}); + +} + +function btnAclDel_click(mod, rule) { + console.log(mod,rule); + var buttons={}; + buttons[langPack.core.iface.dialodDelButton]=function() { + $("#dialogInfo").dialog("close"); + API.exec({ + requestType: "DELETE", + method: "core/userGroup/acl", + data: {groupId: UI.parceURL().id,module: mod, rule: rule}, + onSuccess: function(json) { + reloadACLs(); + }, + errDict: langPack.core.iface.groups.errors, + }); + + }; + + buttons[langPack.core.iface.dialodCloseButton]=function() { $("#dialogInfo").dialog("close"); } + + UI.showInfoDialog({message: langPack.core.iface.delete+"
#"+mod+":"+rule,buttons: buttons}); + +} + + +function btnUserpAdd_click() { + var buttons={}; + var xid=UI.parceURL().id; + + buttons[langPack.core.iface.dialodAddButton]=function() { + let fdata=UI.collectForm("addgrp", true,false, false,true); + if (fdata.validateErrCount==0) { + API.exec({ + errDict: langPack.core.iface.groups.errors, + requestType: "PUT", + data: { + userId: fdata.name_id.val, + groupId: xid + }, + method: "core/userGroupMembership", + onSuccess: function(json) { + UI.closeDialog('addgrp'); + reloadMembers(); + } + }); + } + }; + buttons[langPack.core.iface.dialodCloseButton]=function() { UI.closeDialog('addgrp'); } + + UI.createDialog( + UI.addFieldGroup([ + UI.addField({item: "dag_name", title: langPack.core.iface.title, type: "autocomplete", reqx: "true",acFunction: function(request,response) { + + API.exec({ + requestType: "POST", + method: "core/user/search", + data: {pattern: request.term, pageSize: 10}, + onSuccess: function(json) { + let rv=[]; + $.each(json.data,function(key,val) { + rv.push({id: val.id, value: val.fullName}); + }); + response(rv); + return false; + }, + errDict: langPack.core.iface.groups.errors, + }); + }}), + ]), + langPack.core.iface.add, + buttons, + 185,1,'addgrp'); + + + UI.openDialog('addgrp') +} + +function btnAclAdd_click() { + var buttons={}; + var xid=UI.parceURL().id; + var modules=[]; + + buttons[langPack.core.iface.dialodAddButton]=function() { + let fdata=UI.collectForm("addgrp", true,false, false,true); + + if (fdata.validateErrCount==0) { + API.exec({ + errDict: langPack.core.iface.groups.errors, + requestType: "PUT", + data: { + groupId: xid, + module: fdata.module.val, + rule: fdata.rule.val, + forAll: fdata.all.val + }, + method: "core/userGroup/acl", + onSuccess: function(json) { + UI.closeDialog('addgrp'); + reloadACLs(); + } + }); + } + }; + buttons[langPack.core.iface.dialodCloseButton]=function() { UI.closeDialog('addgrp'); } + + UI.createDialog( + UI.addFieldGroup([ + UI.addField({item: "acl_module", title: langPack.core.iface.module, type: "select", reqx: "true",args: {"":"--- "+langPack.core.iface.groups.selectModule+" ---"}}), + UI.addField({item: "acl_rule", title: langPack.core.iface.groups.aclRule, type: "select", reqx: "true"}), + UI.addField({item: "acl_all", title: langPack.core.iface.groups.allModules, type: "select",args: {0: langPack.core.iface.no, 1: langPack.core.iface.yes}}), + ]), + + langPack.core.iface.add, + buttons, + 300,1,'addgrp'); + + $("#acl_module").change(function(ref) { + let mod=($(ref.currentTarget).val()); + $("#acl_rule").empty(); + if (mod=="") { + return; + } + + $.each(modules[mod].ACLRules, function(key,val) { + $("
",{class: "idx", text: "#"}).appendTo(tx); + $("",{class: "", text: langPack.core.iface.invCode}).appendTo(tx); + $("",{class: "", text: langPack.core.iface.users.fullName}).appendTo(tx); + $("",{class: "", text: langPack.core.iface.users.active}).appendTo(tx); + $("",{class: "button", append: $("",{class: "fas fa-ellipsis-h"})}).appendTo(tx); + tx.appendTo("div.widget.members"); + + let i=0; + $.each(json.data, function(idx, ugm) { + i++; + let row=$("
",{class: "idx", text: i}).appendTo(row); + $("",{class: "", text: UI.formatInvCode(ugm.user.invCode)}).appendTo(row); + $("",{class: "xFullName", text: ugm.user.fullName}).appendTo(row); + $("",{class: "", text: ugm.user.active}).appendTo(row); + $("",{class: "button", append: $("",{class: "fas fa-ellipsis-h"})}) + .click(usmContextMenuOpen) + .appendTo(row); + + + $("
",{class: "datatable sel"}); + $("",{}).bind('contextmenu', usmContextMenuOpen).addClass("contextMenu").attr("groupId",group.id).attr("xname",group.name); + $("",{append: row}).appendTo(tx); + }); + + + }) + + +} + +function usmContextMenuOpen(el) { + + let groupName=$(el.target).closest("tr").find("td.xGroupName").text(); + UI.contextMenuOpen(el,[ + {title: langPack.core.iface.open, onClick: function() { + $(el.currentTarget).closest("tr").click(); + }}, + {title: langPack.core.iface.delete, onClick: function() { + btnGroupDel_click(el); + }}, + + ],groupName); + + return false; +} + + +function btnGroupAdd_click() { + var buttons={}; + + buttons[langPack.core.iface.dialodAddButton]=function() { + let fdata=UI.collectForm("addgrp", true,false, false,true); + if (fdata.validateErrCount==0) { + API.exec({ + errDict: langPack.core.iface.groups.errors, + requestType: "PUT", + data: { + name: fdata.name.val, + isList: fdata.list.val + }, + method: "core/userGroup", + onSuccess: function(json) { + UI.closeDialog('addgrp'); + reloadGroups(); + } + }); + } + }; + buttons[langPack.core.iface.dialodCloseButton]=function() { UI.closeDialog('addgrp'); } + + UI.createDialog( + UI.addFieldGroup([ + UI.addField({item: "dag_name", title: langPack.core.iface.title, type: "input", reqx: "true"}), + UI.addField({item: "dag_list", title: langPack.core.iface.groups.isList, type: "select",args: {"0":"ACL", "1": langPack.core.iface.groups.isList}}), + ]), + langPack.core.iface.dialodAddButton, + buttons, + 245,1,'addgrp'); + + + UI.openDialog('addgrp') +} + +function btnGroupDel_click(ref) { + var gid=($(ref.currentTarget).attr("groupId")); + var buttons={}; + buttons[langPack.core.iface.dialodDelButton]=function() { + $("#dialogInfo").dialog("close"); + API.exec({ + requestType: "DELETE", + method: "core/userGroup/"+gid, + onSuccess: function(json) { + reloadGroups(); + }, + errDict: langPack.core.iface.groups.errors, + }); + + }; + + buttons[langPack.core.iface.dialodCloseButton]=function() { $("#dialogInfo").dialog("close"); } + + UI.showInfoDialog(langPack.core.iface.groups.delButtonTitle+" #"+gid+" "+$(ref.currentTarget).attr("xname"),langPack.core.iface.groups.delButtonTitle,buttons); +} \ No newline at end of file diff --git a/static/js/core/coreModView.js b/static/js/core/coreModView.js new file mode 100644 index 0000000..ab0ec07 --- /dev/null +++ b/static/js/core/coreModView.js @@ -0,0 +1,259 @@ +import * as API from './api.js'; +import * as UI from './ui.js'; +import { langPack } from './langpack.js'; + +export function load() { + UI.createLeftPanel([{id: "gendesc", title: langPack.core.iface.genDescTitle, }]); + UI.createRightPanel([{id: "aclRules", title: langPack.core.iface.groups.aclRule}, + {id: "features", title: langPack.core.iface.modules.features, preLoad: reloadFeatures }, + {id: "configKeys", title: langPack.core.iface.modules.settings, preLoad: reloadConfig }, + ]); + + reloadGenDesc(); + +} + +function reloadGenDesc() { + API.exec("GET","core/moduleInfo/"+UI.parceURL().id,{},function(json) { + $("div.widget#gendesc").empty(); + UI.addFieldGroup([ + /* + + >> ACLRules >> tab + >> features >> tab + >> configKeys >> tab + + + + */ + UI.addField({title: langPack.core.iface.modules.globalAccessKey, val: json.data.globalAccessKey}), + UI.addField({title: langPack.core.iface.installed, val: json.data.installDate}), + UI.addField({title: langPack.core.iface.modules.instanceOf, val: json.data.instanceOf}), + UI.addField({title: langPack.core.iface.language, val: json.data.languages}), + UI.addField({title: langPack.core.iface.modules.priority, val: json.data.modPriority}), + UI.addField({title: langPack.core.iface.version, val: json.data.modVersion}), + UI.addField({title: langPack.core.iface.title, val: json.data.name}), + UI.addField({title: langPack.core.iface.modules.namespace, val: json.data.namespace}), + UI.addField({title: langPack.core.iface.desc, val: json.data.title}), + UI.addField({title: langPack.core.iface.updated, val: json.data.updateDate}), + + UI.addField({title: langPack.core.iface.modules.authRequired, val: json.data.authRequired?langPack.core.iface.yes:langPack.core.iface.no}), + UI.addField({title: langPack.core.iface.active, val: json.data.enabled?langPack.core.iface.yes:langPack.core.iface.no}), + UI.addField({title: langPack.core.iface.template, val: json.data.isTemplate?langPack.core.iface.yes:langPack.core.iface.no}), + UI.addField({title: langPack.core.iface.modules.multiInstanceAllowed, val: json.data.singleInstanceOnly?langPack.core.iface.no:langPack.core.iface.yes}), + ]).appendTo("div.widget#gendesc") + + $("div.widget.aclRules").empty(); + let tx = $("
",{class: "idx", text: "#"}).appendTo(tx); + $("",{class: "", text: langPack.core.iface.title}).appendTo(tx); + $("",{class: "icon", text: langPack.core.iface.groups.isList}).appendTo(tx); + $("",{class: "button", style: "text-align: center", append: $("",{class: "fas fa-ellipsis-h"})}).appendTo(tx); + + tx.appendTo("div.widget.groups"); + + let i=0; + $.each(json.data, function(idx, group) { + i++; + let row=$("
",{class: "idx", text: i}).appendTo(row); + $("",{class: "xGroupName", text: group.name}).appendTo(row); + $("",{class: "icon", text: group.isList}).appendTo(row); + //$("",{class: "button", append: UI.addButton({id: "btn_group_del_"+group.id,title: langPack.core.iface.groups.delButtonTitle, icon: "fas fa-trash",onClick: btnGroupDel_click, attr: {groupId: group.id, xname: group.name}})}).appendTo(row); + $("",{class: "button", style: "text-align: center", append: $("",{class: "fas fa-ellipsis-h"})}) + .click(usmContextMenuOpen) + .appendTo(row); + row.foxClick("/"+UI.parceURL().module+"/group/"+group.id); + + $("
",{class: "datatable sel"}); + $("",{});//.bind('contextmenu', mliContextMenuOpen).addClass("contextMenu").attr("modName",mod.name).attr("modTitle",mod.title); + $("",{append: row}).appendTo(tx); + }); + + }); +} + +function reloadConfig () { + API.exec("GET","core/moduleInfo/"+UI.parceURL().id+"/config",{},function(json) { + $("div.widget.configKeys").empty(); + let tx = $("
",{class: "idx", text: "#"}).appendTo(tx); + $("",{class: "", text: langPack.core.iface.title}).appendTo(tx); + $("",{class: "", text: langPack.core.iface.desc}).appendTo(tx); + + tx.appendTo("div.widget.aclRules"); + + let i=0; + $.each(json.data.ACLRules, function(idx, mod) { + i++; + let row=$("
",{class: "idx", text: i}).appendTo(row); + $("",{class: "", text: idx}).appendTo(row); + $("",{class: "", text: mod}).appendTo(row); + + + $("
",{class: "datatable sel"}); + $("",{}).bind('contextmenu', mlcContextMenuOpen).addClass("contextMenu").attr("xKey",idx).attr("xSet",isset(json.data.values[idx])); + $("",{append: row}).appendTo(tx); + }); + }); +} + +function reloadFeatures () { + API.exec("GET","core/moduleInfo/"+UI.parceURL().id+"/features",{},function(json) { + $("div.widget.features").empty(); + let tx = $("
",{class: "idx", text: "#"}).appendTo(tx); + $("",{class: "", text: langPack.core.iface.title}).appendTo(tx); + $("",{class: "", text: langPack.core.iface.desc}).appendTo(tx); + $("",{class: "", text: langPack.core.iface.value}).appendTo(tx); + $("",{class: "button", style: "text-align: center", append: $("",{class: "fas fa-ellipsis-h"})}).appendTo(tx); + + tx.appendTo("div.widget.configKeys"); + + let resConfig=json.data.keys; + $.each(json.data.values,function(key,val) { + if (resConfig[key]==undefined) { resConfig[key]=key; } + }); + + let i=0; + $.each(resConfig, function(idx, mod) { + i++; + let row=$("
",{class: "idx", text: i}).appendTo(row); + $("",{class: "", text: idx}).appendTo(row); + $("",{class: "", text: mod}).appendTo(row); + $("",{class: "", text: json.data.values[idx]}).appendTo(row); + $("",{class: "button", style: "text-align: center", append: $("",{class: "fas fa-ellipsis-h"})}) + .click(mlcContextMenuOpen) + .appendTo(row); + + $("
",{class: "datatable sel"}); + $("",{}).bind('contextmenu', mlfContextMenuOpen).addClass("contextMenu").attr("xFeature",idx).attr("xActive",mod); + $("",{append: row}).appendTo(tx); + }); + }); +} + +function mlfContextMenuOpen(el) { + let feature=$(el.target).closest("tr").attr("xFeature"); + let active=$(el.target).closest("tr").attr("xActive")=="true"; + + let menuItems=[]; + + + if (active) { + menuItems.push({title: langPack.core.iface.delete, onClick: function() { + let buttons={}; + buttons[langPack.core.iface.dialodDelButton]=function() { + $("#dialogInfo").dialog("close"); + API.exec({ + requestType: "DELETE", + method: "core/moduleInfo/"+UI.parceURL().id+"/features", + data: {feature: feature}, + onSuccess: function(json) { + reloadFeatures(); + }, + errDict: langPack.core.iface.modules.errors, + }); + + }; + + buttons[langPack.core.iface.dialodCloseButton]=function() { $("#dialogInfo").dialog("close"); } + + UI.showInfoDialog(langPack.core.iface.modules.delFeatureDialogText+" #"+feature+"?",langPack.core.iface.dialodDelButton,buttons); + }}, + ); + } else { + menuItems.push({title: langPack.core.iface.add, onClick: function() { + let buttons={}; + buttons[langPack.core.iface.dialodAddButton]=function() { + $("#dialogInfo").dialog("close"); + API.exec({ + requestType: "PUT", + method: "core/moduleInfo/"+UI.parceURL().id+"/features", + data: {feature: feature}, + onSuccess: function(json) { + reloadFeatures(); + }, + errDict: langPack.core.iface.modules.errors, + }); + + }; + + buttons[langPack.core.iface.dialodCloseButton]=function() { $("#dialogInfo").dialog("close"); } + + UI.showInfoDialog(langPack.core.iface.modules.addFeatureDialogText+" #"+feature+"?",langPack.core.iface.dialodDelButton,buttons); + }}, + ); + } + + UI.contextMenuOpen(el,menuItems,feature); + + return false; +} + +function mlcContextMenuOpen(el) { + let key=$(el.target).closest("tr").attr("xKey"); + let active=$(el.target).closest("tr").attr("xSet")=="true"; + + let menuItems=[]; + if (active) { + menuItems.push({title: langPack.core.iface.delete, onClick: function() { + let buttons={}; + buttons[langPack.core.iface.dialodDelButton]=function() { + $("#dialogInfo").dialog("close"); + API.exec({ + requestType: "DELETE", + method: "core/moduleInfo/"+UI.parceURL().id+"/config", + data: {key: key}, + onSuccess: function(json) { + reloadConfig(); + }, + errDict: langPack.core.iface.modules.errors, + }); + + }; + + buttons[langPack.core.iface.dialodCloseButton]=function() { $("#dialogInfo").dialog("close"); } + + UI.showInfoDialog(langPack.core.iface.modules.delConfigDialogText+" #"+key+"?",langPack.core.iface.dialodDelButton,buttons); + }}); + } + + menuItems.push({title: langPack.core.iface.set, onClick: function() { + var buttons={}; + + buttons[langPack.core.iface.dialodAddButton]=function() { + let fdata=UI.collectForm("addcfg", true,false, false,true); + if (fdata.validateErrCount==0) { + API.exec({ + errDict: langPack.core.iface.groups.errors, + requestType: "PUT", + data: { + value: fdata.val.val, + key: key, + }, + method: "core/moduleInfo/"+UI.parceURL().id+"/config", + onSuccess: function(json) { + UI.closeDialog('addcfg'); + reloadConfig(); + } + }); + } + }; + buttons[langPack.core.iface.dialodCloseButton]=function() { UI.closeDialog('addcfg'); } + + UI.createDialog( + UI.addFieldGroup([ + UI.addField({title: langPack.core.iface.title, val: key}), + UI.addField({item: "cfg_val", title: langPack.core.iface.title, type: "input", reqx: "true"}), + ]), + langPack.core.iface.dialodAddButton, + buttons, + 245,1,'addcfg'); + + + UI.openDialog('addcfg') + }}); + + + + + UI.contextMenuOpen(el,menuItems,key); + + return false; +} diff --git a/static/js/core/coreModule.js b/static/js/core/coreModule.js new file mode 100644 index 0000000..fec834c --- /dev/null +++ b/static/js/core/coreModule.js @@ -0,0 +1,68 @@ +import * as API from './api.js'; +import * as UI from './ui.js'; +import { langPack } from './langpack.js'; + + + +export var menuSelector={ + "groups":"adminGrous", + "modules":"adminModules", + "module":"adminModules", + "users":"adminUsers", + "group":"adminGrous", +}; + +export function load() { + let ref=UI.parceURL(); + switch (ref.function) { + case undefined: + break; + + case "myprofile": + import("./coreMyProfile.js").then(function(mod) { + mod.load(); + }) + break; + + case "modules": + import("./coreModules.js").then(function(mod) { + mod.load(); + }) + break; + + case "module": + import("./coreModView.js").then(function(mod) { + mod.load(); + }) + break; + + case "groups": + import("./coreGroups.js").then(function(mod) { + mod.load(); + }) + break; + + case "group": + import("./coreGroup.js").then(function(mod) { + mod.load(); + }) + break; + + case "users": + import("./coreUsers.js").then(function(mod) { + mod.load(); + }) + break; + + case "userEmailConfirm": + import("./userMailConfirm.js").then(function(mod) { + mod.load(); + }) + break; + + + default: + throw new Error(404); + } + return true; +} \ No newline at end of file diff --git a/static/js/core/coreModules.js b/static/js/core/coreModules.js new file mode 100644 index 0000000..8d448a5 --- /dev/null +++ b/static/js/core/coreModules.js @@ -0,0 +1,171 @@ +import * as API from './api.js'; +import * as UI from './ui.js'; +import { langPack } from './langpack.js'; + +export function load() { + UI.breadcrumbsUpdate(langPack.core.breadcrumbs.modTitle+" / "+langPack.core.breadcrumbs.modules); + + UI.createTabsPanel({ + installed:{"title":langPack.core.iface.modules.installedTabTitle, preLoad: reloadInstalled}, + avail:{"title":langPack.core.iface.modules.availTabTitle, preLoad: reloadAvail}, + }) +} + +function reloadInstalled() { + $("div.widget.installed").empty(); + API.exec("GET","core/modules/installed",{},function(json) { + let tx = $("
",{class: "idx", text: "#"}).appendTo(tx); + $("",{class: "", text: langPack.core.iface.title}).appendTo(tx); + $("",{class: "", text: langPack.core.iface.active}).appendTo(tx); + $("",{class: "button", style: "text-align: center", append: $("",{class: "fas fa-ellipsis-h"})}).appendTo(tx); + + tx.appendTo("div.widget.features"); + + let i=0; + $.each(json.data, function(idx, mod) { + i++; + let row=$("
",{class: "idx", text: i}).appendTo(row); + $("",{class: "", text: idx}).appendTo(row); + $("",{class: "", text: mod}).appendTo(row); + $("",{class: "button", style: "text-align: center", append: $("",{class: "fas fa-ellipsis-h"})}) + .click(mlfContextMenuOpen) + .appendTo(row); + + + $("
",{class: "datatable sel"}); + $("",{}).bind('contextmenu', mliContextMenuOpen).addClass("contextMenu").attr("modName",mod.name).attr("modTitle",mod.title); + $("",{append: row}).appendTo(tx); + }); + + }) +} + +function mliContextMenuOpen(el) { + + let groupName=$(el.target).closest("tr").find("td.xGroupName").text(); + UI.contextMenuOpen(el,[ + {title: langPack.core.iface.open, onClick: function() { + $(el.currentTarget).closest("tr").click(); + }}, + {title: langPack.core.iface.delete, onClick: function() { + moduleDelete_Click(el); + }}, + + ],$(el.target).closest("tr").attr("modName")); + + return false; +} + +function mlaContextMenuOpen(el) { + + let groupName=$(el.target).closest("tr").find("td.xGroupName").text(); + UI.contextMenuOpen(el,[ + {title: langPack.core.iface.install, onClick: function() { + moduleInstall_Click(el); + }}, + + ],$(el.target).closest("tr").attr("modName")); + + return false; +} + +function moduleInstall_Click(el) { + var buttons={}; + + buttons[langPack.core.iface.dialodAddButton]=function() { + let fdata=UI.collectForm("addmod", true,false, false,true); + if (fdata.validateErrCount==0) { + API.exec({ + errDict: langPack.core.iface.modules.errors, + requestType: "PUT", + data: { + module: $(el.target).closest("tr").attr("modName"), + name: fdata.name.val, + priority: fdata.priority.val, + }, + method: "core/modules/installed", + onSuccess: function(json) { + UI.closeDialog('addmod'); + reloadInstalled(); + reloadAvail(); + } + }); + } + }; + buttons[langPack.core.iface.dialodCloseButton]=function() { UI.closeDialog('addmod'); } + + UI.createDialog( + UI.addFieldGroup([ + UI.addField({item: "mod_name", title: langPack.core.iface.title, type: "input", reqx: "true", reqx: "true", regx: "^[A-Za-z0-9_-]*$", val: $(el.target).closest("tr").attr("modName")}), + UI.addField({item: "mod_priority", title: langPack.core.iface.modules.priority, type: "input", reqx: "true", regx: "^[0-9]*$", val: 100}), + ]), + langPack.core.iface.dialodAddButton, + buttons, + 245,1,'addmod'); + + UI.openDialog('addmod') +} + +function moduleDelete_Click(ref) { + var modName=($(ref.target).closest("tr").attr("modName")); + var buttons={}; + buttons[langPack.core.iface.dialodDelButton]=function() { + $("#dialogInfo").dialog("close"); + API.exec({ + requestType: "DELETE", + method: "core/moduleInfo/"+$(ref.target).closest("tr").attr("modName"), + onSuccess: function(json) { + reloadInstalled(); + reloadAvail(); + }, + errDict: langPack.core.iface.modules.errors, + }); + + }; + + buttons[langPack.core.iface.dialodCloseButton]=function() { $("#dialogInfo").dialog("close"); } + + UI.showInfoDialog(langPack.core.iface.modules.delDialogText+" #"+modName+"?",langPack.core.iface.dialodDelButton,buttons); +} + +function reloadAvail() { + $("div.widget.avail").empty(); + API.exec("GET","core/modules/list",{},function(json) { + let tx = $("
",{class: "idx", text: "#"}).appendTo(tx); + $("",{class: "", text: langPack.core.iface.title}).appendTo(tx); + $("",{class: "", text: langPack.core.iface.active}).appendTo(tx); + $("",{class: "", text: langPack.core.iface.modules.instanceOf}).appendTo(tx); + $("",{class: "", text: langPack.core.iface.version}).appendTo(tx); + $("",{class: "", text: langPack.core.iface.desc}).appendTo(tx); + $("",{class: "", text: langPack.core.iface.installed}).appendTo(tx); + $("",{class: "", text: langPack.core.iface.updated}).appendTo(tx); + $("",{class: "button", style: "text-align: center", append: $("",{class: "fas fa-ellipsis-h"})}).appendTo(tx); + tx.appendTo("div.widget.installed"); + + let i=0; + $.each(json.data, function(idx, mod) { + i++; + let row=$("
",{class: "idx", text: i}).appendTo(row); + $("",{class: "", text: mod.name}).appendTo(row); + $("",{class: "", text: mod.enabled}).appendTo(row); + $("",{class: "", text: mod.instanceOf}).appendTo(row); + $("",{class: "", text: mod.modVersion}).appendTo(row); + $("",{class: "", text: mod.title}).appendTo(row); + $("",{class: "", text: UI.stamp2date(mod.installDate,true)}).appendTo(row); + $("",{class: "", text: UI.stamp2date(mod.updateDate,true)}).appendTo(row); + $("",{class: "button", style: "text-align: center", append: $("",{class: "fas fa-ellipsis-h"})}) + .click(mliContextMenuOpen) + .appendTo(row); + row.foxClick("/"+UI.parceURL().module+"/module/"+mod.name); + + + $("
",{class: "datatable sel"}); + $("",{}).bind('contextmenu', mlaContextMenuOpen).addClass("contextMenu").attr("modName",mod.name).attr("modTitle",mod.title); + $("",{append: row}).appendTo(tx); + }); + }) +} \ No newline at end of file diff --git a/static/js/core/coreMyProfile.js b/static/js/core/coreMyProfile.js new file mode 100644 index 0000000..5e2a23a --- /dev/null +++ b/static/js/core/coreMyProfile.js @@ -0,0 +1,8 @@ +import * as API from './api.js'; +import * as UI from './ui.js'; +import { langPack } from './langpack.js'; + +export function load() { + UI.breadcrumbsUpdate(langPack.core.breadcrumbs.modTitle+" / "+langPack.core.breadcrumbs.myprofile); + +} \ No newline at end of file diff --git a/static/js/core/coreUsers.js b/static/js/core/coreUsers.js new file mode 100644 index 0000000..3b3d690 --- /dev/null +++ b/static/js/core/coreUsers.js @@ -0,0 +1,152 @@ +import * as API from './api.js'; +import * as UI from './ui.js'; +import { langPack } from './langpack.js'; + +export function load() { + UI.breadcrumbsUpdate(langPack.core.breadcrumbs.modTitle+" / "+langPack.core.breadcrumbs.users); + + UI.createTabsPanel({ + users:{"title":langPack.core.iface.users.allTitle, preLoad: reloadUsers}, + invites:{"title":langPack.core.iface.users.invitesTitle, preLoad: reloadInvites,buttons: UI.addButton({title: langPack.core.iface.users.inviteButtonTitle, icon: "fas fa-plus", onClick: btnInviteUser_Click})}, + + }) +} + +function reloadUsers() { + $("div.widget.users").empty(); + API.exec("GET","core/user/list",{},function(json) { + let tx = $("
",{class: "idx", text: "#"}).appendTo(tx); + $("",{class: "", text: langPack.core.iface.title}).appendTo(tx); + $("",{class: "", text: langPack.core.iface.version}).appendTo(tx); + $("",{class: "", text: langPack.core.iface.desc}).appendTo(tx); + $("",{class: "", text: langPack.core.iface.modules.singleInstanceOnly}).appendTo(tx); + $("",{class: "", text: langPack.core.iface.modules.instancesCount}).appendTo(tx); + $("",{class: "button", style: "text-align: center", append: $("",{class: "fas fa-ellipsis-h"})}).appendTo(tx); + + tx.appendTo("div.widget.avail"); + let i=0; + $.each(json.data, function(idx, mod) { + i++; + let row=$("
",{class: "idx", text: i}).appendTo(row); + $("",{class: "", text: mod.name}).appendTo(row); + $("",{class: "", text: mod.modVersion}).appendTo(row); + $("",{class: "", text: mod.title}).appendTo(row); + $("",{class: "", html: mod.singleInstanceOnly?'':""}).appendTo(row); + $("",{class: "", text: mod.instancesCount}).appendTo(row); + $("",{class: "button", style: "text-align: center", append: $("",{class: "fas fa-ellipsis-h"})}) + .click(mlaContextMenuOpen) + .appendTo(row); + + $("
",{class: "datatable sel"}); + $("",{}); + $("",{append: row}).appendTo(tx); + }); + + }) +} + +function reloadInvites() { + $("div.widget.invites").empty(); + API.exec("GET","core/userInvitation/list",{},function(json) { + let tx = $("
",{class: "idx", text: "#"}).appendTo(tx); + $("",{class: "", text: langPack.core.iface.invCode}).appendTo(tx); + $("",{class: "", text: langPack.core.iface.users.login}).appendTo(tx); + $("",{class: "", text: langPack.core.iface.users.email}).appendTo(tx); + $("",{class: "", text: langPack.core.iface.users.fullName}).appendTo(tx); + $("",{class: "", text: langPack.core.iface.users.eMainConfirmedQTitle}).appendTo(tx); + $("",{class: "", text: langPack.core.iface.users.active}).appendTo(tx); + tx.appendTo("div.widget.users"); + + let i=0; + $.each(json.data, function(idx, user) { + i++; + let row=$("
",{class: "idx", text: i}).appendTo(row); + $("",{class: "", text: UI.formatInvCode(user.invCode)}).appendTo(row); + $("",{class: "", text: user.login}).appendTo(row); + $("",{class: "", text: user.eMail}).appendTo(row); + $("",{class: "", text: user.fullName}).appendTo(row); + $("",{class: "", text: user.eMailConfirmed}).appendTo(row); + $("",{class: "", text: user.active}).appendTo(row); + + + $("
",{class: "datatable sel"}); + $("",{}).bind('contextmenu', uimContextMenuOpen).addClass("contextMenu").attr("invId",user.id).attr("xname",formatRegCode(user.regCode)); + $("",{append: row}).appendTo(tx); + }); + + }) +} + +function uimContextMenuOpen(el) { + let xName=$(el.target).closest("tr").attr("xname"); + let xId=$(el.target).closest("tr").attr("invId"); + UI.contextMenuOpen(el,[ + {title: langPack.core.iface.delete, onClick: function() { + var buttons={}; + buttons[langPack.core.iface.dialodDelButton]=function() { + $("#dialogInfo").dialog("close"); + API.exec({ + requestType: "DELETE", + method: "core/userInvitation/"+xId, + onSuccess: function(json) { + reloadInvites(); + }, + errDict: langPack.core.iface.users.errors, + }); + + }; + + buttons[langPack.core.iface.dialodCloseButton]=function() { $("#dialogInfo").dialog("close"); } + + UI.showInfoDialog(langPack.core.iface.users.delInvDialogTitle+" "+xName,langPack.core.iface.users.delInvDialogTitle,buttons); + }}, + ],xName); + + return false; +} + +function formatRegCode(code) { + if ((typeof(code) != 'string') ||code.length==0) { + return langPack.core.iface.emptyInvCode; + } else { + return code.substr(0,4)+"-"+code.substr(4,4)+"-"+code.substr(8,4)+"-"+code.substr(12); + } +} + +function btnInviteUser_Click(ref) { + var buttons={}; + + buttons[langPack.core.iface.dialodAddButton]=function() { + let fdata=UI.collectForm("addgrp", true,false, false,true); + if (fdata.validateErrCount==0) { + API.exec({ + errDict: langPack.core.iface.users.errors, + requestType: "PUT", + data: { + eMail: fdata.email.val, + expireStamp: isset(fdata.expiration.val)?UI.date2stamp(fdata.expiration.val):undefined, + allowMultiUse: fdata.type.val, + }, + method: "core/userInvitation", + onSuccess: function(json) { + UI.closeDialog('addgrp'); + reloadInvites(); + } + }); + } + }; + buttons[langPack.core.iface.dialodCloseButton]=function() { UI.closeDialog('addgrp'); } + + UI.createDialog( + UI.addFieldGroup([ + UI.addField({item: "dag_email", title: langPack.core.iface.users.email, type: "input"}), + UI.addField({item: "dag_type", title: langPack.core.iface.users.allowMultiUseTitle, type: "select",args: {"false":langPack.core.iface.no, "true": langPack.core.iface.yes}}), + UI.addField({item: "dag_expiration", title: langPack.core.iface.users.invitationExpire, type: "datetime", args: {curr: UI.stamp2isodate(UI.getUnixTime(1209600))}}), + ]), + langPack.core.iface.dialodAddButton, + buttons, + 325,1,'addgrp'); + + + UI.openDialog('addgrp') +} \ No newline at end of file diff --git a/static/js/core/fox-common.js b/static/js/core/fox-common.js new file mode 100644 index 0000000..9d603fb --- /dev/null +++ b/static/js/core/fox-common.js @@ -0,0 +1,40 @@ +var UI, API, langPack; +import("./ui.js").then(function(mod) { UI=mod; }); +import("./api.js").then(function(mod) { API=mod; }); +import('./langpack.js').then(function(mod) { langPack=mod.langPack; }); + + +function base64_decode( data ) { // Decodes data encoded with MIME base64 + // + // + original by: Tyler Akins (http://rumkin.com) + + + var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + var o1, o2, o3, h1, h2, h3, h4, bits, i=0, enc=''; + + do { // unpack four hexets into three octets using index points in b64 + h1 = b64.indexOf(data.charAt(i++)); + h2 = b64.indexOf(data.charAt(i++)); + h3 = b64.indexOf(data.charAt(i++)); + h4 = b64.indexOf(data.charAt(i++)); + + bits = h1<<18 | h2<<12 | h3<<6 | h4; + + o1 = bits>>16 & 0xff; + o2 = bits>>8 & 0xff; + o3 = bits & 0xff; + + if (h3 == 64) enc += String.fromCharCode(o1); + else if (h4 == 64) enc += String.fromCharCode(o1, o2); + else enc += String.fromCharCode(o1, o2, o3); + } while (i < data.length); + + return enc; +}; + +function isset(val) +{ + return !(val === null || val == '' || val === undefined); +}; + + diff --git a/static/js/core/jquery-ui.js b/static/js/core/jquery-ui.js new file mode 100644 index 0000000..915f050 --- /dev/null +++ b/static/js/core/jquery-ui.js @@ -0,0 +1,18706 @@ +/*! jQuery UI - v1.12.1 - 2018-08-26 +* http://jqueryui.com +* Includes: widget.js, position.js, data.js, disable-selection.js, focusable.js, form-reset-mixin.js, jquery-1-7.js, keycode.js, labels.js, scroll-parent.js, tabbable.js, unique-id.js, widgets/draggable.js, widgets/droppable.js, widgets/resizable.js, widgets/selectable.js, widgets/sortable.js, widgets/accordion.js, widgets/autocomplete.js, widgets/button.js, widgets/checkboxradio.js, widgets/controlgroup.js, widgets/datepicker.js, widgets/dialog.js, widgets/menu.js, widgets/mouse.js, widgets/progressbar.js, widgets/selectmenu.js, widgets/slider.js, widgets/spinner.js, widgets/tabs.js, widgets/tooltip.js, effect.js, effects/effect-blind.js, effects/effect-bounce.js, effects/effect-clip.js, effects/effect-drop.js, effects/effect-explode.js, effects/effect-fade.js, effects/effect-fold.js, effects/effect-highlight.js, effects/effect-puff.js, effects/effect-pulsate.js, effects/effect-scale.js, effects/effect-shake.js, effects/effect-size.js, effects/effect-slide.js, effects/effect-transfer.js +* Copyright jQuery Foundation and other contributors; Licensed MIT */ + +(function( factory ) { + if ( typeof define === "function" && define.amd ) { + + // AMD. Register as an anonymous module. + define([ "jquery" ], factory ); + } else { + + // Browser globals + factory( jQuery ); + } +}(function( $ ) { + +$.ui = $.ui || {}; + +var version = $.ui.version = "1.12.1"; + + +/*! + * jQuery UI Widget 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: Widget +//>>group: Core +//>>description: Provides a factory for creating stateful widgets with a common API. +//>>docs: http://api.jqueryui.com/jQuery.widget/ +//>>demos: http://jqueryui.com/widget/ + + + +var widgetUuid = 0; +var widgetSlice = Array.prototype.slice; + +$.cleanData = ( function( orig ) { + return function( elems ) { + var events, elem, i; + for ( i = 0; ( elem = elems[ i ] ) != null; i++ ) { + try { + + // Only trigger remove when necessary to save time + events = $._data( elem, "events" ); + if ( events && events.remove ) { + $( elem ).triggerHandler( "remove" ); + } + + // Http://bugs.jquery.com/ticket/8235 + } catch ( e ) {} + } + orig( elems ); + }; +} )( $.cleanData ); + +$.widget = function( name, base, prototype ) { + var existingConstructor, constructor, basePrototype; + + // ProxiedPrototype allows the provided prototype to remain unmodified + // so that it can be used as a mixin for multiple widgets (#8876) + var proxiedPrototype = {}; + + var namespace = name.split( "." )[ 0 ]; + name = name.split( "." )[ 1 ]; + var fullName = namespace + "-" + name; + + if ( !prototype ) { + prototype = base; + base = $.Widget; + } + + if ( $.isArray( prototype ) ) { + prototype = $.extend.apply( null, [ {} ].concat( prototype ) ); + } + + // Create selector for plugin + $.expr[ ":" ][ fullName.toLowerCase() ] = function( elem ) { + return !!$.data( elem, fullName ); + }; + + $[ namespace ] = $[ namespace ] || {}; + existingConstructor = $[ namespace ][ name ]; + constructor = $[ namespace ][ name ] = function( options, element ) { + + // Allow instantiation without "new" keyword + if ( !this._createWidget ) { + return new constructor( options, element ); + } + + // Allow instantiation without initializing for simple inheritance + // must use "new" keyword (the code above always passes args) + if ( arguments.length ) { + this._createWidget( options, element ); + } + }; + + // Extend with the existing constructor to carry over any static properties + $.extend( constructor, existingConstructor, { + version: prototype.version, + + // Copy the object used to create the prototype in case we need to + // redefine the widget later + _proto: $.extend( {}, prototype ), + + // Track widgets that inherit from this widget in case this widget is + // redefined after a widget inherits from it + _childConstructors: [] + } ); + + basePrototype = new base(); + + // We need to make the options hash a property directly on the new instance + // otherwise we'll modify the options hash on the prototype that we're + // inheriting from + basePrototype.options = $.widget.extend( {}, basePrototype.options ); + $.each( prototype, function( prop, value ) { + if ( !$.isFunction( value ) ) { + proxiedPrototype[ prop ] = value; + return; + } + proxiedPrototype[ prop ] = ( function() { + function _super() { + return base.prototype[ prop ].apply( this, arguments ); + } + + function _superApply( args ) { + return base.prototype[ prop ].apply( this, args ); + } + + return function() { + var __super = this._super; + var __superApply = this._superApply; + var returnValue; + + this._super = _super; + this._superApply = _superApply; + + returnValue = value.apply( this, arguments ); + + this._super = __super; + this._superApply = __superApply; + + return returnValue; + }; + } )(); + } ); + constructor.prototype = $.widget.extend( basePrototype, { + + // TODO: remove support for widgetEventPrefix + // always use the name + a colon as the prefix, e.g., draggable:start + // don't prefix for widgets that aren't DOM-based + widgetEventPrefix: existingConstructor ? ( basePrototype.widgetEventPrefix || name ) : name + }, proxiedPrototype, { + constructor: constructor, + namespace: namespace, + widgetName: name, + widgetFullName: fullName + } ); + + // If this widget is being redefined then we need to find all widgets that + // are inheriting from it and redefine all of them so that they inherit from + // the new version of this widget. We're essentially trying to replace one + // level in the prototype chain. + if ( existingConstructor ) { + $.each( existingConstructor._childConstructors, function( i, child ) { + var childPrototype = child.prototype; + + // Redefine the child widget using the same prototype that was + // originally used, but inherit from the new version of the base + $.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, + child._proto ); + } ); + + // Remove the list of existing child constructors from the old constructor + // so the old child constructors can be garbage collected + delete existingConstructor._childConstructors; + } else { + base._childConstructors.push( constructor ); + } + + $.widget.bridge( name, constructor ); + + return constructor; +}; + +$.widget.extend = function( target ) { + var input = widgetSlice.call( arguments, 1 ); + var inputIndex = 0; + var inputLength = input.length; + var key; + var value; + + for ( ; inputIndex < inputLength; inputIndex++ ) { + for ( key in input[ inputIndex ] ) { + value = input[ inputIndex ][ key ]; + if ( input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) { + + // Clone objects + if ( $.isPlainObject( value ) ) { + target[ key ] = $.isPlainObject( target[ key ] ) ? + $.widget.extend( {}, target[ key ], value ) : + + // Don't extend strings, arrays, etc. with objects + $.widget.extend( {}, value ); + + // Copy everything else by reference + } else { + target[ key ] = value; + } + } + } + } + return target; +}; + +$.widget.bridge = function( name, object ) { + var fullName = object.prototype.widgetFullName || name; + $.fn[ name ] = function( options ) { + var isMethodCall = typeof options === "string"; + var args = widgetSlice.call( arguments, 1 ); + var returnValue = this; + + if ( isMethodCall ) { + + // If this is an empty collection, we need to have the instance method + // return undefined instead of the jQuery instance + if ( !this.length && options === "instance" ) { + returnValue = undefined; + } else { + this.each( function() { + var methodValue; + var instance = $.data( this, fullName ); + + if ( options === "instance" ) { + returnValue = instance; + return false; + } + + if ( !instance ) { + return $.error( "cannot call methods on " + name + + " prior to initialization; " + + "attempted to call method '" + options + "'" ); + } + + if ( !$.isFunction( instance[ options ] ) || options.charAt( 0 ) === "_" ) { + return $.error( "no such method '" + options + "' for " + name + + " widget instance" ); + } + + methodValue = instance[ options ].apply( instance, args ); + + if ( methodValue !== instance && methodValue !== undefined ) { + returnValue = methodValue && methodValue.jquery ? + returnValue.pushStack( methodValue.get() ) : + methodValue; + return false; + } + } ); + } + } else { + + // Allow multiple hashes to be passed on init + if ( args.length ) { + options = $.widget.extend.apply( null, [ options ].concat( args ) ); + } + + this.each( function() { + var instance = $.data( this, fullName ); + if ( instance ) { + instance.option( options || {} ); + if ( instance._init ) { + instance._init(); + } + } else { + $.data( this, fullName, new object( options, this ) ); + } + } ); + } + + return returnValue; + }; +}; + +$.Widget = function( /* options, element */ ) {}; +$.Widget._childConstructors = []; + +$.Widget.prototype = { + widgetName: "widget", + widgetEventPrefix: "", + defaultElement: "
", + + options: { + classes: {}, + disabled: false, + + // Callbacks + create: null + }, + + _createWidget: function( options, element ) { + element = $( element || this.defaultElement || this )[ 0 ]; + this.element = $( element ); + this.uuid = widgetUuid++; + this.eventNamespace = "." + this.widgetName + this.uuid; + + this.bindings = $(); + this.hoverable = $(); + this.focusable = $(); + this.classesElementLookup = {}; + + if ( element !== this ) { + $.data( element, this.widgetFullName, this ); + this._on( true, this.element, { + remove: function( event ) { + if ( event.target === element ) { + this.destroy(); + } + } + } ); + this.document = $( element.style ? + + // Element within the document + element.ownerDocument : + + // Element is window or document + element.document || element ); + this.window = $( this.document[ 0 ].defaultView || this.document[ 0 ].parentWindow ); + } + + this.options = $.widget.extend( {}, + this.options, + this._getCreateOptions(), + options ); + + this._create(); + + if ( this.options.disabled ) { + this._setOptionDisabled( this.options.disabled ); + } + + this._trigger( "create", null, this._getCreateEventData() ); + this._init(); + }, + + _getCreateOptions: function() { + return {}; + }, + + _getCreateEventData: $.noop, + + _create: $.noop, + + _init: $.noop, + + destroy: function() { + var that = this; + + this._destroy(); + $.each( this.classesElementLookup, function( key, value ) { + that._removeClass( value, key ); + } ); + + // We can probably remove the unbind calls in 2.0 + // all event bindings should go through this._on() + this.element + .off( this.eventNamespace ) + .removeData( this.widgetFullName ); + this.widget() + .off( this.eventNamespace ) + .removeAttr( "aria-disabled" ); + + // Clean up events and states + this.bindings.off( this.eventNamespace ); + }, + + _destroy: $.noop, + + widget: function() { + return this.element; + }, + + option: function( key, value ) { + var options = key; + var parts; + var curOption; + var i; + + if ( arguments.length === 0 ) { + + // Don't return a reference to the internal hash + return $.widget.extend( {}, this.options ); + } + + if ( typeof key === "string" ) { + + // Handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } } + options = {}; + parts = key.split( "." ); + key = parts.shift(); + if ( parts.length ) { + curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] ); + for ( i = 0; i < parts.length - 1; i++ ) { + curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {}; + curOption = curOption[ parts[ i ] ]; + } + key = parts.pop(); + if ( arguments.length === 1 ) { + return curOption[ key ] === undefined ? null : curOption[ key ]; + } + curOption[ key ] = value; + } else { + if ( arguments.length === 1 ) { + return this.options[ key ] === undefined ? null : this.options[ key ]; + } + options[ key ] = value; + } + } + + this._setOptions( options ); + + return this; + }, + + _setOptions: function( options ) { + var key; + + for ( key in options ) { + this._setOption( key, options[ key ] ); + } + + return this; + }, + + _setOption: function( key, value ) { + if ( key === "classes" ) { + this._setOptionClasses( value ); + } + + this.options[ key ] = value; + + if ( key === "disabled" ) { + this._setOptionDisabled( value ); + } + + return this; + }, + + _setOptionClasses: function( value ) { + var classKey, elements, currentElements; + + for ( classKey in value ) { + currentElements = this.classesElementLookup[ classKey ]; + if ( value[ classKey ] === this.options.classes[ classKey ] || + !currentElements || + !currentElements.length ) { + continue; + } + + // We are doing this to create a new jQuery object because the _removeClass() call + // on the next line is going to destroy the reference to the current elements being + // tracked. We need to save a copy of this collection so that we can add the new classes + // below. + elements = $( currentElements.get() ); + this._removeClass( currentElements, classKey ); + + // We don't use _addClass() here, because that uses this.options.classes + // for generating the string of classes. We want to use the value passed in from + // _setOption(), this is the new value of the classes option which was passed to + // _setOption(). We pass this value directly to _classes(). + elements.addClass( this._classes( { + element: elements, + keys: classKey, + classes: value, + add: true + } ) ); + } + }, + + _setOptionDisabled: function( value ) { + this._toggleClass( this.widget(), this.widgetFullName + "-disabled", null, !!value ); + + // If the widget is becoming disabled, then nothing is interactive + if ( value ) { + this._removeClass( this.hoverable, null, "ui-state-hover" ); + this._removeClass( this.focusable, null, "ui-state-focus" ); + } + }, + + enable: function() { + return this._setOptions( { disabled: false } ); + }, + + disable: function() { + return this._setOptions( { disabled: true } ); + }, + + _classes: function( options ) { + var full = []; + var that = this; + + options = $.extend( { + element: this.element, + classes: this.options.classes || {} + }, options ); + + function processClassString( classes, checkOption ) { + var current, i; + for ( i = 0; i < classes.length; i++ ) { + current = that.classesElementLookup[ classes[ i ] ] || $(); + if ( options.add ) { + current = $( $.unique( current.get().concat( options.element.get() ) ) ); + } else { + current = $( current.not( options.element ).get() ); + } + that.classesElementLookup[ classes[ i ] ] = current; + full.push( classes[ i ] ); + if ( checkOption && options.classes[ classes[ i ] ] ) { + full.push( options.classes[ classes[ i ] ] ); + } + } + } + + this._on( options.element, { + "remove": "_untrackClassesElement" + } ); + + if ( options.keys ) { + processClassString( options.keys.match( /\S+/g ) || [], true ); + } + if ( options.extra ) { + processClassString( options.extra.match( /\S+/g ) || [] ); + } + + return full.join( " " ); + }, + + _untrackClassesElement: function( event ) { + var that = this; + $.each( that.classesElementLookup, function( key, value ) { + if ( $.inArray( event.target, value ) !== -1 ) { + that.classesElementLookup[ key ] = $( value.not( event.target ).get() ); + } + } ); + }, + + _removeClass: function( element, keys, extra ) { + return this._toggleClass( element, keys, extra, false ); + }, + + _addClass: function( element, keys, extra ) { + return this._toggleClass( element, keys, extra, true ); + }, + + _toggleClass: function( element, keys, extra, add ) { + add = ( typeof add === "boolean" ) ? add : extra; + var shift = ( typeof element === "string" || element === null ), + options = { + extra: shift ? keys : extra, + keys: shift ? element : keys, + element: shift ? this.element : element, + add: add + }; + options.element.toggleClass( this._classes( options ), add ); + return this; + }, + + _on: function( suppressDisabledCheck, element, handlers ) { + var delegateElement; + var instance = this; + + // No suppressDisabledCheck flag, shuffle arguments + if ( typeof suppressDisabledCheck !== "boolean" ) { + handlers = element; + element = suppressDisabledCheck; + suppressDisabledCheck = false; + } + + // No element argument, shuffle and use this.element + if ( !handlers ) { + handlers = element; + element = this.element; + delegateElement = this.widget(); + } else { + element = delegateElement = $( element ); + this.bindings = this.bindings.add( element ); + } + + $.each( handlers, function( event, handler ) { + function handlerProxy() { + + // Allow widgets to customize the disabled handling + // - disabled as an array instead of boolean + // - disabled class as method for disabling individual parts + if ( !suppressDisabledCheck && + ( instance.options.disabled === true || + $( this ).hasClass( "ui-state-disabled" ) ) ) { + return; + } + return ( typeof handler === "string" ? instance[ handler ] : handler ) + .apply( instance, arguments ); + } + + // Copy the guid so direct unbinding works + if ( typeof handler !== "string" ) { + handlerProxy.guid = handler.guid = + handler.guid || handlerProxy.guid || $.guid++; + } + + var match = event.match( /^([\w:-]*)\s*(.*)$/ ); + var eventName = match[ 1 ] + instance.eventNamespace; + var selector = match[ 2 ]; + + if ( selector ) { + delegateElement.on( eventName, selector, handlerProxy ); + } else { + element.on( eventName, handlerProxy ); + } + } ); + }, + + _off: function( element, eventName ) { + eventName = ( eventName || "" ).split( " " ).join( this.eventNamespace + " " ) + + this.eventNamespace; + element.off( eventName ).off( eventName ); + + // Clear the stack to avoid memory leaks (#10056) + this.bindings = $( this.bindings.not( element ).get() ); + this.focusable = $( this.focusable.not( element ).get() ); + this.hoverable = $( this.hoverable.not( element ).get() ); + }, + + _delay: function( handler, delay ) { + function handlerProxy() { + return ( typeof handler === "string" ? instance[ handler ] : handler ) + .apply( instance, arguments ); + } + var instance = this; + return setTimeout( handlerProxy, delay || 0 ); + }, + + _hoverable: function( element ) { + this.hoverable = this.hoverable.add( element ); + this._on( element, { + mouseenter: function( event ) { + this._addClass( $( event.currentTarget ), null, "ui-state-hover" ); + }, + mouseleave: function( event ) { + this._removeClass( $( event.currentTarget ), null, "ui-state-hover" ); + } + } ); + }, + + _focusable: function( element ) { + this.focusable = this.focusable.add( element ); + this._on( element, { + focusin: function( event ) { + this._addClass( $( event.currentTarget ), null, "ui-state-focus" ); + }, + focusout: function( event ) { + this._removeClass( $( event.currentTarget ), null, "ui-state-focus" ); + } + } ); + }, + + _trigger: function( type, event, data ) { + var prop, orig; + var callback = this.options[ type ]; + + data = data || {}; + event = $.Event( event ); + event.type = ( type === this.widgetEventPrefix ? + type : + this.widgetEventPrefix + type ).toLowerCase(); + + // The original event may come from any element + // so we need to reset the target on the new event + event.target = this.element[ 0 ]; + + // Copy original event properties over to the new event + orig = event.originalEvent; + if ( orig ) { + for ( prop in orig ) { + if ( !( prop in event ) ) { + event[ prop ] = orig[ prop ]; + } + } + } + + this.element.trigger( event, data ); + return !( $.isFunction( callback ) && + callback.apply( this.element[ 0 ], [ event ].concat( data ) ) === false || + event.isDefaultPrevented() ); + } +}; + +$.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) { + $.Widget.prototype[ "_" + method ] = function( element, options, callback ) { + if ( typeof options === "string" ) { + options = { effect: options }; + } + + var hasOptions; + var effectName = !options ? + method : + options === true || typeof options === "number" ? + defaultEffect : + options.effect || defaultEffect; + + options = options || {}; + if ( typeof options === "number" ) { + options = { duration: options }; + } + + hasOptions = !$.isEmptyObject( options ); + options.complete = callback; + + if ( options.delay ) { + element.delay( options.delay ); + } + + if ( hasOptions && $.effects && $.effects.effect[ effectName ] ) { + element[ method ]( options ); + } else if ( effectName !== method && element[ effectName ] ) { + element[ effectName ]( options.duration, options.easing, callback ); + } else { + element.queue( function( next ) { + $( this )[ method ](); + if ( callback ) { + callback.call( element[ 0 ] ); + } + next(); + } ); + } + }; +} ); + +var widget = $.widget; + + +/*! + * jQuery UI Position 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/position/ + */ + +//>>label: Position +//>>group: Core +//>>description: Positions elements relative to other elements. +//>>docs: http://api.jqueryui.com/position/ +//>>demos: http://jqueryui.com/position/ + + +( function() { +var cachedScrollbarWidth, + max = Math.max, + abs = Math.abs, + rhorizontal = /left|center|right/, + rvertical = /top|center|bottom/, + roffset = /[\+\-]\d+(\.[\d]+)?%?/, + rposition = /^\w+/, + rpercent = /%$/, + _position = $.fn.position; + +function getOffsets( offsets, width, height ) { + return [ + parseFloat( offsets[ 0 ] ) * ( rpercent.test( offsets[ 0 ] ) ? width / 100 : 1 ), + parseFloat( offsets[ 1 ] ) * ( rpercent.test( offsets[ 1 ] ) ? height / 100 : 1 ) + ]; +} + +function parseCss( element, property ) { + return parseInt( $.css( element, property ), 10 ) || 0; +} + +function getDimensions( elem ) { + var raw = elem[ 0 ]; + if ( raw.nodeType === 9 ) { + return { + width: elem.width(), + height: elem.height(), + offset: { top: 0, left: 0 } + }; + } + if ( $.isWindow( raw ) ) { + return { + width: elem.width(), + height: elem.height(), + offset: { top: elem.scrollTop(), left: elem.scrollLeft() } + }; + } + if ( raw.preventDefault ) { + return { + width: 0, + height: 0, + offset: { top: raw.pageY, left: raw.pageX } + }; + } + return { + width: elem.outerWidth(), + height: elem.outerHeight(), + offset: elem.offset() + }; +} + +$.position = { + scrollbarWidth: function() { + if ( cachedScrollbarWidth !== undefined ) { + return cachedScrollbarWidth; + } + var w1, w2, + div = $( "
" + + "
" ), + innerDiv = div.children()[ 0 ]; + + $( "body" ).append( div ); + w1 = innerDiv.offsetWidth; + div.css( "overflow", "scroll" ); + + w2 = innerDiv.offsetWidth; + + if ( w1 === w2 ) { + w2 = div[ 0 ].clientWidth; + } + + div.remove(); + + return ( cachedScrollbarWidth = w1 - w2 ); + }, + getScrollInfo: function( within ) { + var overflowX = within.isWindow || within.isDocument ? "" : + within.element.css( "overflow-x" ), + overflowY = within.isWindow || within.isDocument ? "" : + within.element.css( "overflow-y" ), + hasOverflowX = overflowX === "scroll" || + ( overflowX === "auto" && within.width < within.element[ 0 ].scrollWidth ), + hasOverflowY = overflowY === "scroll" || + ( overflowY === "auto" && within.height < within.element[ 0 ].scrollHeight ); + return { + width: hasOverflowY ? $.position.scrollbarWidth() : 0, + height: hasOverflowX ? $.position.scrollbarWidth() : 0 + }; + }, + getWithinInfo: function( element ) { + var withinElement = $( element || window ), + isWindow = $.isWindow( withinElement[ 0 ] ), + isDocument = !!withinElement[ 0 ] && withinElement[ 0 ].nodeType === 9, + hasOffset = !isWindow && !isDocument; + return { + element: withinElement, + isWindow: isWindow, + isDocument: isDocument, + offset: hasOffset ? $( element ).offset() : { left: 0, top: 0 }, + scrollLeft: withinElement.scrollLeft(), + scrollTop: withinElement.scrollTop(), + width: withinElement.outerWidth(), + height: withinElement.outerHeight() + }; + } +}; + +$.fn.position = function( options ) { + if ( !options || !options.of ) { + return _position.apply( this, arguments ); + } + + // Make a copy, we don't want to modify arguments + options = $.extend( {}, options ); + + var atOffset, targetWidth, targetHeight, targetOffset, basePosition, dimensions, + target = $( options.of ), + within = $.position.getWithinInfo( options.within ), + scrollInfo = $.position.getScrollInfo( within ), + collision = ( options.collision || "flip" ).split( " " ), + offsets = {}; + + dimensions = getDimensions( target ); + if ( target[ 0 ].preventDefault ) { + + // Force left top to allow flipping + options.at = "left top"; + } + targetWidth = dimensions.width; + targetHeight = dimensions.height; + targetOffset = dimensions.offset; + + // Clone to reuse original targetOffset later + basePosition = $.extend( {}, targetOffset ); + + // Force my and at to have valid horizontal and vertical positions + // if a value is missing or invalid, it will be converted to center + $.each( [ "my", "at" ], function() { + var pos = ( options[ this ] || "" ).split( " " ), + horizontalOffset, + verticalOffset; + + if ( pos.length === 1 ) { + pos = rhorizontal.test( pos[ 0 ] ) ? + pos.concat( [ "center" ] ) : + rvertical.test( pos[ 0 ] ) ? + [ "center" ].concat( pos ) : + [ "center", "center" ]; + } + pos[ 0 ] = rhorizontal.test( pos[ 0 ] ) ? pos[ 0 ] : "center"; + pos[ 1 ] = rvertical.test( pos[ 1 ] ) ? pos[ 1 ] : "center"; + + // Calculate offsets + horizontalOffset = roffset.exec( pos[ 0 ] ); + verticalOffset = roffset.exec( pos[ 1 ] ); + offsets[ this ] = [ + horizontalOffset ? horizontalOffset[ 0 ] : 0, + verticalOffset ? verticalOffset[ 0 ] : 0 + ]; + + // Reduce to just the positions without the offsets + options[ this ] = [ + rposition.exec( pos[ 0 ] )[ 0 ], + rposition.exec( pos[ 1 ] )[ 0 ] + ]; + } ); + + // Normalize collision option + if ( collision.length === 1 ) { + collision[ 1 ] = collision[ 0 ]; + } + + if ( options.at[ 0 ] === "right" ) { + basePosition.left += targetWidth; + } else if ( options.at[ 0 ] === "center" ) { + basePosition.left += targetWidth / 2; + } + + if ( options.at[ 1 ] === "bottom" ) { + basePosition.top += targetHeight; + } else if ( options.at[ 1 ] === "center" ) { + basePosition.top += targetHeight / 2; + } + + atOffset = getOffsets( offsets.at, targetWidth, targetHeight ); + basePosition.left += atOffset[ 0 ]; + basePosition.top += atOffset[ 1 ]; + + return this.each( function() { + var collisionPosition, using, + elem = $( this ), + elemWidth = elem.outerWidth(), + elemHeight = elem.outerHeight(), + marginLeft = parseCss( this, "marginLeft" ), + marginTop = parseCss( this, "marginTop" ), + collisionWidth = elemWidth + marginLeft + parseCss( this, "marginRight" ) + + scrollInfo.width, + collisionHeight = elemHeight + marginTop + parseCss( this, "marginBottom" ) + + scrollInfo.height, + position = $.extend( {}, basePosition ), + myOffset = getOffsets( offsets.my, elem.outerWidth(), elem.outerHeight() ); + + if ( options.my[ 0 ] === "right" ) { + position.left -= elemWidth; + } else if ( options.my[ 0 ] === "center" ) { + position.left -= elemWidth / 2; + } + + if ( options.my[ 1 ] === "bottom" ) { + position.top -= elemHeight; + } else if ( options.my[ 1 ] === "center" ) { + position.top -= elemHeight / 2; + } + + position.left += myOffset[ 0 ]; + position.top += myOffset[ 1 ]; + + collisionPosition = { + marginLeft: marginLeft, + marginTop: marginTop + }; + + $.each( [ "left", "top" ], function( i, dir ) { + if ( $.ui.position[ collision[ i ] ] ) { + $.ui.position[ collision[ i ] ][ dir ]( position, { + targetWidth: targetWidth, + targetHeight: targetHeight, + elemWidth: elemWidth, + elemHeight: elemHeight, + collisionPosition: collisionPosition, + collisionWidth: collisionWidth, + collisionHeight: collisionHeight, + offset: [ atOffset[ 0 ] + myOffset[ 0 ], atOffset [ 1 ] + myOffset[ 1 ] ], + my: options.my, + at: options.at, + within: within, + elem: elem + } ); + } + } ); + + if ( options.using ) { + + // Adds feedback as second argument to using callback, if present + using = function( props ) { + var left = targetOffset.left - position.left, + right = left + targetWidth - elemWidth, + top = targetOffset.top - position.top, + bottom = top + targetHeight - elemHeight, + feedback = { + target: { + element: target, + left: targetOffset.left, + top: targetOffset.top, + width: targetWidth, + height: targetHeight + }, + element: { + element: elem, + left: position.left, + top: position.top, + width: elemWidth, + height: elemHeight + }, + horizontal: right < 0 ? "left" : left > 0 ? "right" : "center", + vertical: bottom < 0 ? "top" : top > 0 ? "bottom" : "middle" + }; + if ( targetWidth < elemWidth && abs( left + right ) < targetWidth ) { + feedback.horizontal = "center"; + } + if ( targetHeight < elemHeight && abs( top + bottom ) < targetHeight ) { + feedback.vertical = "middle"; + } + if ( max( abs( left ), abs( right ) ) > max( abs( top ), abs( bottom ) ) ) { + feedback.important = "horizontal"; + } else { + feedback.important = "vertical"; + } + options.using.call( this, props, feedback ); + }; + } + + elem.offset( $.extend( position, { using: using } ) ); + } ); +}; + +$.ui.position = { + fit: { + left: function( position, data ) { + var within = data.within, + withinOffset = within.isWindow ? within.scrollLeft : within.offset.left, + outerWidth = within.width, + collisionPosLeft = position.left - data.collisionPosition.marginLeft, + overLeft = withinOffset - collisionPosLeft, + overRight = collisionPosLeft + data.collisionWidth - outerWidth - withinOffset, + newOverRight; + + // Element is wider than within + if ( data.collisionWidth > outerWidth ) { + + // Element is initially over the left side of within + if ( overLeft > 0 && overRight <= 0 ) { + newOverRight = position.left + overLeft + data.collisionWidth - outerWidth - + withinOffset; + position.left += overLeft - newOverRight; + + // Element is initially over right side of within + } else if ( overRight > 0 && overLeft <= 0 ) { + position.left = withinOffset; + + // Element is initially over both left and right sides of within + } else { + if ( overLeft > overRight ) { + position.left = withinOffset + outerWidth - data.collisionWidth; + } else { + position.left = withinOffset; + } + } + + // Too far left -> align with left edge + } else if ( overLeft > 0 ) { + position.left += overLeft; + + // Too far right -> align with right edge + } else if ( overRight > 0 ) { + position.left -= overRight; + + // Adjust based on position and margin + } else { + position.left = max( position.left - collisionPosLeft, position.left ); + } + }, + top: function( position, data ) { + var within = data.within, + withinOffset = within.isWindow ? within.scrollTop : within.offset.top, + outerHeight = data.within.height, + collisionPosTop = position.top - data.collisionPosition.marginTop, + overTop = withinOffset - collisionPosTop, + overBottom = collisionPosTop + data.collisionHeight - outerHeight - withinOffset, + newOverBottom; + + // Element is taller than within + if ( data.collisionHeight > outerHeight ) { + + // Element is initially over the top of within + if ( overTop > 0 && overBottom <= 0 ) { + newOverBottom = position.top + overTop + data.collisionHeight - outerHeight - + withinOffset; + position.top += overTop - newOverBottom; + + // Element is initially over bottom of within + } else if ( overBottom > 0 && overTop <= 0 ) { + position.top = withinOffset; + + // Element is initially over both top and bottom of within + } else { + if ( overTop > overBottom ) { + position.top = withinOffset + outerHeight - data.collisionHeight; + } else { + position.top = withinOffset; + } + } + + // Too far up -> align with top + } else if ( overTop > 0 ) { + position.top += overTop; + + // Too far down -> align with bottom edge + } else if ( overBottom > 0 ) { + position.top -= overBottom; + + // Adjust based on position and margin + } else { + position.top = max( position.top - collisionPosTop, position.top ); + } + } + }, + flip: { + left: function( position, data ) { + var within = data.within, + withinOffset = within.offset.left + within.scrollLeft, + outerWidth = within.width, + offsetLeft = within.isWindow ? within.scrollLeft : within.offset.left, + collisionPosLeft = position.left - data.collisionPosition.marginLeft, + overLeft = collisionPosLeft - offsetLeft, + overRight = collisionPosLeft + data.collisionWidth - outerWidth - offsetLeft, + myOffset = data.my[ 0 ] === "left" ? + -data.elemWidth : + data.my[ 0 ] === "right" ? + data.elemWidth : + 0, + atOffset = data.at[ 0 ] === "left" ? + data.targetWidth : + data.at[ 0 ] === "right" ? + -data.targetWidth : + 0, + offset = -2 * data.offset[ 0 ], + newOverRight, + newOverLeft; + + if ( overLeft < 0 ) { + newOverRight = position.left + myOffset + atOffset + offset + data.collisionWidth - + outerWidth - withinOffset; + if ( newOverRight < 0 || newOverRight < abs( overLeft ) ) { + position.left += myOffset + atOffset + offset; + } + } else if ( overRight > 0 ) { + newOverLeft = position.left - data.collisionPosition.marginLeft + myOffset + + atOffset + offset - offsetLeft; + if ( newOverLeft > 0 || abs( newOverLeft ) < overRight ) { + position.left += myOffset + atOffset + offset; + } + } + }, + top: function( position, data ) { + var within = data.within, + withinOffset = within.offset.top + within.scrollTop, + outerHeight = within.height, + offsetTop = within.isWindow ? within.scrollTop : within.offset.top, + collisionPosTop = position.top - data.collisionPosition.marginTop, + overTop = collisionPosTop - offsetTop, + overBottom = collisionPosTop + data.collisionHeight - outerHeight - offsetTop, + top = data.my[ 1 ] === "top", + myOffset = top ? + -data.elemHeight : + data.my[ 1 ] === "bottom" ? + data.elemHeight : + 0, + atOffset = data.at[ 1 ] === "top" ? + data.targetHeight : + data.at[ 1 ] === "bottom" ? + -data.targetHeight : + 0, + offset = -2 * data.offset[ 1 ], + newOverTop, + newOverBottom; + if ( overTop < 0 ) { + newOverBottom = position.top + myOffset + atOffset + offset + data.collisionHeight - + outerHeight - withinOffset; + if ( newOverBottom < 0 || newOverBottom < abs( overTop ) ) { + position.top += myOffset + atOffset + offset; + } + } else if ( overBottom > 0 ) { + newOverTop = position.top - data.collisionPosition.marginTop + myOffset + atOffset + + offset - offsetTop; + if ( newOverTop > 0 || abs( newOverTop ) < overBottom ) { + position.top += myOffset + atOffset + offset; + } + } + } + }, + flipfit: { + left: function() { + $.ui.position.flip.left.apply( this, arguments ); + $.ui.position.fit.left.apply( this, arguments ); + }, + top: function() { + $.ui.position.flip.top.apply( this, arguments ); + $.ui.position.fit.top.apply( this, arguments ); + } + } +}; + +} )(); + +var position = $.ui.position; + + +/*! + * jQuery UI :data 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: :data Selector +//>>group: Core +//>>description: Selects elements which have data stored under the specified key. +//>>docs: http://api.jqueryui.com/data-selector/ + + +var data = $.extend( $.expr[ ":" ], { + data: $.expr.createPseudo ? + $.expr.createPseudo( function( dataName ) { + return function( elem ) { + return !!$.data( elem, dataName ); + }; + } ) : + + // Support: jQuery <1.8 + function( elem, i, match ) { + return !!$.data( elem, match[ 3 ] ); + } +} ); + +/*! + * jQuery UI Disable Selection 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: disableSelection +//>>group: Core +//>>description: Disable selection of text content within the set of matched elements. +//>>docs: http://api.jqueryui.com/disableSelection/ + +// This file is deprecated + + +var disableSelection = $.fn.extend( { + disableSelection: ( function() { + var eventType = "onselectstart" in document.createElement( "div" ) ? + "selectstart" : + "mousedown"; + + return function() { + return this.on( eventType + ".ui-disableSelection", function( event ) { + event.preventDefault(); + } ); + }; + } )(), + + enableSelection: function() { + return this.off( ".ui-disableSelection" ); + } +} ); + + +/*! + * jQuery UI Focusable 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: :focusable Selector +//>>group: Core +//>>description: Selects elements which can be focused. +//>>docs: http://api.jqueryui.com/focusable-selector/ + + + +// Selectors +$.ui.focusable = function( element, hasTabindex ) { + var map, mapName, img, focusableIfVisible, fieldset, + nodeName = element.nodeName.toLowerCase(); + + if ( "area" === nodeName ) { + map = element.parentNode; + mapName = map.name; + if ( !element.href || !mapName || map.nodeName.toLowerCase() !== "map" ) { + return false; + } + img = $( "img[usemap='#" + mapName + "']" ); + return img.length > 0 && img.is( ":visible" ); + } + + if ( /^(input|select|textarea|button|object)$/.test( nodeName ) ) { + focusableIfVisible = !element.disabled; + + if ( focusableIfVisible ) { + + // Form controls within a disabled fieldset are disabled. + // However, controls within the fieldset's legend do not get disabled. + // Since controls generally aren't placed inside legends, we skip + // this portion of the check. + fieldset = $( element ).closest( "fieldset" )[ 0 ]; + if ( fieldset ) { + focusableIfVisible = !fieldset.disabled; + } + } + } else if ( "a" === nodeName ) { + focusableIfVisible = element.href || hasTabindex; + } else { + focusableIfVisible = hasTabindex; + } + + return focusableIfVisible && $( element ).is( ":visible" ) && visible( $( element ) ); +}; + +// Support: IE 8 only +// IE 8 doesn't resolve inherit to visible/hidden for computed values +function visible( element ) { + var visibility = element.css( "visibility" ); + while ( visibility === "inherit" ) { + element = element.parent(); + visibility = element.css( "visibility" ); + } + return visibility !== "hidden"; +} + +$.extend( $.expr[ ":" ], { + focusable: function( element ) { + return $.ui.focusable( element, $.attr( element, "tabindex" ) != null ); + } +} ); + +var focusable = $.ui.focusable; + + + + +// Support: IE8 Only +// IE8 does not support the form attribute and when it is supplied. It overwrites the form prop +// with a string, so we need to find the proper form. +var form = $.fn.form = function() { + return typeof this[ 0 ].form === "string" ? this.closest( "form" ) : $( this[ 0 ].form ); +}; + + +/*! + * jQuery UI Form Reset Mixin 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: Form Reset Mixin +//>>group: Core +//>>description: Refresh input widgets when their form is reset +//>>docs: http://api.jqueryui.com/form-reset-mixin/ + + + +var formResetMixin = $.ui.formResetMixin = { + _formResetHandler: function() { + var form = $( this ); + + // Wait for the form reset to actually happen before refreshing + setTimeout( function() { + var instances = form.data( "ui-form-reset-instances" ); + $.each( instances, function() { + this.refresh(); + } ); + } ); + }, + + _bindFormResetHandler: function() { + this.form = this.element.form(); + if ( !this.form.length ) { + return; + } + + var instances = this.form.data( "ui-form-reset-instances" ) || []; + if ( !instances.length ) { + + // We don't use _on() here because we use a single event handler per form + this.form.on( "reset.ui-form-reset", this._formResetHandler ); + } + instances.push( this ); + this.form.data( "ui-form-reset-instances", instances ); + }, + + _unbindFormResetHandler: function() { + if ( !this.form.length ) { + return; + } + + var instances = this.form.data( "ui-form-reset-instances" ); + instances.splice( $.inArray( this, instances ), 1 ); + if ( instances.length ) { + this.form.data( "ui-form-reset-instances", instances ); + } else { + this.form + .removeData( "ui-form-reset-instances" ) + .off( "reset.ui-form-reset" ); + } + } +}; + + +/*! + * jQuery UI Support for jQuery core 1.7.x 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + */ + +//>>label: jQuery 1.7 Support +//>>group: Core +//>>description: Support version 1.7.x of jQuery core + + + +// Support: jQuery 1.7 only +// Not a great way to check versions, but since we only support 1.7+ and only +// need to detect <1.8, this is a simple check that should suffice. Checking +// for "1.7." would be a bit safer, but the version string is 1.7, not 1.7.0 +// and we'll never reach 1.70.0 (if we do, we certainly won't be supporting +// 1.7 anymore). See #11197 for why we're not using feature detection. +if ( $.fn.jquery.substring( 0, 3 ) === "1.7" ) { + + // Setters for .innerWidth(), .innerHeight(), .outerWidth(), .outerHeight() + // Unlike jQuery Core 1.8+, these only support numeric values to set the + // dimensions in pixels + $.each( [ "Width", "Height" ], function( i, name ) { + var side = name === "Width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ], + type = name.toLowerCase(), + orig = { + innerWidth: $.fn.innerWidth, + innerHeight: $.fn.innerHeight, + outerWidth: $.fn.outerWidth, + outerHeight: $.fn.outerHeight + }; + + function reduce( elem, size, border, margin ) { + $.each( side, function() { + size -= parseFloat( $.css( elem, "padding" + this ) ) || 0; + if ( border ) { + size -= parseFloat( $.css( elem, "border" + this + "Width" ) ) || 0; + } + if ( margin ) { + size -= parseFloat( $.css( elem, "margin" + this ) ) || 0; + } + } ); + return size; + } + + $.fn[ "inner" + name ] = function( size ) { + if ( size === undefined ) { + return orig[ "inner" + name ].call( this ); + } + + return this.each( function() { + $( this ).css( type, reduce( this, size ) + "px" ); + } ); + }; + + $.fn[ "outer" + name ] = function( size, margin ) { + if ( typeof size !== "number" ) { + return orig[ "outer" + name ].call( this, size ); + } + + return this.each( function() { + $( this ).css( type, reduce( this, size, true, margin ) + "px" ); + } ); + }; + } ); + + $.fn.addBack = function( selector ) { + return this.add( selector == null ? + this.prevObject : this.prevObject.filter( selector ) + ); + }; +} + +; +/*! + * jQuery UI Keycode 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: Keycode +//>>group: Core +//>>description: Provide keycodes as keynames +//>>docs: http://api.jqueryui.com/jQuery.ui.keyCode/ + + +var keycode = $.ui.keyCode = { + BACKSPACE: 8, + COMMA: 188, + DELETE: 46, + DOWN: 40, + END: 35, + ENTER: 13, + ESCAPE: 27, + HOME: 36, + LEFT: 37, + PAGE_DOWN: 34, + PAGE_UP: 33, + PERIOD: 190, + RIGHT: 39, + SPACE: 32, + TAB: 9, + UP: 38 +}; + + + + +// Internal use only +var escapeSelector = $.ui.escapeSelector = ( function() { + var selectorEscape = /([!"#$%&'()*+,./:;<=>?@[\]^`{|}~])/g; + return function( selector ) { + return selector.replace( selectorEscape, "\\$1" ); + }; +} )(); + + +/*! + * jQuery UI Labels 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: labels +//>>group: Core +//>>description: Find all the labels associated with a given input +//>>docs: http://api.jqueryui.com/labels/ + + + +var labels = $.fn.labels = function() { + var ancestor, selector, id, labels, ancestors; + + // Check control.labels first + if ( this[ 0 ].labels && this[ 0 ].labels.length ) { + return this.pushStack( this[ 0 ].labels ); + } + + // Support: IE <= 11, FF <= 37, Android <= 2.3 only + // Above browsers do not support control.labels. Everything below is to support them + // as well as document fragments. control.labels does not work on document fragments + labels = this.eq( 0 ).parents( "label" ); + + // Look for the label based on the id + id = this.attr( "id" ); + if ( id ) { + + // We don't search against the document in case the element + // is disconnected from the DOM + ancestor = this.eq( 0 ).parents().last(); + + // Get a full set of top level ancestors + ancestors = ancestor.add( ancestor.length ? ancestor.siblings() : this.siblings() ); + + // Create a selector for the label based on the id + selector = "label[for='" + $.ui.escapeSelector( id ) + "']"; + + labels = labels.add( ancestors.find( selector ).addBack( selector ) ); + + } + + // Return whatever we have found for labels + return this.pushStack( labels ); +}; + + +/*! + * jQuery UI Scroll Parent 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: scrollParent +//>>group: Core +//>>description: Get the closest ancestor element that is scrollable. +//>>docs: http://api.jqueryui.com/scrollParent/ + + + +var scrollParent = $.fn.scrollParent = function( includeHidden ) { + var position = this.css( "position" ), + excludeStaticParent = position === "absolute", + overflowRegex = includeHidden ? /(auto|scroll|hidden)/ : /(auto|scroll)/, + scrollParent = this.parents().filter( function() { + var parent = $( this ); + if ( excludeStaticParent && parent.css( "position" ) === "static" ) { + return false; + } + return overflowRegex.test( parent.css( "overflow" ) + parent.css( "overflow-y" ) + + parent.css( "overflow-x" ) ); + } ).eq( 0 ); + + return position === "fixed" || !scrollParent.length ? + $( this[ 0 ].ownerDocument || document ) : + scrollParent; +}; + + +/*! + * jQuery UI Tabbable 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: :tabbable Selector +//>>group: Core +//>>description: Selects elements which can be tabbed to. +//>>docs: http://api.jqueryui.com/tabbable-selector/ + + + +var tabbable = $.extend( $.expr[ ":" ], { + tabbable: function( element ) { + var tabIndex = $.attr( element, "tabindex" ), + hasTabindex = tabIndex != null; + return ( !hasTabindex || tabIndex >= 0 ) && $.ui.focusable( element, hasTabindex ); + } +} ); + + +/*! + * jQuery UI Unique ID 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: uniqueId +//>>group: Core +//>>description: Functions to generate and remove uniqueId's +//>>docs: http://api.jqueryui.com/uniqueId/ + + + +var uniqueId = $.fn.extend( { + uniqueId: ( function() { + var uuid = 0; + + return function() { + return this.each( function() { + if ( !this.id ) { + this.id = "ui-id-" + ( ++uuid ); + } + } ); + }; + } )(), + + removeUniqueId: function() { + return this.each( function() { + if ( /^ui-id-\d+$/.test( this.id ) ) { + $( this ).removeAttr( "id" ); + } + } ); + } +} ); + + + + +// This file is deprecated +var ie = $.ui.ie = !!/msie [\w.]+/.exec( navigator.userAgent.toLowerCase() ); + +/*! + * jQuery UI Mouse 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +//>>label: Mouse +//>>group: Widgets +//>>description: Abstracts mouse-based interactions to assist in creating certain widgets. +//>>docs: http://api.jqueryui.com/mouse/ + + + +var mouseHandled = false; +$( document ).on( "mouseup", function() { + mouseHandled = false; +} ); + +var widgetsMouse = $.widget( "ui.mouse", { + version: "1.12.1", + options: { + cancel: "input, textarea, button, select, option", + distance: 1, + delay: 0 + }, + _mouseInit: function() { + var that = this; + + this.element + .on( "mousedown." + this.widgetName, function( event ) { + return that._mouseDown( event ); + } ) + .on( "click." + this.widgetName, function( event ) { + if ( true === $.data( event.target, that.widgetName + ".preventClickEvent" ) ) { + $.removeData( event.target, that.widgetName + ".preventClickEvent" ); + event.stopImmediatePropagation(); + return false; + } + } ); + + this.started = false; + }, + + // TODO: make sure destroying one instance of mouse doesn't mess with + // other instances of mouse + _mouseDestroy: function() { + this.element.off( "." + this.widgetName ); + if ( this._mouseMoveDelegate ) { + this.document + .off( "mousemove." + this.widgetName, this._mouseMoveDelegate ) + .off( "mouseup." + this.widgetName, this._mouseUpDelegate ); + } + }, + + _mouseDown: function( event ) { + + // don't let more than one widget handle mouseStart + if ( mouseHandled ) { + return; + } + + this._mouseMoved = false; + + // We may have missed mouseup (out of window) + ( this._mouseStarted && this._mouseUp( event ) ); + + this._mouseDownEvent = event; + + var that = this, + btnIsLeft = ( event.which === 1 ), + + // event.target.nodeName works around a bug in IE 8 with + // disabled inputs (#7620) + elIsCancel = ( typeof this.options.cancel === "string" && event.target.nodeName ? + $( event.target ).closest( this.options.cancel ).length : false ); + if ( !btnIsLeft || elIsCancel || !this._mouseCapture( event ) ) { + return true; + } + + this.mouseDelayMet = !this.options.delay; + if ( !this.mouseDelayMet ) { + this._mouseDelayTimer = setTimeout( function() { + that.mouseDelayMet = true; + }, this.options.delay ); + } + + if ( this._mouseDistanceMet( event ) && this._mouseDelayMet( event ) ) { + this._mouseStarted = ( this._mouseStart( event ) !== false ); + if ( !this._mouseStarted ) { + event.preventDefault(); + return true; + } + } + + // Click event may never have fired (Gecko & Opera) + if ( true === $.data( event.target, this.widgetName + ".preventClickEvent" ) ) { + $.removeData( event.target, this.widgetName + ".preventClickEvent" ); + } + + // These delegates are required to keep context + this._mouseMoveDelegate = function( event ) { + return that._mouseMove( event ); + }; + this._mouseUpDelegate = function( event ) { + return that._mouseUp( event ); + }; + + this.document + .on( "mousemove." + this.widgetName, this._mouseMoveDelegate ) + .on( "mouseup." + this.widgetName, this._mouseUpDelegate ); + + event.preventDefault(); + + mouseHandled = true; + return true; + }, + + _mouseMove: function( event ) { + + // Only check for mouseups outside the document if you've moved inside the document + // at least once. This prevents the firing of mouseup in the case of IE<9, which will + // fire a mousemove event if content is placed under the cursor. See #7778 + // Support: IE <9 + if ( this._mouseMoved ) { + + // IE mouseup check - mouseup happened when mouse was out of window + if ( $.ui.ie && ( !document.documentMode || document.documentMode < 9 ) && + !event.button ) { + return this._mouseUp( event ); + + // Iframe mouseup check - mouseup occurred in another document + } else if ( !event.which ) { + + // Support: Safari <=8 - 9 + // Safari sets which to 0 if you press any of the following keys + // during a drag (#14461) + if ( event.originalEvent.altKey || event.originalEvent.ctrlKey || + event.originalEvent.metaKey || event.originalEvent.shiftKey ) { + this.ignoreMissingWhich = true; + } else if ( !this.ignoreMissingWhich ) { + return this._mouseUp( event ); + } + } + } + + if ( event.which || event.button ) { + this._mouseMoved = true; + } + + if ( this._mouseStarted ) { + this._mouseDrag( event ); + return event.preventDefault(); + } + + if ( this._mouseDistanceMet( event ) && this._mouseDelayMet( event ) ) { + this._mouseStarted = + ( this._mouseStart( this._mouseDownEvent, event ) !== false ); + ( this._mouseStarted ? this._mouseDrag( event ) : this._mouseUp( event ) ); + } + + return !this._mouseStarted; + }, + + _mouseUp: function( event ) { + this.document + .off( "mousemove." + this.widgetName, this._mouseMoveDelegate ) + .off( "mouseup." + this.widgetName, this._mouseUpDelegate ); + + if ( this._mouseStarted ) { + this._mouseStarted = false; + + if ( event.target === this._mouseDownEvent.target ) { + $.data( event.target, this.widgetName + ".preventClickEvent", true ); + } + + this._mouseStop( event ); + } + + if ( this._mouseDelayTimer ) { + clearTimeout( this._mouseDelayTimer ); + delete this._mouseDelayTimer; + } + + this.ignoreMissingWhich = false; + mouseHandled = false; + event.preventDefault(); + }, + + _mouseDistanceMet: function( event ) { + return ( Math.max( + Math.abs( this._mouseDownEvent.pageX - event.pageX ), + Math.abs( this._mouseDownEvent.pageY - event.pageY ) + ) >= this.options.distance + ); + }, + + _mouseDelayMet: function( /* event */ ) { + return this.mouseDelayMet; + }, + + // These are placeholder methods, to be overriden by extending plugin + _mouseStart: function( /* event */ ) {}, + _mouseDrag: function( /* event */ ) {}, + _mouseStop: function( /* event */ ) {}, + _mouseCapture: function( /* event */ ) { return true; } +} ); + + + + +// $.ui.plugin is deprecated. Use $.widget() extensions instead. +var plugin = $.ui.plugin = { + add: function( module, option, set ) { + var i, + proto = $.ui[ module ].prototype; + for ( i in set ) { + proto.plugins[ i ] = proto.plugins[ i ] || []; + proto.plugins[ i ].push( [ option, set[ i ] ] ); + } + }, + call: function( instance, name, args, allowDisconnected ) { + var i, + set = instance.plugins[ name ]; + + if ( !set ) { + return; + } + + if ( !allowDisconnected && ( !instance.element[ 0 ].parentNode || + instance.element[ 0 ].parentNode.nodeType === 11 ) ) { + return; + } + + for ( i = 0; i < set.length; i++ ) { + if ( instance.options[ set[ i ][ 0 ] ] ) { + set[ i ][ 1 ].apply( instance.element, args ); + } + } + } +}; + + + +var safeActiveElement = $.ui.safeActiveElement = function( document ) { + var activeElement; + + // Support: IE 9 only + // IE9 throws an "Unspecified error" accessing document.activeElement from an
",{class: "idx", text: "#"}).appendTo(tx); + $("",{class: "", text: langPack.core.iface.users.regCode}).appendTo(tx); + $("",{class: "", text: langPack.core.iface.users.email}).appendTo(tx); + $("",{class: "", text: langPack.core.iface.users.allowMultiUseQTitle}).appendTo(tx); + $("",{class: "", text: langPack.core.iface.users.invitationExpire}).appendTo(tx); + $("",{class: "", text: langPack.core.iface.users.inviteToGroupTitleQ}).appendTo(tx); + $("",{class: "button", style: "text-align: center", append: $("",{class: "fas fa-ellipsis-h"})}).appendTo(tx); + tx.appendTo("div.widget.invites"); + + let i=0; + $.each(json.data, function(idx, user) { + i++; + let row=$("
",{class: "idx", text: i}).appendTo(row); + $("",{class: "code", text: formatRegCode(user.regCode)}).appendTo(row); + $("",{class: "", text: user.eMail}).appendTo(row); + $("",{class: "", text: user.allowMultiUse?langPack.core.iface.yes:langPack.core.iface.no}).appendTo(row); + $("",{class: "", text: UI.stamp2date(user.expireStamp,true)}).appendTo(row); + $("",{class: "", text: user.joinGroupsId.length>0?langPack.core.iface.yes:langPack.core.iface.no}).appendTo(row); + $("",{class: "button", style: "text-align: center", append: $("",{class: "fas fa-ellipsis-h"})}) + .click(uimContextMenuOpen) + .appendTo(row); + + $("