diff --git a/LICENSE b/LICENSE deleted file mode 100644 index f288702..0000000 --- a/LICENSE +++ /dev/null @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. diff --git a/README.md b/README.md deleted file mode 100644 index 75b22a2..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# wptag -文派茶馆 🏷 (WPTAG)网站统计及营销活动追踪代码 HTML 标签嵌入埋点管理器 diff --git a/admin/class-wptag-admin-controller.php b/admin/class-wptag-admin-controller.php deleted file mode 100644 index db89a6f..0000000 --- a/admin/class-wptag-admin-controller.php +++ /dev/null @@ -1,301 +0,0 @@ -snippet_manager = $snippet_manager; - $this->template_manager = $template_manager; - - add_action('admin_menu', [$this, 'add_admin_menu']); - add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_assets']); - add_action('admin_init', [$this, 'handle_form_submissions']); - add_action('admin_notices', [$this, 'display_admin_notices']); - } - - public function add_admin_menu() { - $capability = 'manage_options'; - - $this->page_hook_suffix['main'] = add_menu_page( - __('WPTAG', 'wptag'), - __('WPTAG', 'wptag'), - $capability, - 'wptag', - [$this, 'render_dashboard_page'], - 'dashicons-code-standards', - 85 - ); - - $this->page_hook_suffix['dashboard'] = add_submenu_page( - 'wptag', - __('Dashboard', 'wptag'), - __('Dashboard', 'wptag'), - $capability, - 'wptag', - [$this, 'render_dashboard_page'] - ); - - $this->page_hook_suffix['snippets'] = add_submenu_page( - 'wptag', - __('Code Snippets', 'wptag'), - __('Code Snippets', 'wptag'), - $capability, - 'wptag-snippets', - [$this, 'render_snippets_page'] - ); - - $this->page_hook_suffix['templates'] = add_submenu_page( - 'wptag', - __('Service Templates', 'wptag'), - __('Service Templates', 'wptag'), - $capability, - 'wptag-templates', - [$this, 'render_templates_page'] - ); - - $this->page_hook_suffix['settings'] = add_submenu_page( - 'wptag', - __('Settings', 'wptag'), - __('Settings', 'wptag'), - $capability, - 'wptag-settings', - [$this, 'render_settings_page'] - ); - } - - public function enqueue_admin_assets($hook) { - if (!in_array($hook, $this->page_hook_suffix)) { - return; - } - - wp_enqueue_style( - 'wptag-admin', - WPTAG_PLUGIN_URL . 'assets/css/admin.css', - ['wp-components'], - WPTAG_VERSION - ); - - wp_enqueue_script( - 'wptag-admin', - WPTAG_PLUGIN_URL . 'assets/js/admin.js', - ['jquery', 'wp-api', 'wp-i18n', 'wp-components', 'wp-element'], - WPTAG_VERSION, - true - ); - - wp_localize_script('wptag-admin', 'wptagAdmin', [ - 'ajaxUrl' => admin_url('admin-ajax.php'), - 'nonce' => wp_create_nonce('wptag_admin'), - 'strings' => [ - 'confirmDelete' => __('Are you sure you want to delete this snippet?', 'wptag'), - 'saved' => __('Settings saved successfully.', 'wptag'), - 'error' => __('An error occurred. Please try again.', 'wptag') - ] - ]); - - if (isset($_GET['action']) && ($_GET['action'] === 'new' || $_GET['action'] === 'edit')) { - wp_enqueue_code_editor(['type' => 'text/html']); - } - } - - public function render_dashboard_page() { - require_once WPTAG_PLUGIN_DIR . 'admin/partials/dashboard.php'; - } - - public function render_snippets_page() { - require_once WPTAG_PLUGIN_DIR . 'admin/partials/snippets.php'; - } - - public function render_templates_page() { - require_once WPTAG_PLUGIN_DIR . 'admin/partials/templates.php'; - } - - public function render_settings_page() { - require_once WPTAG_PLUGIN_DIR . 'admin/partials/settings.php'; - } - - public function handle_form_submissions() { - if (!isset($_POST['wptag_action'])) { - return; - } - - $action = sanitize_key($_POST['wptag_action']); - - if (!wp_verify_nonce($_POST['_wpnonce'], 'wptag_' . $action)) { - wp_die(__('Security check failed', 'wptag')); - } - - if (!current_user_can('manage_options')) { - wp_die(__('Permission denied', 'wptag')); - } - - switch ($action) { - case 'create_snippet': - $this->handle_create_snippet(); - break; - - case 'update_snippet': - $this->handle_update_snippet(); - break; - - case 'delete_snippet': - $this->handle_delete_snippet(); - break; - - case 'toggle_snippet': - $this->handle_toggle_snippet(); - break; - - case 'save_settings': - $this->handle_save_settings(); - break; - } - } - - private function handle_create_snippet() { - $data = [ - 'name' => $_POST['name'] ?? '', - 'description' => $_POST['description'] ?? '', - 'code' => $_POST['code'] ?? '', - 'code_type' => $_POST['code_type'] ?? 'html', - 'position' => $_POST['position'] ?? 'head', - 'category' => $_POST['category'] ?? 'custom', - 'priority' => $_POST['priority'] ?? 10, - 'status' => isset($_POST['status']) ? 1 : 0, - 'device_type' => $_POST['device_type'] ?? 'all', - 'load_method' => $_POST['load_method'] ?? 'normal', - 'conditions' => $this->parse_conditions($_POST['conditions'] ?? []) - ]; - - $result = $this->snippet_manager->create_snippet($data); - - if (is_wp_error($result)) { - $this->add_admin_notice($result->get_error_message(), 'error'); - } else { - $this->add_admin_notice(__('Snippet created successfully', 'wptag'), 'success'); - wp_redirect(admin_url('admin.php?page=wptag-snippets')); - exit; - } - } - - private function handle_update_snippet() { - $id = intval($_POST['snippet_id'] ?? 0); - - if (!$id) { - $this->add_admin_notice(__('Invalid snippet ID', 'wptag'), 'error'); - return; - } - - $data = [ - 'name' => $_POST['name'] ?? '', - 'description' => $_POST['description'] ?? '', - 'code' => $_POST['code'] ?? '', - 'code_type' => $_POST['code_type'] ?? 'html', - 'position' => $_POST['position'] ?? 'head', - 'category' => $_POST['category'] ?? 'custom', - 'priority' => $_POST['priority'] ?? 10, - 'status' => isset($_POST['status']) ? 1 : 0, - 'device_type' => $_POST['device_type'] ?? 'all', - 'load_method' => $_POST['load_method'] ?? 'normal', - 'conditions' => $this->parse_conditions($_POST['conditions'] ?? []) - ]; - - $result = $this->snippet_manager->update_snippet($id, $data); - - if (is_wp_error($result)) { - $this->add_admin_notice($result->get_error_message(), 'error'); - } else { - $this->add_admin_notice(__('Snippet updated successfully', 'wptag'), 'success'); - } - } - - private function handle_delete_snippet() { - $id = intval($_GET['snippet_id'] ?? 0); - - if (!$id) { - $this->add_admin_notice(__('Invalid snippet ID', 'wptag'), 'error'); - return; - } - - $result = $this->snippet_manager->delete_snippet($id); - - if (is_wp_error($result)) { - $this->add_admin_notice($result->get_error_message(), 'error'); - } else { - $this->add_admin_notice(__('Snippet deleted successfully', 'wptag'), 'success'); - } - - wp_redirect(admin_url('admin.php?page=wptag-snippets')); - exit; - } - - private function handle_toggle_snippet() { - $id = intval($_GET['snippet_id'] ?? 0); - - if (!$id) { - wp_die(json_encode(['success' => false, 'message' => 'Invalid ID'])); - } - - $result = $this->snippet_manager->toggle_status($id); - - if (is_wp_error($result)) { - wp_die(json_encode(['success' => false, 'message' => $result->get_error_message()])); - } - - wp_die(json_encode(['success' => true, 'status' => $result])); - } - - private function handle_save_settings() { - $settings = [ - 'enable_cache' => isset($_POST['enable_cache']) ? 1 : 0, - 'cache_ttl' => intval($_POST['cache_ttl'] ?? 3600), - 'enable_debug' => isset($_POST['enable_debug']) ? 1 : 0, - 'cleanup_on_uninstall' => isset($_POST['cleanup_on_uninstall']) ? 1 : 0 - ]; - - update_option('wptag_settings', $settings); - - $this->add_admin_notice(__('Settings saved successfully', 'wptag'), 'success'); - - wp_redirect(admin_url('admin.php?page=wptag-settings')); - exit; - } - - private function parse_conditions($conditions_data) { - if (empty($conditions_data) || !is_array($conditions_data)) { - return null; - } - - return $conditions_data; - } - - private function add_admin_notice($message, $type = 'info') { - set_transient('wptag_admin_notice', [ - 'message' => $message, - 'type' => $type - ], 30); - } - - public function display_admin_notices() { - $notice = get_transient('wptag_admin_notice'); - - if (!$notice) { - return; - } - - delete_transient('wptag_admin_notice'); - - $class = 'notice notice-' . esc_attr($notice['type']); - printf( - '

%2$s

', - $class, - esc_html($notice['message']) - ); - } -} diff --git a/admin/class-wptag-admin-interface.php b/admin/class-wptag-admin-interface.php deleted file mode 100644 index e447fa2..0000000 --- a/admin/class-wptag-admin-interface.php +++ /dev/null @@ -1,195 +0,0 @@ -' . esc_html($label); - if ($required) { - echo ' *'; - } - echo ''; - } - - switch ($type) { - case 'text': - case 'email': - case 'url': - case 'number': - self::render_input_field($type, $name, $value, $required); - break; - - case 'textarea': - self::render_textarea_field($name, $value, $required); - break; - - case 'select': - self::render_select_field($name, $value, $options, $required); - break; - - case 'checkbox': - self::render_checkbox_field($name, $value, $label); - break; - - case 'radio': - self::render_radio_field($name, $value, $options); - break; - } - - if ($description) { - echo '

' . esc_html($description) . '

'; - } - } - - private static function render_input_field($type, $name, $value, $required) { - printf( - '', - esc_attr($type), - esc_attr($name), - esc_attr($name), - esc_attr($value), - $required ? 'required' : '' - ); - } - - private static function render_textarea_field($name, $value, $required) { - printf( - '', - esc_attr($name), - esc_attr($name), - $required ? 'required' : '', - esc_textarea($value) - ); - } - - private static function render_select_field($name, $value, $options, $required) { - printf( - ''; - } - - private static function render_checkbox_field($name, $value, $label) { - printf( - '', - esc_attr($name), - esc_attr($name), - checked($value, 1, false), - esc_html($label) - ); - } - - private static function render_radio_field($name, $value, $options) { - foreach ($options as $option_value => $option_label) { - printf( - '
', - esc_attr($name), - esc_attr($option_value), - checked($value, $option_value, false), - esc_html($option_label) - ); - } - } - - public static function render_modal($id, $title, $content, $footer = '') { - ?> - - 'notice-info', - 'success' => 'notice-success', - 'warning' => 'notice-warning', - 'error' => 'notice-error' - ]; - - $class = $classes[$type] ?? 'notice-info'; - - printf( - '

%s

', - esc_attr($class), - esc_html($message) - ); - } - - public static function render_tabs($tabs, $current_tab) { - echo ''; - } - - public static function render_action_buttons($actions) { - echo '
'; - - foreach ($actions as $action) { - $class = $action['primary'] ?? false ? 'button-primary' : 'button-secondary'; - $url = $action['url'] ?? '#'; - $onclick = $action['onclick'] ?? ''; - - printf( - '%s ', - esc_url($url), - esc_attr($class), - $onclick ? 'onclick="' . esc_attr($onclick) . '"' : '', - esc_html($action['label']) - ); - } - - echo '
'; - } -} diff --git a/admin/class-wptag-ajax-handler.php b/admin/class-wptag-ajax-handler.php deleted file mode 100644 index 8d3ddfe..0000000 --- a/admin/class-wptag-ajax-handler.php +++ /dev/null @@ -1,322 +0,0 @@ -snippet_manager = $snippet_manager; - $this->template_manager = $template_manager; - - $this->register_ajax_handlers(); - } - - private function register_ajax_handlers() { - $actions = [ - 'wptag_toggle_snippet', - 'wptag_delete_snippet', - 'wptag_search_snippets', - 'wptag_validate_code', - 'wptag_preview_snippet', - 'wptag_get_template', - 'wptag_process_template', - 'wptag_export_snippets', - 'wptag_import_snippets', - 'wptag_clear_cache' - ]; - - foreach ($actions as $action) { - add_action('wp_ajax_' . $action, [$this, 'handle_' . str_replace('wptag_', '', $action)]); - } - } - - public function handle_toggle_snippet() { - $this->verify_ajax_request(); - - $snippet_id = intval($_POST['snippet_id'] ?? 0); - - if (!$snippet_id) { - wp_send_json_error(['message' => __('Invalid snippet ID', 'wptag')]); - } - - $result = $this->snippet_manager->toggle_status($snippet_id); - - if (is_wp_error($result)) { - wp_send_json_error(['message' => $result->get_error_message()]); - } - - wp_send_json_success([ - 'status' => $result, - 'message' => $result ? __('Snippet enabled', 'wptag') : __('Snippet disabled', 'wptag') - ]); - } - - public function handle_delete_snippet() { - $this->verify_ajax_request(); - - $snippet_id = intval($_POST['snippet_id'] ?? 0); - - if (!$snippet_id) { - wp_send_json_error(['message' => __('Invalid snippet ID', 'wptag')]); - } - - $result = $this->snippet_manager->delete_snippet($snippet_id); - - if (is_wp_error($result)) { - wp_send_json_error(['message' => $result->get_error_message()]); - } - - wp_send_json_success(['message' => __('Snippet deleted successfully', 'wptag')]); - } - - public function handle_search_snippets() { - $this->verify_ajax_request(); - - $args = [ - 'search' => sanitize_text_field($_POST['search'] ?? ''), - 'category' => sanitize_key($_POST['category'] ?? ''), - 'position' => sanitize_key($_POST['position'] ?? ''), - 'status' => isset($_POST['status']) ? intval($_POST['status']) : null, - 'per_page' => intval($_POST['per_page'] ?? 20), - 'page' => intval($_POST['page'] ?? 1) - ]; - - $snippets = $this->snippet_manager->get_snippets($args); - - wp_send_json_success(['snippets' => $snippets]); - } - - public function handle_validate_code() { - $this->verify_ajax_request(); - - $code = $_POST['code'] ?? ''; - $code_type = sanitize_key($_POST['code_type'] ?? 'html'); - - $errors = []; - - switch ($code_type) { - case 'javascript': - $errors = $this->validate_javascript($code); - break; - - case 'css': - $errors = $this->validate_css($code); - break; - - case 'html': - $errors = $this->validate_html($code); - break; - } - - if (empty($errors)) { - wp_send_json_success(['message' => __('Code is valid', 'wptag')]); - } else { - wp_send_json_error(['errors' => $errors]); - } - } - - public function handle_preview_snippet() { - $this->verify_ajax_request(); - - $snippet_id = intval($_POST['snippet_id'] ?? 0); - - if (!$snippet_id) { - wp_send_json_error(['message' => __('Invalid snippet ID', 'wptag')]); - } - - $snippet = $this->snippet_manager->get_snippet($snippet_id); - - if (!$snippet) { - wp_send_json_error(['message' => __('Snippet not found', 'wptag')]); - } - - $preview_url = add_query_arg([ - 'wptag_preview' => 1, - 'snippet_id' => $snippet_id - ], home_url()); - - wp_send_json_success(['preview_url' => $preview_url]); - } - - public function handle_get_template() { - $this->verify_ajax_request(); - - $service_type = sanitize_key($_POST['service_type'] ?? ''); - - if (!$service_type) { - wp_send_json_error(['message' => __('Invalid service type', 'wptag')]); - } - - $template = $this->template_manager->get_template($service_type); - - if (!$template) { - wp_send_json_error(['message' => __('Template not found', 'wptag')]); - } - - wp_send_json_success(['template' => $template]); - } - - public function handle_process_template() { - $this->verify_ajax_request(); - - $service_type = sanitize_key($_POST['service_type'] ?? ''); - $config_data = $_POST['config'] ?? []; - - if (!$service_type) { - wp_send_json_error(['message' => __('Invalid service type', 'wptag')]); - } - - $result = $this->template_manager->process_template_config($service_type, $config_data); - - if (is_wp_error($result)) { - wp_send_json_error(['message' => $result->get_error_message()]); - } - - $snippet_data = array_merge($result, [ - 'description' => sprintf(__('Generated from %s template', 'wptag'), $result['name']), - 'status' => 1 - ]); - - $snippet_id = $this->snippet_manager->create_snippet($snippet_data); - - if (is_wp_error($snippet_id)) { - wp_send_json_error(['message' => $snippet_id->get_error_message()]); - } - - wp_send_json_success([ - 'snippet_id' => $snippet_id, - 'message' => __('Snippet created successfully from template', 'wptag') - ]); - } - - public function handle_export_snippets() { - $this->verify_ajax_request(); - - $snippet_ids = array_map('intval', $_POST['snippet_ids'] ?? []); - - $export_data = [ - 'version' => WPTAG_VERSION, - 'exported_at' => current_time('mysql'), - 'snippets' => [] - ]; - - foreach ($snippet_ids as $id) { - $snippet = $this->snippet_manager->get_snippet($id); - if ($snippet) { - unset($snippet['id'], $snippet['created_by'], $snippet['last_modified_by']); - $export_data['snippets'][] = $snippet; - } - } - - wp_send_json_success([ - 'filename' => 'wptag-export-' . date('Y-m-d') . '.json', - 'data' => json_encode($export_data, JSON_PRETTY_PRINT) - ]); - } - - public function handle_import_snippets() { - $this->verify_ajax_request(); - - $import_data = json_decode(stripslashes($_POST['import_data'] ?? ''), true); - - if (!$import_data || !isset($import_data['snippets'])) { - wp_send_json_error(['message' => __('Invalid import data', 'wptag')]); - } - - $imported = 0; - $errors = []; - - foreach ($import_data['snippets'] as $snippet) { - $result = $this->snippet_manager->create_snippet($snippet); - - if (is_wp_error($result)) { - $errors[] = sprintf( - __('Failed to import "%s": %s', 'wptag'), - $snippet['name'], - $result->get_error_message() - ); - } else { - $imported++; - } - } - - if ($imported > 0) { - $message = sprintf( - _n('%d snippet imported successfully', '%d snippets imported successfully', $imported, 'wptag'), - $imported - ); - - if (!empty($errors)) { - $message .= ' ' . __('Some snippets failed to import.', 'wptag'); - } - - wp_send_json_success([ - 'message' => $message, - 'imported' => $imported, - 'errors' => $errors - ]); - } else { - wp_send_json_error([ - 'message' => __('No snippets were imported', 'wptag'), - 'errors' => $errors - ]); - } - } - - public function handle_clear_cache() { - $this->verify_ajax_request(); - - $cache_manager = new WPTag_Cache_Manager(); - $cache_manager->flush(); - - wp_send_json_success(['message' => __('Cache cleared successfully', 'wptag')]); - } - - private function verify_ajax_request() { - if (!check_ajax_referer('wptag_admin', 'nonce', false)) { - wp_send_json_error(['message' => __('Security check failed', 'wptag')]); - } - - if (!current_user_can('manage_options')) { - wp_send_json_error(['message' => __('Permission denied', 'wptag')]); - } - } - - private function validate_javascript($code) { - $errors = []; - - if (preg_match('/\bdocument\.write\b/i', $code)) { - $errors[] = __('document.write is not recommended', 'wptag'); - } - - if (preg_match('/\beval\s*\(/i', $code)) { - $errors[] = __('eval() is potentially dangerous', 'wptag'); - } - - return $errors; - } - - private function validate_css($code) { - $errors = []; - - if (preg_match('/@import\s+url/i', $code)) { - $errors[] = __('@import may affect performance', 'wptag'); - } - - return $errors; - } - - private function validate_html($code) { - $errors = []; - - if (preg_match('/]*src=["\'](?!https?:\/\/)/i', $code)) { - $errors[] = __('Use absolute URLs for external scripts', 'wptag'); - } - - return $errors; - } -} diff --git a/admin/partials/dashboard.php b/admin/partials/dashboard.php deleted file mode 100644 index 41b9123..0000000 --- a/admin/partials/dashboard.php +++ /dev/null @@ -1,177 +0,0 @@ -prefix . 'wptag_snippets'; -$logs_table = $wpdb->prefix . 'wptag_logs'; - -$total_snippets = $wpdb->get_var("SELECT COUNT(*) FROM $snippets_table"); -$active_snippets = $wpdb->get_var("SELECT COUNT(*) FROM $snippets_table WHERE status = 1"); -$categories_count = $wpdb->get_results("SELECT category, COUNT(*) as count FROM $snippets_table GROUP BY category", ARRAY_A); - -$recent_activity = $wpdb->get_results( - "SELECT l.*, u.display_name - FROM $logs_table l - LEFT JOIN {$wpdb->users} u ON l.user_id = u.ID - ORDER BY l.created_at DESC - LIMIT 10", - ARRAY_A -); - -$categories = $this->snippet_manager->get_categories(); -?> - -
-
-

-
- -
-
-
-

-
-
- -
-

-
-
- -
-

-
-
- -
-

-
- 0 ? round(($active_snippets / $total_snippets) * 100) : 0; - echo $success_rate . '%'; - ?> -
-
-
- -
-
-

- - - - - - - - - - - - - - - - - - - - - -
-
- -
-

- - -

-
    -
  1. -
  2. -
  3. -
  4. -
-
-
- -

- - - - - - - - - - - - - - - - - - - - - - - - - -
- -
-
-
- - diff --git a/admin/partials/settings.php b/admin/partials/settings.php deleted file mode 100644 index cc2f324..0000000 --- a/admin/partials/settings.php +++ /dev/null @@ -1,245 +0,0 @@ - 1, - 'cache_ttl' => 3600, - 'enable_debug' => 0, - 'cleanup_on_uninstall' => 0 -]); - -$cache_manager = new WPTag_Cache_Manager(); -$cache_stats = $cache_manager->get_cache_stats(); -?> - -
-
-

-
- -
-
- - - -

- - - - - - - - - - - - - - - -
- -

- -

-
- - - - -

- -

-
-
-

- - -

-

- - -

-
-

- -

-
- -

- - - - - -
- -

- -

-
- -

- - - - - -
- -

- -

-
- -

- - - - - - - - - - -
-

- -
-

- - -
- -

- -

-
-
-
- - - - diff --git a/admin/partials/snippet-form.php b/admin/partials/snippet-form.php deleted file mode 100644 index 9990564..0000000 --- a/admin/partials/snippet-form.php +++ /dev/null @@ -1,207 +0,0 @@ -snippet_manager->get_snippet($snippet_id); - if (!$snippet) { - wp_die(__('Snippet not found', 'wptag')); - } - $is_edit = true; -} - -$categories = $this->snippet_manager->get_categories(); -$positions = $this->snippet_manager->get_positions(); -?> - -
-
-

- - - - -

-
- -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -

-
- - - -

-
- - - -
- - - -

-
- - - -

-
- - - -
- - - -

-
- - - -
- - - -

-
- - - -

-
- - -
-

-
-
- - -
- - - - -
- - -
- -
-
-
- -

- -

-
-
-
diff --git a/admin/partials/snippets.php b/admin/partials/snippets.php deleted file mode 100644 index 5c5ec9f..0000000 --- a/admin/partials/snippets.php +++ /dev/null @@ -1,149 +0,0 @@ - sanitize_text_field($_GET['search'] ?? ''), - 'category' => sanitize_key($_GET['filter_category'] ?? ''), - 'position' => sanitize_key($_GET['filter_position'] ?? ''), - 'status' => isset($_GET['filter_status']) ? intval($_GET['filter_status']) : null, - 'per_page' => $per_page, - 'page' => $current_page -]; - -$snippets = $this->snippet_manager->get_snippets($args); -$categories = $this->snippet_manager->get_categories(); -$positions = $this->snippet_manager->get_positions(); -?> - -
-
-

- - - - -

-
- -
-
-
- - -
- -
- - -
- -
- - -
- -
- - -
-
- - -
- - - - - - - -

-

- - - -
- - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - -
- -
- - - - - -
- -
-
diff --git a/admin/partials/templates.php b/admin/partials/templates.php deleted file mode 100644 index a6861b8..0000000 --- a/admin/partials/templates.php +++ /dev/null @@ -1,119 +0,0 @@ -template_manager->get_templates($selected_category); -$categories = $this->template_manager->get_categories(); - -$templates_by_category = []; -foreach ($templates as $template) { - $cat = $template['service_category']; - if (!isset($templates_by_category[$cat])) { - $templates_by_category[$cat] = []; - } - $templates_by_category[$cat][] = $template; -} -?> - -
-
-

-
- -
-
- -

-
- -
- - - - $label) : ?> - - - - -
- - -
- - - - - - -

-

-
- - $cat_templates) : ?> - - -

- -
- -
-

-

- -
- v - -
- - -
- -
- - -
-
- - - - __('Track website traffic and user behavior with Google Analytics 4', 'wptag'), - 'google_analytics_universal' => __('Track website traffic with Universal Analytics (legacy)', 'wptag'), - 'facebook_pixel' => __('Track conversions and build audiences for Facebook ads', 'wptag'), - 'google_ads' => __('Track conversions for Google Ads campaigns', 'wptag'), - 'google_search_console' => __('Verify site ownership for Google Search Console', 'wptag'), - 'baidu_tongji' => __('Track website traffic with Baidu Analytics', 'wptag'), - 'cnzz' => __('Track website traffic with CNZZ Analytics', 'wptag'), - '51la' => __('Track website traffic with 51.la Analytics', 'wptag'), - 'baidu_push' => __('Submit URLs to Baidu for faster indexing', 'wptag'), - 'toutiao_pixel' => __('Track conversions for Toutiao/TikTok ads', 'wptag'), - ]; - - return $descriptions[$service_type] ?? __('Configure and add this service to your site', 'wptag'); -} -?> diff --git a/assets/admin.css b/assets/admin.css index 2b5d52e..4304974 100644 --- a/assets/admin.css +++ b/assets/admin.css @@ -1,374 +1,712 @@ -.wptag-admin-wrap { - margin-top: 20px; +.wptag-admin { + margin: 20px 0; } .wptag-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + padding: 20px; background: #fff; border: 1px solid #ccd0d4; - border-bottom: 0; - padding: 20px; + border-radius: 4px; +} + +.wptag-header-info p { + margin: 0; + color: #666; +} + +.wptag-header-actions { + display: flex; + gap: 10px; +} + +.nav-tab-wrapper { margin-bottom: 0; } -.wptag-header h1 { - margin: 0; - font-size: 24px; - font-weight: 400; - line-height: 1.3; +.nav-tab .count { + background: #72aee6; + color: #fff; + padding: 2px 6px; + border-radius: 10px; + font-size: 11px; + margin-left: 5px; } -.wptag-header .page-title-action { - margin-left: 10px; +.nav-tab-active .count { + background: #fff; + color: #2271b1; } -.wptag-content { +.tab-content { background: #fff; border: 1px solid #ccd0d4; + border-top: none; padding: 20px; + border-radius: 0 0 4px 4px; } -.wptag-stats-grid { +.wptag-no-services { + text-align: center; + padding: 40px 20px; + color: #666; +} + +.wptag-no-services p { + font-size: 16px; + margin: 0; +} + +.wptag-services-grid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 20px; margin-bottom: 30px; } -.wptag-stat-card { - background: #f8f9fa; - border: 1px solid #e2e4e7; - border-radius: 4px; - padding: 20px; - text-align: center; +.wptag-service-card { + background: #f9f9f9; + border: 1px solid #e1e1e1; + border-radius: 8px; + overflow: hidden; + transition: all 0.3s ease; } -.wptag-stat-card h3 { - margin: 0 0 10px; - font-size: 14px; - color: #666; - font-weight: 400; +.wptag-service-card:hover { + box-shadow: 0 2px 8px rgba(0,0,0,0.1); } -.wptag-stat-card .stat-value { - font-size: 32px; - font-weight: 600; - color: #2271b1; +.wptag-service-card.disabled { + opacity: 0.6; } -.wptag-table { - width: 100%; - border-collapse: collapse; - margin-top: 20px; +.wptag-service-card.disabled .wptag-service-content { + pointer-events: none; } -.wptag-table th, -.wptag-table td { - padding: 12px; - text-align: left; - border-bottom: 1px solid #e2e4e7; -} - -.wptag-table th { - background: #f8f9fa; - font-weight: 600; - color: #2c3338; -} - -.wptag-table tbody tr:hover { - background: #f6f7f7; -} - -.wptag-status-badge { - display: inline-block; - padding: 3px 8px; - border-radius: 3px; - font-size: 12px; - font-weight: 500; -} - -.wptag-status-badge.active { - background: #d4f4dd; - color: #00a32a; -} - -.wptag-status-badge.inactive { - background: #f5e6e6; - color: #d63638; -} - -.wptag-actions { +.wptag-service-header { display: flex; - gap: 10px; -} - -.wptag-action-link { - color: #2271b1; - text-decoration: none; - font-size: 13px; -} - -.wptag-action-link:hover { - color: #135e96; - text-decoration: underline; -} - -.wptag-action-link.delete { - color: #d63638; -} - -.wptag-action-link.delete:hover { - color: #a02222; -} - -.wptag-form-table { - width: 100%; - max-width: 800px; -} - -.wptag-form-table th { - width: 200px; - padding: 20px 10px 20px 0; - vertical-align: top; - text-align: left; - font-weight: 600; -} - -.wptag-form-table td { - padding: 15px 10px; -} - -.wptag-form-table input[type="text"], -.wptag-form-table input[type="number"], -.wptag-form-table select, -.wptag-form-table textarea { - width: 100%; - max-width: 400px; -} - -.wptag-code-editor { - width: 100%; - min-height: 300px; - font-family: Consolas, Monaco, monospace; - font-size: 13px; -} - -.wptag-conditions-builder { - background: #f8f9fa; - border: 1px solid #e2e4e7; - border-radius: 4px; - padding: 20px; - margin-top: 10px; -} - -.wptag-condition-group { - background: #fff; - border: 1px solid #e2e4e7; - border-radius: 4px; - padding: 15px; - margin-bottom: 15px; -} - -.wptag-condition-row { - display: flex; - gap: 10px; align-items: center; - margin-bottom: 10px; -} - -.wptag-condition-row select, -.wptag-condition-row input { - flex: 1; - min-width: 0; -} - -.wptag-templates-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); - gap: 20px; - margin-top: 20px; -} - -.wptag-template-card { - background: #fff; - border: 1px solid #e2e4e7; - border-radius: 4px; padding: 20px; - transition: box-shadow 0.2s; + background: #fff; + border-bottom: 1px solid #e1e1e1; } -.wptag-template-card:hover { - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +.wptag-service-icon { + margin-right: 15px; } -.wptag-template-card h3 { - margin: 0 0 10px; - font-size: 16px; - color: #2c3338; -} - -.wptag-template-card p { +.wptag-service-icon .dashicons { + font-size: 24px; + width: 24px; + height: 24px; color: #666; - margin: 0 0 15px; - font-size: 14px; + transition: color 0.3s ease; } -.wptag-template-card .button { - width: 100%; - text-align: center; +.wptag-service-card:hover .wptag-service-icon .dashicons { + color: #2271b1; } -.wptag-modal { - display: none; - position: fixed; +.wptag-service-item:hover .wptag-service-icon .dashicons { + color: #2271b1; +} + +.wptag-service-title { + flex: 1; + display: flex; + justify-content: space-between; + align-items: center; +} + +.wptag-service-title h3 { + margin: 0; + font-size: 16px; + font-weight: 600; + color: #23282d; +} + +.wptag-switch { + position: relative; + display: inline-block; + width: 50px; + height: 24px; +} + +.wptag-switch input { + opacity: 0; + width: 0; + height: 0; +} + +.wptag-slider { + position: absolute; + cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; - background: rgba(0, 0, 0, 0.7); + background-color: #ccc; + transition: .4s; + border-radius: 24px; +} + +.wptag-slider:before { + position: absolute; + content: ""; + height: 18px; + width: 18px; + left: 3px; + bottom: 3px; + background-color: white; + transition: .4s; + border-radius: 50%; +} + +input:checked + .wptag-slider { + background-color: #2271b1; +} + +.wptag-slider.disabled { + pointer-events: none; + opacity: 0.5; +} + +.wptag-code-editor-wrapper { + position: relative; + border: 1px solid #ddd; + border-radius: 4px; + background: #f8f9fa; + margin-bottom: 10px; +} + +.wptag-code-editor { + width: 100%; + padding: 15px; + border: none; + border-radius: 4px; + font-family: 'Consolas', 'Monaco', 'Courier New', monospace; + font-size: 13px; + line-height: 1.5; + background: #fff; + color: #333; + resize: none; + overflow-y: auto; + min-height: 120px; + max-height: 400px; + box-sizing: border-box; + transition: border-color 0.3s ease; +} + +.wptag-code-editor:focus { + outline: none; + border-color: #2271b1; + box-shadow: 0 0 0 1px #2271b1; +} + +.wptag-code-editor:disabled { + background-color: #f0f0f0; + color: #666; + cursor: not-allowed; +} + +.wptag-code-editor-toolbar { + position: absolute; + top: 8px; + right: 8px; + display: flex; + gap: 5px; + opacity: 0.7; + transition: opacity 0.3s ease; +} + +.wptag-code-editor-wrapper:hover .wptag-code-editor-toolbar { + opacity: 1; +} + +.wptag-code-editor-toolbar .button { + padding: 4px 8px; + min-height: auto; + background: rgba(255, 255, 255, 0.9); + border: 1px solid #ddd; + border-radius: 3px; + font-size: 12px; +} + +.wptag-code-editor-toolbar .button:hover { + background: #f0f0f0; +} + +.wptag-advanced-toggle { + border-top: 1px solid #e1e1e1; + padding-top: 15px; + margin-top: 15px; +} + +.wptag-toggle-advanced { + display: flex; + align-items: center; + gap: 5px; + color: #666; + text-decoration: none; + font-size: 13px; + padding: 5px 0; + border: none; + background: none; + cursor: pointer; + transition: color 0.2s ease; +} + +.wptag-toggle-advanced:hover { + color: #2271b1; +} + +.wptag-toggle-advanced:focus { + outline: none; + box-shadow: none; + color: #2271b1; +} + +.wptag-toggle-advanced .dashicons { + font-size: 16px; + width: 16px; + height: 16px; + transition: transform 0.2s ease; +} + +.wptag-advanced-settings { + margin-top: 15px; + padding-top: 15px; + border-top: 1px solid #e1e1e1; +} + +.wptag-service-card.disabled .wptag-code-editor-wrapper { + opacity: 0.6; + pointer-events: none; +} + +.wptag-service-card.disabled .wptag-code-editor-toolbar { + opacity: 0.5; + pointer-events: none; +} + +.wptag-service-card.disabled .wptag-toggle-advanced { + opacity: 0.6; + pointer-events: none; +} + +.wptag-service-content { + padding: 20px; +} + +.wptag-form-row { + margin-bottom: 20px; +} + +.wptag-form-label { + display: block; + margin-bottom: 5px; + font-weight: 600; + color: #23282d; +} + +.wptag-radio-group { + display: flex; + gap: 20px; +} + +.wptag-radio-group label { + display: flex; + align-items: center; + font-weight: normal; + cursor: pointer; +} + +.wptag-radio-group input[type="radio"] { + margin-right: 8px; +} + +.wptag-input-group { + display: flex; + gap: 10px; + align-items: flex-start; +} + +.wptag-input, +.wptag-textarea, +.wptag-select { + flex: 1; + padding: 8px 12px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 14px; + transition: border-color 0.3s ease; +} + +.wptag-input:focus, +.wptag-textarea:focus, +.wptag-select:focus { + border-color: #2271b1; + outline: none; + box-shadow: 0 0 0 1px #2271b1; +} + +.wptag-input-small { + max-width: 100px; +} + +.wptag-textarea { + font-family: Consolas, Monaco, monospace; + font-size: 13px; + min-height: 120px; + resize: vertical; +} + +.wptag-validation-result { + margin-top: 8px; + font-size: 13px; + font-weight: 600; +} + +.wptag-validation-result.valid { + color: #46b450; +} + +.wptag-validation-result.invalid { + color: #dc3232; +} + +.wptag-validation-result.loading { + color: #0073aa; +} + +.wptag-template-fields, +.wptag-custom-fields { + transition: all 0.3s ease; +} + +.wptag-advanced-settings { + margin-top: 15px; + padding-top: 15px; + border-top: 1px solid #e1e1e1; + display: none; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 15px; +} + +.wptag-form-actions { + text-align: right; + padding-top: 20px; + border-top: 1px solid #ddd; +} + +.wptag-validate-btn, +.wptag-preview-btn { + background: #0073aa; + color: #fff; + border: none; + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; + font-size: 13px; + transition: background-color 0.3s ease; +} + +.wptag-validate-btn:hover, +.wptag-preview-btn:hover { + background: #005a87; +} + +.wptag-validate-btn:disabled, +.wptag-preview-btn:disabled { + background: #ccc; + cursor: not-allowed; +} + +.wptag-preview-btn { + background: #666; +} + +.wptag-preview-btn:hover { + background: #444; +} + +.wptag-services-management { + margin-top: 0; +} + +.wptag-services-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30px; + padding: 20px; + background: #f9f9f9; + border: 1px solid #e1e1e1; + border-radius: 4px; +} + +.wptag-services-header p { + margin: 0; + color: #666; +} + +.wptag-services-actions { + display: flex; + gap: 10px; +} + +.wptag-service-category { + margin-bottom: 40px; +} + +.wptag-service-category h2 { + margin-bottom: 20px; + color: #23282d; + font-size: 20px; + font-weight: 600; + border-bottom: 2px solid #2271b1; + padding-bottom: 10px; +} + +.wptag-service-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 20px; + background: #fff; + border: 1px solid #e1e1e1; + border-radius: 8px; + margin-bottom: 15px; + transition: all 0.3s ease; +} + +.wptag-service-item:hover { + box-shadow: 0 2px 8px rgba(0,0,0,0.1); +} + +.wptag-service-item.disabled { + opacity: 0.6; +} + +.wptag-service-info { + display: flex; + align-items: center; + flex: 1; +} + +.wptag-service-details h3 { + margin: 0 0 5px 0; + font-size: 16px; + font-weight: 600; + color: #23282d; +} + +.wptag-service-details p { + margin: 0; + color: #666; + font-size: 14px; +} + +#wptag-preview-modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0,0,0,0.5); z-index: 100000; + display: flex; + align-items: center; + justify-content: center; } .wptag-modal-content { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); background: #fff; - border-radius: 4px; - max-width: 600px; - width: 90%; - max-height: 90vh; - overflow: auto; + border-radius: 8px; + max-width: 80%; + max-height: 80%; + overflow: hidden; + display: flex; + flex-direction: column; + box-shadow: 0 10px 40px rgba(0,0,0,0.3); } .wptag-modal-header { + display: flex; + justify-content: space-between; + align-items: center; padding: 20px; - border-bottom: 1px solid #e2e4e7; + border-bottom: 1px solid #e1e1e1; + background: #f9f9f9; } -.wptag-modal-header h2 { +.wptag-modal-header h3 { margin: 0; - font-size: 20px; + font-size: 18px; + color: #23282d; +} + +.wptag-modal-close { + background: none; + border: none; + font-size: 24px; + cursor: pointer; + color: #666; + padding: 0; + width: 30px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + transition: all 0.3s ease; +} + +.wptag-modal-close:hover { + color: #000; + background: rgba(0,0,0,0.1); } .wptag-modal-body { padding: 20px; + overflow: auto; } -.wptag-modal-footer { - padding: 20px; - border-top: 1px solid #e2e4e7; - text-align: right; -} - -.wptag-filters { - display: flex; - gap: 15px; - margin-bottom: 20px; - flex-wrap: wrap; -} - -.wptag-filter-item { - display: flex; - flex-direction: column; - gap: 5px; -} - -.wptag-filter-item label { - font-size: 13px; - font-weight: 600; - color: #2c3338; -} - -.wptag-empty-state { - text-align: center; - padding: 60px 20px; - color: #666; -} - -.wptag-empty-state svg { - width: 64px; - height: 64px; - margin-bottom: 20px; - opacity: 0.3; -} - -.wptag-empty-state h3 { - font-size: 18px; - font-weight: 400; - margin: 0 0 10px; -} - -.wptag-empty-state p { - margin: 0 0 20px; -} - -.wptag-tabs { - display: flex; - border-bottom: 1px solid #e2e4e7; - margin: -20px -20px 20px; - padding: 0 20px; -} - -.wptag-tab { - padding: 15px 20px; - border-bottom: 2px solid transparent; - text-decoration: none; - color: #2c3338; - font-weight: 500; - transition: all 0.2s; -} - -.wptag-tab:hover { - color: #2271b1; -} - -.wptag-tab.active { - color: #2271b1; - border-bottom-color: #2271b1; -} - -.wptag-notice { - padding: 12px 20px; - margin: 20px 0; +#wptag-preview-code { + background: #f4f4f4; + border: 1px solid #ddd; border-radius: 4px; - display: flex; - align-items: center; - gap: 10px; + padding: 15px; + font-family: Consolas, Monaco, monospace; + font-size: 13px; + line-height: 1.5; + white-space: pre-wrap; + word-wrap: break-word; + margin: 0; + min-height: 200px; + color: #333; } -.wptag-notice.info { - background: #e5f5fa; - color: #0073aa; -} - -.wptag-notice.success { - background: #d4f4dd; - color: #00a32a; -} - -.wptag-notice.warning { - background: #fcf9e8; - color: #996800; -} - -.wptag-notice.error { - background: #f5e6e6; - color: #d63638; +@media screen and (max-width: 1200px) { + .wptag-services-grid { + grid-template-columns: 1fr; + } } @media screen and (max-width: 782px) { - .wptag-form-table th { - width: auto; - display: block; - padding-bottom: 5px; + .wptag-header { + flex-direction: column; + gap: 15px; } - .wptag-filters { + .wptag-header-actions { + width: 100%; + justify-content: center; + } + + .wptag-services-header { + flex-direction: column; + gap: 15px; + } + + .wptag-services-actions { + width: 100%; + justify-content: center; + } + + .wptag-input-group { flex-direction: column; } - .wptag-condition-row { + .wptag-input-group button { + align-self: flex-start; + } + + .wptag-advanced-settings { + grid-template-columns: 1fr; + } + + .wptag-radio-group { flex-direction: column; + gap: 10px; + } + + .wptag-modal-content { + max-width: 95%; + max-height: 95%; + } + + .wptag-service-item { + flex-direction: column; + gap: 15px; + } + + .wptag-service-info { + text-align: center; + } + + .wptag-service-toggle { + align-self: center; } } + +.wptag-loading { + position: relative; + opacity: 0.6; + pointer-events: none; +} + +.wptag-loading::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 20px; + height: 20px; + border: 2px solid #f3f3f3; + border-top: 2px solid #2271b1; + border-radius: 50%; + animation: spin 1s linear infinite; + transform: translate(-50%, -50%); +} + +@keyframes spin { + 0% { transform: translate(-50%, -50%) rotate(0deg); } + 100% { transform: translate(-50%, -50%) rotate(360deg); } +} + +.wptag-text-center { + text-align: center; +} + +.wptag-text-muted { + color: #666; +} + +.wptag-mb-0 { + margin-bottom: 0; +} + +.wptag-mt-20 { + margin-top: 20px; +} + +.wptag-hidden { + display: none; +} + +.notice { + margin: 5px 0 15px 0; +} + +.notice.notice-success { + border-left-color: #46b450; +} + +.notice.notice-error { + border-left-color: #dc3232; +} \ No newline at end of file diff --git a/assets/admin.js b/assets/admin.js index d3ff23e..35a59fa 100644 --- a/assets/admin.js +++ b/assets/admin.js @@ -1,322 +1,490 @@ -(function($) { - 'use strict'; +jQuery(document).ready(function($) { + WPTagAdmin.init(); +}); - const WPTagAdmin = { - init: function() { - this.bindEvents(); - this.initCodeEditor(); - this.initConditionsBuilder(); - }, - - bindEvents: function() { - $(document).on('click', '.wptag-toggle-status', this.toggleStatus); - $(document).on('click', '.wptag-delete-snippet', this.deleteSnippet); - $(document).on('change', '.wptag-filter', this.filterSnippets); - $(document).on('click', '.wptag-template-use', this.useTemplate); - $(document).on('submit', '.wptag-snippet-form', this.validateForm); - $(document).on('click', '.wptag-add-condition', this.addCondition); - $(document).on('click', '.wptag-remove-condition', this.removeCondition); - $(document).on('change', '#code_type', this.updateCodeEditor); - }, - - toggleStatus: function(e) { - e.preventDefault(); +var WPTagAdmin = { + + init: function() { + var $ = jQuery; + this.handleServiceToggle(); + this.handleCodeTypeToggle(); + this.handleValidation(); + this.handlePreview(); + this.handleExportImport(); + this.handleModal(); + this.handleFormSubmission(); + this.handleServicesManagement(); + this.handleReset(); + this.handleTabSwitching(); + this.handleAdvancedToggle(); + this.handleCodeEditor(); + }, + + handleServiceToggle: function() { + var $ = jQuery; + $('.wptag-service-card input[name*="[enabled]"]').on('change', function() { + var $card = $(this).closest('.wptag-service-card'); + var isEnabled = $(this).is(':checked'); - const $link = $(this); - const snippetId = $link.data('snippet-id'); + if (isEnabled) { + $card.removeClass('disabled'); + $card.find('.wptag-service-content input, .wptag-service-content select, .wptag-service-content textarea, .wptag-service-content button').prop('disabled', false); + } else { + $card.addClass('disabled'); + $card.find('.wptag-service-content input, .wptag-service-content select, .wptag-service-content textarea, .wptag-service-content button').prop('disabled', true); + } + }); + + $('.wptag-service-card input[name*="[enabled]"]').trigger('change'); + }, + + handleCodeTypeToggle: function() { + var $ = jQuery; + $('input[name*="[use_template]"]').on('change', function() { + var $card = $(this).closest('.wptag-service-card'); + var useTemplate = $(this).val() === '1' && $(this).is(':checked'); + + if (useTemplate) { + $card.find('.wptag-template-fields').show(); + $card.find('.wptag-custom-fields').hide(); + } else { + $card.find('.wptag-template-fields').hide(); + $card.find('.wptag-custom-fields').show(); + } + }); + + $('input[name*="[use_template]"]:checked').trigger('change'); + }, + + handleValidation: function() { + var $ = jQuery; + $('.wptag-validate-btn').on('click', function() { + var $btn = $(this); + var $card = $btn.closest('.wptag-service-card'); + var service = $card.data('service'); + var $result = $card.find('.wptag-validation-result'); + + var useTemplate = $card.find('input[name*="[use_template]"]:checked').val() === '1'; + var idValue = $card.find('input[type="text"]').first().val(); + var customCode = $card.find('textarea').val(); + + $btn.prop('disabled', true).text(wptagAdmin.strings.validating); + $result.removeClass('valid invalid').addClass('loading').text(wptagAdmin.strings.validating); $.ajax({ - url: wptagAdmin.ajaxUrl, + url: wptagAdmin.ajax_url, type: 'POST', data: { - action: 'wptag_toggle_snippet', - snippet_id: snippetId, - nonce: wptagAdmin.nonce + action: 'wptag_validate_code', + nonce: wptagAdmin.nonce, + service: service, + use_template: useTemplate ? '1' : '0', + id_value: idValue, + custom_code: customCode }, success: function(response) { if (response.success) { - location.reload(); + $result.removeClass('loading invalid').addClass('valid') + .text('✓ ' + response.data.message); } else { - alert(response.data.message || wptagAdmin.strings.error); + $result.removeClass('loading valid').addClass('invalid') + .text('✗ ' + response.data.message); } }, error: function() { - alert(wptagAdmin.strings.error); + $result.removeClass('loading valid').addClass('invalid') + .text('✗ Validation failed'); + }, + complete: function() { + $btn.prop('disabled', false).text('Validate'); } }); - }, - - deleteSnippet: function(e) { - e.preventDefault(); + }); + + $('input[type="text"], textarea').on('input', function() { + $(this).closest('.wptag-service-card').find('.wptag-validation-result').text(''); + }); + }, + + handlePreview: function() { + var $ = jQuery; + $('.wptag-preview-btn').on('click', function() { + var $btn = $(this); + var $card = $btn.closest('.wptag-service-card'); + var service = $card.data('service'); + var idValue = $card.find('input[type="text"]').first().val(); - if (!confirm(wptagAdmin.strings.confirmDelete)) { + if (!idValue.trim()) { + alert('Please enter an ID value to preview'); return; } - const $link = $(this); - const snippetId = $link.data('snippet-id'); + $btn.prop('disabled', true).text(wptagAdmin.strings.loading); $.ajax({ - url: wptagAdmin.ajaxUrl, + url: wptagAdmin.ajax_url, type: 'POST', data: { - action: 'wptag_delete_snippet', - snippet_id: snippetId, - nonce: wptagAdmin.nonce + action: 'wptag_preview_code', + nonce: wptagAdmin.nonce, + service: service, + id_value: idValue }, success: function(response) { if (response.success) { - $link.closest('tr').fadeOut(400, function() { - $(this).remove(); - }); + $('#wptag-preview-code').text(response.data.preview); + $('#wptag-preview-modal').show(); } else { - alert(response.data.message || wptagAdmin.strings.error); + alert('Error: ' + response.data.message); } }, error: function() { - alert(wptagAdmin.strings.error); + alert('Failed to generate preview'); + }, + complete: function() { + $btn.prop('disabled', false).text(wptagAdmin.strings.preview); } }); - }, - - filterSnippets: function() { - const filters = { - search: $('#filter-search').val(), - category: $('#filter-category').val(), - position: $('#filter-position').val(), - status: $('#filter-status').val() - }; + }); + }, + + handleExportImport: function() { + var $ = jQuery; + $('#wptag-export-btn').on('click', function() { + var $btn = $(this); + var originalText = $btn.text(); + $btn.prop('disabled', true).text(wptagAdmin.strings.loading); - const params = new URLSearchParams(filters); - params.delete('page'); - - Object.keys(filters).forEach(key => { - if (!filters[key]) { - params.delete(key); + $.ajax({ + url: wptagAdmin.ajax_url, + type: 'POST', + data: { + action: 'wptag_export_settings', + nonce: wptagAdmin.nonce + }, + success: function(response) { + if (response.success) { + WPTagAdmin.downloadFile(response.data.data, response.data.filename); + WPTagAdmin.showNotice('success', wptagAdmin.strings.export_success); + } else { + alert('Export failed: ' + response.data.message); + } + }, + error: function() { + alert('Export failed'); + }, + complete: function() { + $btn.prop('disabled', false).text(originalText); } }); + }); + + $('#wptag-import-btn').on('click', function() { + $('#wptag-import-file').click(); + }); + + $('#wptag-import-file').on('change', function() { + var file = this.files[0]; + if (!file) return; - window.location.href = window.location.pathname + '?page=wptag-snippets&' + params.toString(); - }, - - initCodeEditor: function() { - const $codeTextarea = $('#snippet-code'); - - if ($codeTextarea.length && typeof wp !== 'undefined' && wp.codeEditor) { - const editorSettings = wp.codeEditor.defaultSettings || {}; - const codeType = $('#code_type').val(); - - editorSettings.codemirror = { - ...editorSettings.codemirror, - mode: this.getCodeMirrorMode(codeType), - lineNumbers: true, - lineWrapping: true, - indentUnit: 2, - tabSize: 2 - }; - - this.codeEditor = wp.codeEditor.initialize($codeTextarea, editorSettings); - } - }, - - getCodeMirrorMode: function(codeType) { - const modes = { - 'html': 'htmlmixed', - 'javascript': 'javascript', - 'css': 'css' - }; - - return modes[codeType] || 'htmlmixed'; - }, - - updateCodeEditor: function() { - const codeType = $(this).val(); - - if (WPTagAdmin.codeEditor && WPTagAdmin.codeEditor.codemirror) { - WPTagAdmin.codeEditor.codemirror.setOption('mode', WPTagAdmin.getCodeMirrorMode(codeType)); - } - }, - - initConditionsBuilder: function() { - const $builder = $('.wptag-conditions-builder'); - - if (!$builder.length) { + if (!confirm(wptagAdmin.strings.confirm_import)) { return; } - this.loadConditionTypes(); - }, - - loadConditionTypes: function() { - $.ajax({ - url: wptagAdmin.ajaxUrl, - type: 'POST', - data: { - action: 'wptag_get_condition_types', - nonce: wptagAdmin.nonce - }, - success: function(response) { - if (response.success) { - WPTagAdmin.conditionTypes = response.data.types; + var reader = new FileReader(); + reader.onload = function(e) { + var importData = e.target.result; + + $.ajax({ + url: wptagAdmin.ajax_url, + type: 'POST', + data: { + action: 'wptag_import_settings', + nonce: wptagAdmin.nonce, + import_data: importData + }, + success: function(response) { + if (response.success) { + WPTagAdmin.showNotice('success', wptagAdmin.strings.import_success); + setTimeout(function() { + location.reload(); + }, 1000); + } else { + alert('Import failed: ' + response.data.message); + } + }, + error: function() { + alert('Import failed'); } - } - }); - }, - - addCondition: function(e) { - e.preventDefault(); + }); + }; + reader.readAsText(file); - const $group = $(this).closest('.wptag-condition-group'); - const $newRow = WPTagAdmin.createConditionRow(); + this.value = ''; + }); + }, + + handleReset: function() { + var $ = jQuery; + $('#wptag-reset-btn').on('click', function() { + if (!confirm(wptagAdmin.strings.confirm_reset)) { + return; + } - $group.find('.wptag-conditions-list').append($newRow); - }, - - removeCondition: function(e) { - e.preventDefault(); - - $(this).closest('.wptag-condition-row').fadeOut(300, function() { - $(this).remove(); - }); - }, - - createConditionRow: function() { - const html = ` -
- - - - -
- `; - - return $(html); - }, - - useTemplate: function(e) { - e.preventDefault(); - - const $button = $(this); - const serviceType = $button.data('service-type'); + var $btn = $(this); + var originalText = $btn.text(); + $btn.prop('disabled', true).text(wptagAdmin.strings.loading); $.ajax({ - url: wptagAdmin.ajaxUrl, + url: wptagAdmin.ajax_url, type: 'POST', data: { - action: 'wptag_get_template', - service_type: serviceType, + action: 'wptag_reset_settings', nonce: wptagAdmin.nonce }, success: function(response) { if (response.success) { - WPTagAdmin.showTemplateModal(response.data.template); + WPTagAdmin.showNotice('success', wptagAdmin.strings.reset_success); + setTimeout(function() { + location.reload(); + }, 1000); } else { - alert(response.data.message || wptagAdmin.strings.error); + alert('Reset failed: ' + response.data.message); } - } - }); - }, - - showTemplateModal: function(template) { - const fields = template.config_fields.map(field => { - return ` -
- - -
- `; - }).join(''); - - const modalHtml = ` -
-
-
-

${template.service_name} Configuration

-
-
-
- ${fields} -
- -
-
-
- `; - - $('body').append(modalHtml); - $('#template-config-modal').fadeIn(); - - $('#template-config-form').on('submit', function(e) { - e.preventDefault(); - WPTagAdmin.processTemplate(template.service_type, $(this).serialize()); - }); - }, - - processTemplate: function(serviceType, formData) { - $.ajax({ - url: wptagAdmin.ajaxUrl, - type: 'POST', - data: { - action: 'wptag_process_template', - service_type: serviceType, - config: formData, - nonce: wptagAdmin.nonce }, - success: function(response) { - if (response.success) { - window.location.href = `admin.php?page=wptag-snippets&action=edit&snippet_id=${response.data.snippet_id}`; - } else { - alert(response.data.message || wptagAdmin.strings.error); + error: function() { + alert('Reset failed'); + }, + complete: function() { + $btn.prop('disabled', false).text(originalText); + } + }); + }); + }, + + handleServicesManagement: function() { + var $ = jQuery; + $('#wptag-enable-all').on('click', function() { + $('input[name="enabled_services[]"]').prop('checked', true); + $('.wptag-service-item').removeClass('disabled'); + }); + + $('#wptag-disable-all').on('click', function() { + $('input[name="enabled_services[]"]').prop('checked', false); + $('.wptag-service-item').addClass('disabled'); + }); + + $('.wptag-service-item input[type="checkbox"]').on('change', function() { + var $item = $(this).closest('.wptag-service-item'); + var isChecked = $(this).is(':checked'); + + if (isChecked) { + $item.removeClass('disabled'); + } else { + $item.addClass('disabled'); + } + }); + + $('.wptag-service-item input[type="checkbox"]').trigger('change'); + }, + + handleModal: function() { + var $ = jQuery; + $('.wptag-modal-close, #wptag-preview-modal').on('click', function(e) { + if (e.target === this) { + $('#wptag-preview-modal').hide(); + } + }); + + $('.wptag-modal-content').on('click', function(e) { + e.stopPropagation(); + }); + + $(document).on('keydown', function(e) { + if (e.keyCode === 27 && $('#wptag-preview-modal').is(':visible')) { + $('#wptag-preview-modal').hide(); + } + }); + }, + + handleFormSubmission: function() { + var $ = jQuery; + $('.wptag-settings-form').on('submit', function(e) { + var hasErrors = false; + + $('.wptag-service-card').each(function() { + var $card = $(this); + var isEnabled = $card.find('input[name*="[enabled]"]').is(':checked'); + + if (!isEnabled) { + return; + } + + var useTemplate = $card.find('input[name*="[use_template]"]:checked').val() === '1'; + + if (useTemplate) { + var idInput = $card.find('input[type="text"]').first(); + if (idInput.val().trim() === '') { + alert('Please enter an ID for enabled services or disable them.'); + idInput.focus(); + hasErrors = true; + return false; + } + } else { + var customCodeTextarea = $card.find('textarea'); + if (customCodeTextarea.val().trim() === '') { + alert('Please enter custom code for enabled services or disable them.'); + customCodeTextarea.focus(); + hasErrors = true; + return false; } } }); - }, - - closeModal: function() { - $('.wptag-modal').fadeOut(300, function() { - $(this).remove(); - }); - }, - - validateForm: function(e) { - const $form = $(this); - const name = $form.find('#snippet-name').val(); - const code = WPTagAdmin.codeEditor ? - WPTagAdmin.codeEditor.codemirror.getValue() : - $form.find('#snippet-code').val(); - if (!name.trim()) { + if (hasErrors) { e.preventDefault(); - alert('Please enter a snippet name'); return false; } - if (!code.trim()) { - e.preventDefault(); - alert('Please enter some code'); - return false; + $(this).find('input[type="submit"]').prop('disabled', true).val('Saving...'); + }); + }, + + handleTabSwitching: function() { + var $ = jQuery; + $('.nav-tab').on('click', function(e) { + var href = $(this).attr('href'); + if (href && href.indexOf('tab=') !== -1) { + var tab = href.split('tab=')[1]; + var currentUrl = window.location.href.split('?')[0]; + var newUrl = currentUrl + '?page=wptag-settings&tab=' + tab; + window.location.href = newUrl; } + }); + }, + + handleAdvancedToggle: function() { + var $ = jQuery; + $('.wptag-toggle-advanced').on('click', function(e) { + e.preventDefault(); + var $btn = $(this); + var $card = $btn.closest('.wptag-service-card'); + var $advanced = $card.find('.wptag-advanced-settings'); + var $icon = $btn.find('.dashicons'); - return true; + if ($advanced.is(':visible')) { + $advanced.slideUp(); + $icon.removeClass('dashicons-arrow-up-alt2').addClass('dashicons-arrow-down-alt2'); + $btn.html('Advanced Settings'); + } else { + $advanced.slideDown(); + $icon.removeClass('dashicons-arrow-down-alt2').addClass('dashicons-arrow-up-alt2'); + $btn.html('Hide Advanced Settings'); + } + }); + }, + + handleCodeEditor: function() { + var $ = jQuery; + $('.wptag-code-editor').each(function() { + var $editor = $(this); + + $editor.on('input', function() { + WPTagAdmin.updateEditorHeight(this); + }); + + $editor.on('keydown', function(e) { + if (e.keyCode === 9) { + e.preventDefault(); + var start = this.selectionStart; + var end = this.selectionEnd; + var value = this.value; + this.value = value.substring(0, start) + " " + value.substring(end); + this.selectionStart = this.selectionEnd = start + 2; + } + }); + + WPTagAdmin.updateEditorHeight(this); + }); + + $('.wptag-format-code').on('click', function() { + var $btn = $(this); + var $editor = $btn.closest('.wptag-code-editor-wrapper').find('.wptag-code-editor'); + var code = $editor.val(); + + if (code.trim()) { + try { + var formatted = WPTagAdmin.formatCode(code); + $editor.val(formatted); + WPTagAdmin.updateEditorHeight($editor[0]); + } catch (e) { + console.log('Code formatting failed:', e); + } + } + }); + + $('.wptag-clear-code').on('click', function() { + if (confirm('Are you sure you want to clear the code?')) { + var $btn = $(this); + var $editor = $btn.closest('.wptag-code-editor-wrapper').find('.wptag-code-editor'); + $editor.val(''); + WPTagAdmin.updateEditorHeight($editor[0]); + } + }); + }, + + updateEditorHeight: function(editor) { + editor.style.height = 'auto'; + editor.style.height = Math.max(editor.scrollHeight, 120) + 'px'; + }, + + formatCode: function(code) { + try { + return code + .replace(/>\n<') + .replace(/^\s+|\s+$/g, '') + .split('\n') + .map(function(line) { + return line.trim(); + }) + .filter(function(line) { + return line.length > 0; + }) + .join('\n'); + } catch (e) { + console.log('Code formatting error:', e); + return code; } - }; - - $(document).ready(function() { - WPTagAdmin.init(); - }); - -})(jQuery); + }, + + downloadFile: function(data, filename) { + var blob = new Blob([data], { type: 'application/json' }); + var url = window.URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = filename; + a.style.display = 'none'; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + }, + + showNotice: function(type, message) { + var $ = jQuery; + var $notice = $('

' + message + '

'); + $('.wptag-admin h1').after($notice); + + setTimeout(function() { + $notice.fadeOut(function() { + $(this).remove(); + }); + }, 5000); + + $notice.find('.notice-dismiss').on('click', function() { + $notice.fadeOut(function() { + $(this).remove(); + }); + }); + } +}; \ No newline at end of file diff --git a/includes/class-admin.php b/includes/class-admin.php new file mode 100644 index 0000000..93d5d4f --- /dev/null +++ b/includes/class-admin.php @@ -0,0 +1,672 @@ +config = $config; + $this->validator = new Validator($config); + $this->output_manager = new Output_Manager($config); + + $this->init_hooks(); + } + + private function current_user_can_manage_codes() { + return current_user_can('manage_options') || current_user_can('wptag_manage_codes'); + } + + private function current_user_can_manage_services() { + return current_user_can('manage_options') || current_user_can('wptag_manage_services'); + } + + private function init_hooks() { + add_action('admin_menu', array($this, 'add_admin_menu')); + add_action('admin_init', array($this, 'admin_init')); + add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_assets')); + add_action('wp_ajax_wptag_validate_code', array($this, 'ajax_validate_code')); + add_action('wp_ajax_wptag_preview_code', array($this, 'ajax_preview_code')); + add_action('wp_ajax_wptag_export_settings', array($this, 'ajax_export_settings')); + add_action('wp_ajax_wptag_import_settings', array($this, 'ajax_import_settings')); + add_action('wp_ajax_wptag_reset_settings', array($this, 'ajax_reset_settings')); + add_filter('plugin_action_links_' . plugin_basename(WPTAG_PLUGIN_FILE), array($this, 'add_action_links')); + add_filter('plugin_row_meta', array($this, 'add_row_meta'), 10, 2); + add_action('init', array($this, 'add_custom_capabilities')); + } + + public function add_custom_capabilities() { + $role = get_role('administrator'); + if ($role) { + $role->add_cap('wptag_manage_codes'); + $role->add_cap('wptag_manage_services'); + } + + $role = get_role('editor'); + if ($role) { + $role->add_cap('wptag_manage_codes'); + } + } + + public function add_admin_menu() { + $main_hook = add_options_page( + 'WPTag Settings', + 'WPTag', + 'manage_options', + 'wptag-settings', + array($this, 'display_admin_page') + ); + + add_action('load-' . $main_hook, array($this, 'handle_form_submission')); + } + + public function admin_init() { + register_setting( + 'wptag_settings_group', + 'wptag_settings', + array( + 'sanitize_callback' => array($this, 'sanitize_settings'), + 'default' => array() + ) + ); + + register_setting( + 'wptag_services_group', + 'wptag_services', + array( + 'sanitize_callback' => array($this, 'sanitize_services'), + 'default' => array() + ) + ); + } + + public function enqueue_admin_assets($hook) { + if ($hook !== 'settings_page_wptag-settings') { + return; + } + + wp_enqueue_style( + 'wptag-admin', + WPTAG_PLUGIN_URL . 'assets/admin.css', + array(), + WPTAG_VERSION + ); + + wp_enqueue_script( + 'wptag-admin', + WPTAG_PLUGIN_URL . 'assets/admin.js', + array('jquery', 'wp-util'), + WPTAG_VERSION, + true + ); + + wp_localize_script('wptag-admin', 'wptagAdmin', array( + 'ajax_url' => admin_url('admin-ajax.php'), + 'nonce' => wp_create_nonce('wptag_admin_nonce'), + 'strings' => array( + 'validating' => 'Validating...', + 'valid' => 'Valid', + 'invalid' => 'Invalid', + 'preview' => 'Preview', + 'close' => 'Close', + 'export_success' => 'Settings exported successfully', + 'import_success' => 'Settings imported successfully', + 'reset_success' => 'Settings reset successfully', + 'confirm_reset' => 'Are you sure you want to reset all settings? This cannot be undone.', + 'confirm_import' => 'This will overwrite your current settings. Continue?', + 'loading' => 'Loading...' + ) + )); + } + + public function display_admin_page() { + if (!$this->current_user_can_manage_codes() && !$this->current_user_can_manage_services()) { + wp_die(__('You do not have sufficient permissions to access this page.')); + } + + $this->handle_form_submission(); + + $active_tab = isset($_GET['tab']) ? sanitize_text_field($_GET['tab']) : 'analytics'; + $categories = $this->get_categories_with_services(); + + ?> +
+

+ + display_admin_notices(); ?> + +
+
+

Manage your tracking codes and analytics services with ease.

+
+
+ current_user_can_manage_codes()): ?> + + + + +
+
+ + + +
+ current_user_can_manage_services()): ?> + display_services_tab(); ?> + + display_category_tab($active_tab, $categories[$active_tab] ?? array()); ?> + +
+
+ + + + + current_user_can_manage_services()) { + echo '
'; + echo '

You do not have permission to manage services.

'; + echo '
'; + return; + } + + $all_services = $this->config->get_all_services(); + $enabled_services = $this->config->get_enabled_services(); + $categories = array(); + + foreach ($all_services as $service_key => $service_config) { + $categories[$service_config['category']][$service_key] = $service_config; + } + + ?> +
+
+

Enable or disable tracking services. Only enabled services will appear in the category tabs.

+
+ + +
+
+ +
+ + + $category_services): ?> +
+

+
+ $service_config): ?> +
+
+
+ +
+
+

+

+
+
+
+ +
+
+ +
+
+ + +
+ +
+
+
+ config->get_services_config(); + + foreach ($services_config as $service_key => $service_config) { + $category = $service_config['category']; + if (!isset($categories[$category])) { + $categories[$category] = array( + 'title' => ucfirst($category), + 'services' => array() + ); + } + $categories[$category]['services'][$service_key] = $service_config; + } + + return $categories; + } + + private function display_category_tab($active_tab, $category_data) { + if (empty($category_data['services'])) { + echo '
'; + echo '

No services enabled for this category. Enable some services to get started.

'; + echo '
'; + return; + } + + ?> +
+ + +
+ $service_config): ?> + display_service_card($service_key, $service_config); ?> + +
+ +
+ +
+
+ config->get_service_settings($service_key); + $field_key = $service_config['field']; + $can_edit = $this->current_user_can_manage_codes(); + ?> +
+
+
+ +
+
+

+
+ +
+
+
+ +
+
+
+ + +
+
+ +
> +
+ +
+ > + + + + +
+
+
+
+ +
> +
+ +
+ +
+ + +
+
+ +
+ +
+ +
+
+
+ + + +
+ +
+
+
+

%s

', + esc_attr($notice['type']), + esc_html($notice['message']) + ); + } + + delete_transient('wptag_admin_notices'); + } + + private function add_admin_notice($type, $message) { + $notices = get_transient('wptag_admin_notices') ?: array(); + $notices[] = array('type' => $type, 'message' => $message); + set_transient('wptag_admin_notices', $notices, 30); + } + + public function handle_form_submission() { + if (isset($_POST['wptag_services_nonce']) && wp_verify_nonce($_POST['wptag_services_nonce'], 'wptag_save_services')) { + $this->handle_services_form_submission(); + return; + } + + if (!isset($_POST['wptag_nonce']) || !wp_verify_nonce($_POST['wptag_nonce'], 'wptag_save_settings')) { + return; + } + + if (!$this->current_user_can_manage_codes()) { + $this->add_admin_notice('error', 'You do not have permission to manage tracking codes.'); + return; + } + + $settings = $_POST['wptag_settings'] ?? array(); + + if ($this->validator->validate_settings($settings)) { + $result = $this->config->update_settings($settings); + + if ($result) { + $this->add_admin_notice('success', 'Settings saved successfully.'); + } else { + $this->add_admin_notice('error', 'Failed to save settings.'); + } + } else { + $errors = $this->validator->get_error_messages(); + $this->add_admin_notice('error', 'Validation failed: ' . implode(', ', $errors)); + } + + $redirect_url = add_query_arg( + array( + 'page' => 'wptag-settings', + 'tab' => isset($_GET['tab']) ? sanitize_text_field($_GET['tab']) : 'analytics' + ), + admin_url('options-general.php') + ); + + wp_redirect($redirect_url); + exit; + } + + private function handle_services_form_submission() { + if (!$this->current_user_can_manage_services()) { + $this->add_admin_notice('error', 'You do not have permission to manage services.'); + return; + } + + $enabled_services = isset($_POST['enabled_services']) ? array_map('sanitize_text_field', $_POST['enabled_services']) : array(); + + $result = $this->config->update_enabled_services($enabled_services); + + if ($result) { + $this->add_admin_notice('success', 'Services updated successfully.'); + } else { + $this->add_admin_notice('error', 'Failed to update services.'); + } + + wp_redirect(add_query_arg(array('page' => 'wptag-settings', 'tab' => 'services'), admin_url('options-general.php'))); + exit; + } + + public function sanitize_settings($settings) { + return $this->config->sanitize_settings($settings); + } + + public function sanitize_services($services) { + return is_array($services) ? array_map('sanitize_text_field', $services) : array(); + } + + public function ajax_validate_code() { + check_ajax_referer('wptag_admin_nonce', 'nonce'); + + if (!$this->current_user_can_manage_codes()) { + wp_send_json_error(array('message' => 'You do not have permission to validate codes.')); + return; + } + + $service_key = sanitize_text_field($_POST['service']); + $use_template = $_POST['use_template'] === '1'; + + $settings = array( + 'enabled' => true, + 'use_template' => $use_template + ); + + if ($use_template) { + $service_config = $this->config->get_service_config($service_key); + if ($service_config) { + $field_key = $service_config['field']; + $settings[$field_key] = sanitize_text_field($_POST['id_value']); + } + } else { + $settings['custom_code'] = wp_kses_post($_POST['custom_code']); + } + + $is_valid = $this->validator->validate_service_code($service_key, $settings); + + if ($is_valid) { + wp_send_json_success(array('message' => 'Code is valid')); + } else { + $errors = $this->validator->get_error_messages(); + wp_send_json_error(array('message' => implode(', ', $errors))); + } + } + + public function ajax_preview_code() { + check_ajax_referer('wptag_admin_nonce', 'nonce'); + + if (!$this->current_user_can_manage_codes()) { + wp_send_json_error(array('message' => 'You do not have permission to preview codes.')); + return; + } + + $service_key = sanitize_text_field($_POST['service']); + $id_value = sanitize_text_field($_POST['id_value']); + + $preview = $this->output_manager->get_template_preview($service_key, $id_value); + + if (!empty($preview)) { + wp_send_json_success(array('preview' => $preview)); + } else { + wp_send_json_error(array('message' => 'Unable to generate preview')); + } + } + + public function ajax_export_settings() { + check_ajax_referer('wptag_admin_nonce', 'nonce'); + + if (!$this->current_user_can_manage_codes()) { + wp_send_json_error(array('message' => 'You do not have permission to export settings.')); + return; + } + + $export_data = $this->config->export_settings(); + + wp_send_json_success(array( + 'data' => $export_data, + 'filename' => 'wptag-settings-' . date('Y-m-d-H-i-s') . '.json' + )); + } + + public function ajax_import_settings() { + check_ajax_referer('wptag_admin_nonce', 'nonce'); + + if (!$this->current_user_can_manage_codes()) { + wp_send_json_error(array('message' => 'You do not have permission to import settings.')); + return; + } + + $import_data = stripslashes($_POST['import_data']); + + if ($this->validator->validate_import_data($import_data)) { + $result = $this->config->import_settings($import_data); + + if (!is_wp_error($result)) { + wp_send_json_success(array('message' => 'Settings imported successfully')); + } else { + wp_send_json_error(array('message' => $result->get_error_message())); + } + } else { + $errors = $this->validator->get_error_messages(); + wp_send_json_error(array('message' => implode(', ', $errors))); + } + } + + public function ajax_reset_settings() { + check_ajax_referer('wptag_admin_nonce', 'nonce'); + + if (!$this->current_user_can_manage_codes()) { + wp_send_json_error(array('message' => 'You do not have permission to reset settings.')); + return; + } + + $this->config->reset_to_defaults(); + + wp_send_json_success(array('message' => 'Settings reset successfully')); + } + + public function add_action_links($links) { + $settings_link = 'Settings'; + array_unshift($links, $settings_link); + return $links; + } + + public function add_row_meta($links, $file) { + if ($file === plugin_basename(WPTAG_PLUGIN_FILE)) { + $links[] = 'Documentation'; + $links[] = 'Support'; + } + return $links; + } +} \ No newline at end of file diff --git a/includes/class-config.php b/includes/class-config.php new file mode 100644 index 0000000..e1297aa --- /dev/null +++ b/includes/class-config.php @@ -0,0 +1,447 @@ +init_services_config(); + } + + private function init_services_config() { + $this->services_config = array( + 'google_analytics' => array( + 'name' => 'Google Analytics', + 'category' => 'analytics', + 'field' => 'tracking_id', + 'placeholder' => 'G-XXXXXXXXXX or UA-XXXXXXXXX-X', + 'validation_pattern' => '/^(G-[A-Z0-9]{10}|UA-[0-9]+-[0-9]+)$/', + 'default_position' => 'head', + 'template' => 'google_analytics', + 'icon' => 'dashicons-chart-area', + 'description' => 'Track website traffic and user behavior with Google Analytics' + ), + 'google_tag_manager' => array( + 'name' => 'Google Tag Manager', + 'category' => 'analytics', + 'field' => 'container_id', + 'placeholder' => 'GTM-XXXXXXX', + 'validation_pattern' => '/^GTM-[A-Z0-9]{7}$/', + 'default_position' => 'head', + 'template' => 'google_tag_manager', + 'icon' => 'dashicons-tag', + 'description' => 'Manage all your website tags through Google Tag Manager' + ), + 'facebook_pixel' => array( + 'name' => 'Facebook Pixel', + 'category' => 'advertising', + 'field' => 'pixel_id', + 'placeholder' => '123456789012345', + 'validation_pattern' => '/^[0-9]{15}$/', + 'default_position' => 'head', + 'template' => 'facebook_pixel', + 'icon' => 'dashicons-facebook', + 'description' => 'Track conversions and build audiences for Facebook ads' + ), + 'google_ads' => array( + 'name' => 'Google Ads', + 'category' => 'advertising', + 'field' => 'conversion_id', + 'placeholder' => 'AW-123456789', + 'validation_pattern' => '/^AW-[0-9]{10}$/', + 'default_position' => 'head', + 'template' => 'google_ads', + 'icon' => 'dashicons-googleplus', + 'description' => 'Track conversions for Google Ads campaigns' + ), + 'microsoft_clarity' => array( + 'name' => 'Microsoft Clarity', + 'category' => 'analytics', + 'field' => 'project_id', + 'placeholder' => 'abcdefghij', + 'validation_pattern' => '/^[a-z0-9]{10}$/', + 'default_position' => 'head', + 'template' => 'microsoft_clarity', + 'icon' => 'dashicons-visibility', + 'description' => 'Free user behavior analytics with heatmaps and session recordings' + ), + 'hotjar' => array( + 'name' => 'Hotjar', + 'category' => 'analytics', + 'field' => 'site_id', + 'placeholder' => '1234567', + 'validation_pattern' => '/^[0-9]{7}$/', + 'default_position' => 'head', + 'template' => 'hotjar', + 'icon' => 'dashicons-video-alt3', + 'description' => 'Understand user behavior with Hotjar heatmaps and recordings' + ), + 'tiktok_pixel' => array( + 'name' => 'TikTok Pixel', + 'category' => 'advertising', + 'field' => 'pixel_id', + 'placeholder' => 'ABCDEFGHIJKLMNOPQRSTUVWXYZ123456', + 'validation_pattern' => '/^[A-Z0-9]{26}$/', + 'default_position' => 'head', + 'template' => 'tiktok_pixel', + 'icon' => 'dashicons-smartphone', + 'description' => 'Track conversions for TikTok advertising campaigns' + ), + 'linkedin_insight' => array( + 'name' => 'LinkedIn Insight Tag', + 'category' => 'advertising', + 'field' => 'partner_id', + 'placeholder' => '1234567', + 'validation_pattern' => '/^[0-9]{7}$/', + 'default_position' => 'footer', + 'template' => 'linkedin_insight', + 'icon' => 'dashicons-linkedin', + 'description' => 'Track conversions and retarget visitors for LinkedIn ads' + ), + 'twitter_pixel' => array( + 'name' => 'Twitter Pixel', + 'category' => 'advertising', + 'field' => 'pixel_id', + 'placeholder' => 'o1234', + 'validation_pattern' => '/^o[0-9]{4}$/', + 'default_position' => 'head', + 'template' => 'twitter_pixel', + 'icon' => 'dashicons-twitter', + 'description' => 'Track conversions for Twitter advertising campaigns' + ), + 'pinterest_pixel' => array( + 'name' => 'Pinterest Pixel', + 'category' => 'advertising', + 'field' => 'pixel_id', + 'placeholder' => '1234567890123456', + 'validation_pattern' => '/^[0-9]{16}$/', + 'default_position' => 'head', + 'template' => 'pinterest_pixel', + 'icon' => 'dashicons-format-image', + 'description' => 'Track conversions for Pinterest advertising campaigns' + ), + 'snapchat_pixel' => array( + 'name' => 'Snapchat Pixel', + 'category' => 'advertising', + 'field' => 'pixel_id', + 'placeholder' => 'abcdefgh-1234-5678-9012-abcdefghijkl', + 'validation_pattern' => '/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/', + 'default_position' => 'head', + 'template' => 'snapchat_pixel', + 'icon' => 'dashicons-camera', + 'description' => 'Track conversions for Snapchat advertising campaigns' + ), + 'google_optimize' => array( + 'name' => 'Google Optimize', + 'category' => 'analytics', + 'field' => 'container_id', + 'placeholder' => 'GTM-XXXXXXX', + 'validation_pattern' => '/^GTM-[A-Z0-9]{7}$/', + 'default_position' => 'head', + 'template' => 'google_optimize', + 'icon' => 'dashicons-admin-settings', + 'description' => 'A/B testing and website optimization tool' + ), + 'crazyegg' => array( + 'name' => 'Crazy Egg', + 'category' => 'analytics', + 'field' => 'account_id', + 'placeholder' => '12345678', + 'validation_pattern' => '/^[0-9]{8}$/', + 'default_position' => 'head', + 'template' => 'crazyegg', + 'icon' => 'dashicons-admin-tools', + 'description' => 'Heatmap and user session recording tool' + ), + 'mixpanel' => array( + 'name' => 'Mixpanel', + 'category' => 'analytics', + 'field' => 'project_token', + 'placeholder' => 'abcdefghijklmnopqrstuvwxyz123456', + 'validation_pattern' => '/^[a-z0-9]{32}$/', + 'default_position' => 'head', + 'template' => 'mixpanel', + 'icon' => 'dashicons-chart-pie', + 'description' => 'Advanced analytics to understand user behavior' + ), + 'amplitude' => array( + 'name' => 'Amplitude', + 'category' => 'analytics', + 'field' => 'api_key', + 'placeholder' => 'abcdefghijklmnopqrstuvwxyz123456', + 'validation_pattern' => '/^[a-z0-9]{32}$/', + 'default_position' => 'head', + 'template' => 'amplitude', + 'icon' => 'dashicons-chart-bar', + 'description' => 'Product analytics for mobile and web' + ), + 'matomo' => array( + 'name' => 'Matomo', + 'category' => 'analytics', + 'field' => 'site_id', + 'placeholder' => '1', + 'validation_pattern' => '/^[0-9]+$/', + 'default_position' => 'head', + 'template' => 'matomo', + 'icon' => 'dashicons-chart-line', + 'description' => 'Privacy-focused web analytics platform' + ) + ); + } + + public function get_all_services() { + return $this->services_config; + } + + public function get_enabled_services() { + if (null === $this->cached_services) { + $this->cached_services = get_option($this->services_option, array('google_analytics', 'google_tag_manager', 'facebook_pixel', 'google_ads')); + } + return $this->cached_services; + } + + public function update_enabled_services($services) { + $this->cached_services = is_array($services) ? $services : array(); + $result = update_option($this->services_option, $this->cached_services); + + if ($result) { + $this->cached_settings = null; + } + + return $result; + } + + public function get_available_services() { + $enabled_services = $this->get_enabled_services(); + $available = array(); + + foreach ($enabled_services as $service_key) { + if (isset($this->services_config[$service_key])) { + $available[$service_key] = $this->services_config[$service_key]; + } + } + + return $available; + } + + public function get_services_config() { + return $this->get_available_services(); + } + + public function get_service_config($service_key) { + return isset($this->services_config[$service_key]) ? $this->services_config[$service_key] : null; + } + + public function get_services_by_category($category) { + $available_services = $this->get_available_services(); + return array_filter($available_services, function($service) use ($category) { + return $service['category'] === $category; + }); + } + + public function get_categories() { + $available_services = $this->get_available_services(); + $categories = array(); + + foreach ($available_services as $service) { + if (!in_array($service['category'], $categories)) { + $categories[] = $service['category']; + } + } + + return $categories; + } + + public function get_settings() { + if (null === $this->cached_settings) { + $this->cached_settings = get_option($this->option_name, $this->get_default_settings()); + } + return $this->cached_settings; + } + + public function get_service_settings($service_key) { + $settings = $this->get_settings(); + return isset($settings[$service_key]) ? $settings[$service_key] : $this->get_default_service_settings($service_key); + } + + public function update_settings($new_settings) { + $sanitized_settings = $this->sanitize_settings($new_settings); + $result = update_option($this->option_name, $sanitized_settings); + + if ($result) { + $this->cached_settings = $sanitized_settings; + do_action('wptag_settings_updated', $sanitized_settings); + } + + return $result; + } + + public function update_service_settings($service_key, $service_settings) { + $all_settings = $this->get_settings(); + $all_settings[$service_key] = $this->sanitize_service_settings($service_settings); + + return $this->update_settings($all_settings); + } + + private function get_default_settings() { + $defaults = array(); + $available_services = $this->get_available_services(); + + foreach ($available_services as $service_key => $service_config) { + $defaults[$service_key] = $this->get_default_service_settings($service_key); + } + + return $defaults; + } + + private function get_default_service_settings($service_key) { + $service_config = $this->get_service_config($service_key); + if (!$service_config) { + return array(); + } + + $defaults = array( + 'enabled' => false, + 'use_template' => true, + 'custom_code' => '', + 'position' => $service_config['default_position'], + 'priority' => 10, + 'device' => 'all', + 'conditions' => array(), + 'created_at' => current_time('mysql'), + 'updated_at' => current_time('mysql') + ); + + $defaults[$service_config['field']] = ''; + + return $defaults; + } + + public function sanitize_settings($settings) { + $sanitized = array(); + + if (!is_array($settings)) { + return $this->get_default_settings(); + } + + foreach ($settings as $service_key => $service_settings) { + if (isset($this->services_config[$service_key])) { + $sanitized[$service_key] = $this->sanitize_service_settings($service_settings); + } + } + + return $sanitized; + } + + private function sanitize_service_settings($settings) { + $sanitized = array( + 'enabled' => !empty($settings['enabled']), + 'use_template' => isset($settings['use_template']) ? (bool)$settings['use_template'] : true, + 'custom_code' => wp_kses($settings['custom_code'] ?? '', array( + 'script' => array( + 'type' => array(), + 'src' => array(), + 'async' => array(), + 'defer' => array(), + 'id' => array(), + 'class' => array() + ), + 'noscript' => array(), + 'img' => array( + 'src' => array(), + 'alt' => array(), + 'width' => array(), + 'height' => array(), + 'style' => array() + ), + 'iframe' => array( + 'src' => array(), + 'width' => array(), + 'height' => array(), + 'style' => array() + ) + )), + 'position' => sanitize_text_field($settings['position'] ?? 'head'), + 'priority' => intval($settings['priority'] ?? 10), + 'device' => sanitize_text_field($settings['device'] ?? 'all'), + 'conditions' => is_array($settings['conditions'] ?? array()) ? $settings['conditions'] : array(), + 'updated_at' => current_time('mysql') + ); + + foreach ($this->services_config as $service_key => $service_config) { + $field_key = $service_config['field']; + if (isset($settings[$field_key])) { + $sanitized[$field_key] = sanitize_text_field($settings[$field_key]); + } + } + + return $sanitized; + } + + public function install_default_settings() { + $existing_settings = get_option($this->option_name, array()); + $existing_services = get_option($this->services_option, array()); + + if (empty($existing_settings)) { + add_option($this->option_name, $this->get_default_settings()); + } + + if (empty($existing_services)) { + add_option($this->services_option, array('google_analytics', 'google_tag_manager', 'facebook_pixel', 'google_ads')); + } + } + + public function reset_to_defaults() { + delete_option($this->option_name); + delete_option($this->services_option); + $this->cached_settings = null; + $this->cached_services = null; + $this->install_default_settings(); + + do_action('wptag_settings_reset'); + } + + public function export_settings() { + $settings = $this->get_settings(); + $services = $this->get_enabled_services(); + + $export_data = array( + 'version' => WPTAG_VERSION, + 'exported_at' => current_time('mysql'), + 'services' => $services, + 'settings' => $settings + ); + + return wp_json_encode($export_data, JSON_PRETTY_PRINT); + } + + public function import_settings($json_data) { + $data = json_decode($json_data, true); + + if (!is_array($data) || !isset($data['settings'])) { + return new \WP_Error('invalid_data', 'Invalid import data format'); + } + + $settings_result = $this->update_settings($data['settings']); + + if (isset($data['services'])) { + $services_result = $this->update_enabled_services($data['services']); + } + + if ($settings_result) { + do_action('wptag_settings_imported', $data['settings']); + return true; + } + + return new \WP_Error('import_failed', 'Failed to import settings'); + } +} \ No newline at end of file diff --git a/includes/class-frontend.php b/includes/class-frontend.php new file mode 100644 index 0000000..bb33e28 --- /dev/null +++ b/includes/class-frontend.php @@ -0,0 +1,69 @@ +config = $config; + $this->output_manager = new Output_Manager($config); + + $this->init_hooks(); + } + + private function init_hooks() { + add_action('wp_head', array($this, 'output_head_codes'), 1); + add_action('wp_body_open', array($this, 'output_body_codes'), 1); + add_action('wp_footer', array($this, 'output_footer_codes'), 1); + add_action('wp_enqueue_scripts', array($this, 'enqueue_frontend_assets')); + } + + public function output_head_codes() { + if (!$this->should_output_codes()) { + return; + } + + $this->output_manager->output_codes('head'); + } + + public function output_body_codes() { + if (!$this->should_output_codes()) { + return; + } + + $this->output_manager->output_codes('body'); + } + + public function output_footer_codes() { + if (!$this->should_output_codes()) { + return; + } + + $this->output_manager->output_codes('footer'); + } + + private function should_output_codes() { + if (is_admin()) { + return false; + } + + if (is_user_logged_in() && current_user_can('manage_options')) { + $show_for_admin = apply_filters('wptag_show_for_admin', false); + if (!$show_for_admin) { + return false; + } + } + + return apply_filters('wptag_should_output_codes', true); + } + + public function enqueue_frontend_assets() { + do_action('wptag_enqueue_frontend_assets'); + } +} \ No newline at end of file diff --git a/includes/class-loader.php b/includes/class-loader.php new file mode 100644 index 0000000..5ad0620 --- /dev/null +++ b/includes/class-loader.php @@ -0,0 +1,51 @@ +actions = $this->add($this->actions, $hook, $component, $callback, $priority, $accepted_args); + } + + public function add_filter($hook, $component, $callback, $priority = 10, $accepted_args = 1) { + $this->filters = $this->add($this->filters, $hook, $component, $callback, $priority, $accepted_args); + } + + public function add_shortcode($tag, $component, $callback) { + $this->shortcodes = $this->add($this->shortcodes, $tag, $component, $callback); + } + + private function add($hooks, $hook, $component, $callback, $priority = 10, $accepted_args = 1) { + $hooks[] = array( + 'hook' => $hook, + 'component' => $component, + 'callback' => $callback, + 'priority' => $priority, + 'accepted_args' => $accepted_args + ); + + return $hooks; + } + + public function run() { + foreach ($this->filters as $hook) { + add_filter($hook['hook'], array($hook['component'], $hook['callback']), $hook['priority'], $hook['accepted_args']); + } + + foreach ($this->actions as $hook) { + add_action($hook['hook'], array($hook['component'], $hook['callback']), $hook['priority'], $hook['accepted_args']); + } + + foreach ($this->shortcodes as $hook) { + add_shortcode($hook['hook'], array($hook['component'], $hook['callback'])); + } + } +} \ No newline at end of file diff --git a/includes/class-output-manager.php b/includes/class-output-manager.php new file mode 100644 index 0000000..8ef15bc --- /dev/null +++ b/includes/class-output-manager.php @@ -0,0 +1,479 @@ +config = $config; + $this->init_templates(); + } + + private function init_templates() { + $this->templates = array( + 'google_analytics' => array( + 'G-' => ' + +', + 'UA-' => ' + +' + ), + 'google_tag_manager' => array( + 'head' => ' +', + 'body' => ' +' + ), + 'facebook_pixel' => ' + +', + 'google_ads' => ' + +', + 'microsoft_clarity' => ' +', + 'hotjar' => ' +', + 'tiktok_pixel' => ' +', + 'linkedin_insight' => ' + +', + 'twitter_pixel' => ' +', + 'pinterest_pixel' => ' + +', + 'snapchat_pixel' => ' +', + 'google_optimize' => ' +', + 'crazyegg' => ' +', + 'mixpanel' => ' +', + 'amplitude' => ' +', + 'matomo' => ' +', + ); + } + + public function get_codes_for_position($position) { + if (isset($this->cached_codes[$position])) { + return $this->cached_codes[$position]; + } + + $codes = array(); + $all_settings = $this->config->get_settings(); + + foreach ($all_settings as $service_key => $service_settings) { + if (!$this->should_output_service($service_key, $service_settings, $position)) { + continue; + } + + $code = $this->get_service_code($service_key, $service_settings, $position); + if (!empty($code)) { + $priority = intval($service_settings['priority']); + $codes[$priority][] = array( + 'service' => $service_key, + 'code' => $code, + 'priority' => $priority + ); + } + } + + ksort($codes); + + $output_codes = array(); + foreach ($codes as $priority_codes) { + foreach ($priority_codes as $code_data) { + $output_codes[] = $code_data['code']; + } + } + + $this->cached_codes[$position] = $output_codes; + return $output_codes; + } + + private function should_output_service($service_key, $service_settings, $position) { + if (!$service_settings['enabled']) { + return false; + } + + if ($service_settings['position'] !== $position) { + return false; + } + + if (!$this->check_device_condition($service_settings['device'])) { + return false; + } + + if (!$this->check_page_conditions($service_settings['conditions'] ?? array())) { + return false; + } + + return apply_filters('wptag_should_output_service', true, $service_key, $service_settings, $position); + } + + private function check_device_condition($device_setting) { + if ($device_setting === 'all') { + return true; + } + + $is_mobile = wp_is_mobile(); + + if ($device_setting === 'mobile' && $is_mobile) { + return true; + } + + if ($device_setting === 'desktop' && !$is_mobile) { + return true; + } + + return false; + } + + private function check_page_conditions($conditions) { + if (empty($conditions)) { + return true; + } + + foreach ($conditions as $condition) { + if (!$this->check_single_condition($condition)) { + return false; + } + } + + return true; + } + + private function check_single_condition($condition) { + $type = $condition['type'] ?? ''; + $value = $condition['value'] ?? ''; + $operator = $condition['operator'] ?? 'is'; + + switch ($type) { + case 'page_type': + return $this->check_page_type_condition($value, $operator); + case 'post_type': + return $this->check_post_type_condition($value, $operator); + case 'category': + return $this->check_category_condition($value, $operator); + case 'tag': + return $this->check_tag_condition($value, $operator); + case 'user_role': + return $this->check_user_role_condition($value, $operator); + default: + return true; + } + } + + private function check_page_type_condition($value, $operator) { + $current_page_type = $this->get_current_page_type(); + + if ($operator === 'is') { + return $current_page_type === $value; + } elseif ($operator === 'is_not') { + return $current_page_type !== $value; + } + + return true; + } + + private function get_current_page_type() { + if (is_home()) return 'home'; + if (is_front_page()) return 'front_page'; + if (is_single()) return 'single'; + if (is_page()) return 'page'; + if (is_category()) return 'category'; + if (is_tag()) return 'tag'; + if (is_archive()) return 'archive'; + if (is_search()) return 'search'; + if (is_404()) return '404'; + + return 'unknown'; + } + + private function check_post_type_condition($value, $operator) { + $post_type = get_post_type(); + + if ($operator === 'is') { + return $post_type === $value; + } elseif ($operator === 'is_not') { + return $post_type !== $value; + } + + return true; + } + + private function check_category_condition($value, $operator) { + if (is_category($value)) { + return $operator === 'is'; + } elseif (is_single()) { + $has_category = has_category($value); + return $operator === 'is' ? $has_category : !$has_category; + } + + return $operator === 'is_not'; + } + + private function check_tag_condition($value, $operator) { + if (is_tag($value)) { + return $operator === 'is'; + } elseif (is_single()) { + $has_tag = has_tag($value); + return $operator === 'is' ? $has_tag : !$has_tag; + } + + return $operator === 'is_not'; + } + + private function check_user_role_condition($value, $operator) { + $user = wp_get_current_user(); + $has_role = in_array($value, $user->roles); + + return $operator === 'is' ? $has_role : !$has_role; + } + + private function get_service_code($service_key, $service_settings, $position) { + if ($service_settings['use_template']) { + return $this->get_template_code($service_key, $service_settings, $position); + } else { + return $service_settings['custom_code']; + } + } + + private function get_template_code($service_key, $service_settings, $position) { + $service_config = $this->config->get_service_config($service_key); + if (!$service_config) { + return ''; + } + + $template = $this->get_template_for_service($service_key, $service_settings, $position); + if (empty($template)) { + return ''; + } + + $field_key = $service_config['field']; + $id_value = $service_settings[$field_key] ?? ''; + + if (empty($id_value)) { + return ''; + } + + $code = str_replace('{ID}', esc_attr($id_value), $template); + + return apply_filters('wptag_template_code', $code, $service_key, $service_settings, $position); + } + + private function get_template_for_service($service_key, $service_settings, $position) { + if (!isset($this->templates[$service_key])) { + return ''; + } + + $template_data = $this->templates[$service_key]; + + if ($service_key === 'google_analytics') { + $service_config = $this->config->get_service_config($service_key); + $field_key = $service_config['field']; + $id_value = $service_settings[$field_key] ?? ''; + + if (strpos($id_value, 'G-') === 0) { + return $template_data['G-']; + } elseif (strpos($id_value, 'UA-') === 0) { + return $template_data['UA-']; + } + + return ''; + } + + if ($service_key === 'google_tag_manager') { + if ($position === 'head') { + return $template_data['head']; + } elseif ($position === 'body') { + return $template_data['body']; + } + + return ''; + } + + if (is_array($template_data)) { + return $template_data['default'] ?? ''; + } + + return $template_data; + } + + public function output_codes($position) { + $codes = $this->get_codes_for_position($position); + + if (empty($codes)) { + return; + } + + $output = "\n\n"; + + foreach ($codes as $code) { + $output .= $code . "\n"; + } + + $output .= "\n"; + + echo apply_filters('wptag_output_codes', $output, $position); + } + + public function clear_cache() { + $this->cached_codes = array(); + } + + public function get_template_preview($service_key, $id_value) { + $service_config = $this->config->get_service_config($service_key); + if (!$service_config) { + return ''; + } + + $template = $this->get_template_for_service($service_key, array($service_config['field'] => $id_value), 'head'); + if (empty($template)) { + return ''; + } + + return str_replace('{ID}', esc_attr($id_value), $template); + } +} \ No newline at end of file diff --git a/includes/class-validator.php b/includes/class-validator.php new file mode 100644 index 0000000..ac9da90 --- /dev/null +++ b/includes/class-validator.php @@ -0,0 +1,393 @@ +config = $config ?: new Config(); + } + + public function validate_service_code($service_key, $settings) { + $this->errors = array(); + + $service_config = $this->config->get_service_config($service_key); + if (!$service_config) { + $this->add_error('service_not_found', 'Service configuration not found'); + return false; + } + + if (!$settings['enabled']) { + return true; + } + + if ($settings['use_template']) { + return $this->validate_template_code($service_key, $settings, $service_config); + } else { + return $this->validate_custom_code($settings['custom_code']); + } + } + + private function validate_template_code($service_key, $settings, $service_config) { + $field_key = $service_config['field']; + $id_value = $settings[$field_key] ?? ''; + + if (empty($id_value)) { + $this->add_error('empty_id', 'ID field cannot be empty'); + return false; + } + + if (!$this->validate_id_format($service_key, $id_value, $service_config)) { + return false; + } + + return true; + } + + private function validate_id_format($service_key, $id_value, $service_config) { + $pattern = $service_config['validation_pattern'] ?? null; + + if (!$pattern) { + return true; + } + + if (!preg_match($pattern, $id_value)) { + $this->add_error('invalid_id_format', sprintf('Invalid ID format for %s', $service_config['name'])); + return false; + } + + if ($service_key === 'google_analytics') { + return $this->validate_google_analytics_id($id_value); + } + + return true; + } + + private function validate_google_analytics_id($id_value) { + if (strpos($id_value, 'G-') === 0) { + if (!preg_match('/^G-[A-Z0-9]{10}$/', $id_value)) { + $this->add_error('invalid_ga4_format', 'Invalid Google Analytics 4 ID format'); + return false; + } + } elseif (strpos($id_value, 'UA-') === 0) { + if (!preg_match('/^UA-[0-9]+-[0-9]+$/', $id_value)) { + $this->add_error('invalid_ua_format', 'Invalid Universal Analytics ID format'); + return false; + } + } else { + $this->add_error('invalid_ga_format', 'Google Analytics ID must start with G- or UA-'); + return false; + } + + return true; + } + + private function validate_custom_code($custom_code) { + if (empty($custom_code)) { + $this->add_error('empty_custom_code', 'Custom code cannot be empty'); + return false; + } + + if (strlen($custom_code) > 50000) { + $this->add_error('code_too_long', 'Custom code is too long (max 50,000 characters)'); + return false; + } + + if (!$this->validate_script_structure($custom_code)) { + return false; + } + + if (!$this->validate_code_security($custom_code)) { + return false; + } + + return true; + } + + private function validate_script_structure($custom_code) { + $has_script_tag = strpos($custom_code, ''); + + if ($script_open_count !== $script_close_count) { + $this->add_error('mismatched_script_tags', 'Mismatched script tags'); + return false; + } + } + + if ($has_noscript_tag) { + $noscript_open_count = substr_count($custom_code, ''); + + if ($noscript_open_count !== $noscript_close_count) { + $this->add_error('mismatched_noscript_tags', 'Mismatched noscript tags'); + return false; + } + } + + return true; + } + + private function validate_code_security($custom_code) { + $dangerous_patterns = array( + '/\beval\s*\(/i' => 'eval() function is not allowed', + '/\bFunction\s*\(/i' => 'Function() constructor is not allowed', + '/\bsetTimeout\s*\(\s*["\']/' => 'setTimeout with string argument is not allowed', + '/\bsetInterval\s*\(\s*["\']/' => 'setInterval with string argument is not allowed', + '/\bdocument\.write\s*\(/i' => 'document.write() is discouraged', + '/\bwindow\.location\s*=/' => 'Redirecting window.location is not allowed', + '/\bwindow\.open\s*\(/i' => 'window.open() is not allowed', + '/\balert\s*\(/i' => 'alert() is not allowed', + '/\bconfirm\s*\(/i' => 'confirm() is not allowed', + '/\bprompt\s*\(/i' => 'prompt() is not allowed', + '/javascript\s*:/i' => 'javascript: protocol is not allowed', + '/\<\s*iframe[^>]*src\s*=\s*["\']?javascript:/i' => 'javascript: in iframe src is not allowed', + '/\<\s*object[^>]*data\s*=\s*["\']?javascript:/i' => 'javascript: in object data is not allowed' + ); + + foreach ($dangerous_patterns as $pattern => $message) { + if (preg_match($pattern, $custom_code)) { + $this->add_error('security_violation', $message); + return false; + } + } + + if (preg_match_all('/https?:\/\/([^\/\s"\']+)/i', $custom_code, $matches)) { + $domains = $matches[1]; + $suspicious_domains = array( + 'bit.ly', 'tinyurl.com', 'goo.gl', 't.co', 'ow.ly', + 'malware.com', 'virus.com', 'phishing.com' + ); + + foreach ($domains as $domain) { + if (in_array(strtolower($domain), $suspicious_domains)) { + $this->add_error('suspicious_domain', 'Suspicious domain detected: ' . $domain); + return false; + } + } + } + + return true; + } + + public function validate_settings($settings) { + $this->errors = array(); + + if (!is_array($settings)) { + $this->add_error('invalid_settings_format', 'Settings must be an array'); + return false; + } + + $valid = true; + + foreach ($settings as $service_key => $service_settings) { + if (!$this->validate_service_settings($service_key, $service_settings)) { + $valid = false; + } + } + + return $valid; + } + + private function validate_service_settings($service_key, $service_settings) { + $service_config = $this->config->get_service_config($service_key); + if (!$service_config) { + $this->add_error($service_key . '_not_found', 'Service configuration not found for ' . $service_key); + return false; + } + + $valid = true; + + if (!$this->validate_boolean($service_settings['enabled'] ?? false)) { + $this->add_error($service_key . '_enabled_invalid', 'Enabled setting must be boolean'); + $valid = false; + } + + if (!$this->validate_boolean($service_settings['use_template'] ?? true)) { + $this->add_error($service_key . '_use_template_invalid', 'Use template setting must be boolean'); + $valid = false; + } + + if (!$this->validate_position($service_settings['position'] ?? 'head')) { + $this->add_error($service_key . '_position_invalid', 'Invalid position setting'); + $valid = false; + } + + if (!$this->validate_priority($service_settings['priority'] ?? 10)) { + $this->add_error($service_key . '_priority_invalid', 'Priority must be between 1 and 100'); + $valid = false; + } + + if (!$this->validate_device($service_settings['device'] ?? 'all')) { + $this->add_error($service_key . '_device_invalid', 'Invalid device setting'); + $valid = false; + } + + return $valid; + } + + private function validate_boolean($value) { + return is_bool($value) || $value === '1' || $value === '0' || $value === 1 || $value === 0; + } + + private function validate_position($position) { + $valid_positions = array('head', 'body', 'footer'); + return in_array($position, $valid_positions); + } + + private function validate_priority($priority) { + $priority = intval($priority); + return $priority >= 1 && $priority <= 100; + } + + private function validate_device($device) { + $valid_devices = array('all', 'desktop', 'mobile'); + return in_array($device, $valid_devices); + } + + public function validate_import_data($json_data) { + $this->errors = array(); + + $data = json_decode($json_data, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + $this->add_error('invalid_json', 'Invalid JSON format: ' . json_last_error_msg()); + return false; + } + + if (!is_array($data)) { + $this->add_error('invalid_data_type', 'Import data must be an array'); + return false; + } + + if (!isset($data['settings'])) { + $this->add_error('missing_settings', 'Import data missing settings'); + return false; + } + + if (isset($data['version'])) { + if (!$this->validate_version_compatibility($data['version'])) { + return false; + } + } + + if (isset($data['services'])) { + if (!$this->validate_services_list($data['services'])) { + return false; + } + } + + return $this->validate_settings($data['settings']); + } + + private function validate_version_compatibility($version) { + $current_version = WPTAG_VERSION; + + $import_version_parts = explode('.', $version); + $current_version_parts = explode('.', $current_version); + + if (count($import_version_parts) !== 3 || count($current_version_parts) !== 3) { + $this->add_error('invalid_version_format', 'Invalid version format'); + return false; + } + + $import_major = intval($import_version_parts[0]); + $current_major = intval($current_version_parts[0]); + + if ($import_major > $current_major) { + $this->add_error('version_incompatible', 'Import data is from a newer version and may not be compatible'); + return false; + } + + return true; + } + + private function validate_services_list($services) { + if (!is_array($services)) { + $this->add_error('invalid_services_format', 'Services list must be an array'); + return false; + } + + $all_services = $this->config->get_all_services(); + + foreach ($services as $service_key) { + if (!isset($all_services[$service_key])) { + $this->add_error('unknown_service', 'Unknown service: ' . $service_key); + return false; + } + } + + return true; + } + + private function add_error($code, $message) { + $this->errors[] = array( + 'code' => $code, + 'message' => $message + ); + } + + public function get_errors() { + return $this->errors; + } + + public function get_error_messages() { + return array_column($this->errors, 'message'); + } + + public function has_errors() { + return !empty($this->errors); + } + + public function get_last_error() { + if (empty($this->errors)) { + return null; + } + return end($this->errors); + } + + public function clear_errors() { + $this->errors = array(); + } + + public function get_error_count() { + return count($this->errors); + } + + public function validate_tracking_id($service_key, $id_value) { + $service_config = $this->config->get_service_config($service_key); + if (!$service_config) { + return false; + } + + $pattern = $service_config['validation_pattern'] ?? null; + if (!$pattern) { + return true; + } + + return preg_match($pattern, $id_value); + } + + public function sanitize_tracking_id($service_key, $id_value) { + $id_value = trim($id_value); + $id_value = sanitize_text_field($id_value); + + $id_value = wp_kses($id_value, array()); + + return $id_value; + } + + public function get_validation_pattern($service_key) { + $service_config = $this->config->get_service_config($service_key); + return $service_config['validation_pattern'] ?? null; + } +} \ No newline at end of file diff --git a/includes/class-wptag-cache-manager.php b/includes/class-wptag-cache-manager.php deleted file mode 100644 index c90d5ff..0000000 --- a/includes/class-wptag-cache-manager.php +++ /dev/null @@ -1,160 +0,0 @@ -cache_enabled = !defined('WPTAG_DISABLE_CACHE') || !WPTAG_DISABLE_CACHE; - $this->ttl = defined('WPTAG_CACHE_TTL') ? WPTAG_CACHE_TTL : 3600; - } - - public function get($key) { - if (!$this->cache_enabled) { - return false; - } - - return wp_cache_get($key, $this->cache_group); - } - - public function set($key, $value, $ttl = null) { - if (!$this->cache_enabled) { - return false; - } - - $ttl = $ttl ?? $this->ttl; - return wp_cache_set($key, $value, $this->cache_group, $ttl); - } - - public function delete($key) { - return wp_cache_delete($key, $this->cache_group); - } - - public function flush() { - wp_cache_flush(); - } - - public function clear_snippet_cache($snippet_id = null) { - if ($snippet_id) { - $this->delete('snippet_' . $snippet_id); - } - - $this->delete('active_snippets'); - $this->clear_output_cache(); - } - - public function clear_output_cache() { - $positions = ['head', 'footer', 'before_content', 'after_content']; - - foreach ($positions as $position) { - $this->delete_by_prefix('output_' . $position); - } - } - - public function clear_condition_cache() { - $this->delete_by_prefix('condition_'); - } - - private function delete_by_prefix($prefix) { - global $wp_object_cache; - - if (method_exists($wp_object_cache, 'delete_by_group')) { - $wp_object_cache->delete_by_group($this->cache_group); - } else { - $this->delete($prefix); - } - } - - public function warm_cache() { - if (!$this->cache_enabled) { - return; - } - - global $wpdb; - $table = $wpdb->prefix . 'wptag_snippets'; - - $active_snippets = $wpdb->get_results( - "SELECT * FROM $table WHERE status = 1 ORDER BY priority ASC", - ARRAY_A - ); - - $by_position = []; - foreach ($active_snippets as $snippet) { - $position = $snippet['position']; - if (!isset($by_position[$position])) { - $by_position[$position] = []; - } - $by_position[$position][] = $snippet; - } - - foreach ($by_position as $position => $snippets) { - $this->set('snippets_' . $position, $snippets); - } - - $this->set('active_snippets', $active_snippets); - } - - public function get_cache_stats() { - global $wp_object_cache; - - $stats = [ - 'enabled' => $this->cache_enabled, - 'ttl' => $this->ttl, - 'hits' => 0, - 'misses' => 0, - 'size' => 0 - ]; - - if (method_exists($wp_object_cache, 'stats')) { - $cache_stats = $wp_object_cache->stats(); - $stats['hits'] = $cache_stats['hits'] ?? 0; - $stats['misses'] = $cache_stats['misses'] ?? 0; - } - - return $stats; - } - - public function schedule_cleanup() { - if (!wp_next_scheduled('wptag_cache_cleanup')) { - wp_schedule_event(time(), 'daily', 'wptag_cache_cleanup'); - } - - add_action('wptag_cache_cleanup', [$this, 'cleanup_expired']); - } - - public function cleanup_expired() { - global $wpdb; - $logs_table = $wpdb->prefix . 'wptag_logs'; - - $thirty_days_ago = date('Y-m-d H:i:s', strtotime('-30 days')); - - $wpdb->query($wpdb->prepare( - "DELETE FROM $logs_table WHERE created_at < %s", - $thirty_days_ago - )); - - $this->clear_output_cache(); - } - - public function invalidate_on_save($post_id) { - if (wp_is_post_revision($post_id) || wp_is_post_autosave($post_id)) { - return; - } - - $this->clear_output_cache(); - } - - public function init_hooks() { - add_action('save_post', [$this, 'invalidate_on_save']); - add_action('switch_theme', [$this, 'flush']); - add_action('activated_plugin', [$this, 'flush']); - add_action('deactivated_plugin', [$this, 'flush']); - - $this->schedule_cleanup(); - } -} diff --git a/includes/class-wptag-condition-engine.php b/includes/class-wptag-condition-engine.php deleted file mode 100644 index 1d91720..0000000 --- a/includes/class-wptag-condition-engine.php +++ /dev/null @@ -1,311 +0,0 @@ -context_cache[$cache_key])) { - return $this->context_cache[$cache_key]; - } - - $result = $this->process_condition_group($conditions, $context); - $this->context_cache[$cache_key] = $result; - - return $result; - } - - private function process_condition_group($conditions, $context) { - $logic = $conditions['logic'] ?? 'AND'; - $rules = $conditions['rules'] ?? []; - - if (empty($rules)) { - return true; - } - - $results = []; - foreach ($rules as $rule) { - if (isset($rule['rules'])) { - $results[] = $this->process_condition_group($rule, $context); - } else { - $results[] = $this->evaluate_single_condition($rule, $context); - } - } - - if ($logic === 'OR') { - return in_array(true, $results, true); - } else { - return !in_array(false, $results, true); - } - } - - private function evaluate_single_condition($rule, $context) { - $type = $rule['type'] ?? ''; - $operator = $rule['operator'] ?? 'equals'; - $value = $rule['value'] ?? ''; - - switch ($type) { - case 'page_type': - return $this->check_page_type($value, $operator); - - case 'user_status': - return $this->check_user_status($value, $operator); - - case 'user_role': - return $this->check_user_role($value, $operator); - - case 'device_type': - return $this->check_device_type($value, $operator); - - case 'post_id': - return $this->check_post_id($value, $operator); - - case 'category': - return $this->check_category($value, $operator); - - case 'tag': - return $this->check_tag($value, $operator); - - case 'url': - return $this->check_url($value, $operator); - - case 'date_range': - return $this->check_date_range($value, $operator); - - case 'time': - return $this->check_time($value, $operator); - - case 'day_of_week': - return $this->check_day_of_week($value, $operator); - - default: - return apply_filters('wptag_custom_condition', true, $type, $value, $operator, $context); - } - } - - private function check_page_type($value, $operator) { - $page_types = [ - 'home' => is_home() || is_front_page(), - 'single' => is_single(), - 'page' => is_page(), - 'archive' => is_archive(), - 'category' => is_category(), - 'tag' => is_tag(), - 'search' => is_search(), - '404' => is_404(), - 'author' => is_author(), - 'date' => is_date() - ]; - - $is_type = $page_types[$value] ?? false; - - return $operator === 'not_equals' ? !$is_type : $is_type; - } - - private function check_user_status($value, $operator) { - $is_logged_in = is_user_logged_in(); - - if ($value === 'logged_in') { - return $operator === 'not_equals' ? !$is_logged_in : $is_logged_in; - } else { - return $operator === 'not_equals' ? $is_logged_in : !$is_logged_in; - } - } - - private function check_user_role($value, $operator) { - if (!is_user_logged_in()) { - return $operator === 'not_equals'; - } - - $user = wp_get_current_user(); - $has_role = in_array($value, $user->roles); - - return $operator === 'not_equals' ? !$has_role : $has_role; - } - - private function check_device_type($value, $operator) { - $user_agent = $_SERVER['HTTP_USER_AGENT'] ?? ''; - $is_mobile = wp_is_mobile(); - - $device_checks = [ - 'mobile' => $is_mobile && !$this->is_tablet($user_agent), - 'tablet' => $this->is_tablet($user_agent), - 'desktop' => !$is_mobile - ]; - - $is_device = $device_checks[$value] ?? false; - - return $operator === 'not_equals' ? !$is_device : $is_device; - } - - private function is_tablet($user_agent) { - $tablet_patterns = '/iPad|Android.*Tablet|Tablet.*Android|Kindle|Silk|Galaxy Tab/i'; - return preg_match($tablet_patterns, $user_agent); - } - - private function check_post_id($value, $operator) { - $current_id = get_the_ID(); - $ids = array_map('intval', explode(',', $value)); - - switch ($operator) { - case 'equals': - return in_array($current_id, $ids); - case 'not_equals': - return !in_array($current_id, $ids); - case 'greater_than': - return $current_id > $ids[0]; - case 'less_than': - return $current_id < $ids[0]; - default: - return false; - } - } - - private function check_category($value, $operator) { - if (!is_single() && !is_category()) { - return $operator === 'not_equals'; - } - - $categories = explode(',', $value); - $has_category = false; - - if (is_single()) { - foreach ($categories as $cat) { - if (has_category($cat)) { - $has_category = true; - break; - } - } - } elseif (is_category()) { - $current_cat = get_queried_object(); - $has_category = in_array($current_cat->slug, $categories) || - in_array($current_cat->term_id, $categories); - } - - return $operator === 'not_equals' ? !$has_category : $has_category; - } - - private function check_tag($value, $operator) { - if (!is_single() && !is_tag()) { - return $operator === 'not_equals'; - } - - $tags = explode(',', $value); - $has_tag = false; - - if (is_single()) { - foreach ($tags as $tag) { - if (has_tag($tag)) { - $has_tag = true; - break; - } - } - } elseif (is_tag()) { - $current_tag = get_queried_object(); - $has_tag = in_array($current_tag->slug, $tags) || - in_array($current_tag->term_id, $tags); - } - - return $operator === 'not_equals' ? !$has_tag : $has_tag; - } - - private function check_url($value, $operator) { - $current_url = $_SERVER['REQUEST_URI'] ?? ''; - - switch ($operator) { - case 'contains': - return strpos($current_url, $value) !== false; - case 'not_contains': - return strpos($current_url, $value) === false; - case 'equals': - return $current_url === $value; - case 'not_equals': - return $current_url !== $value; - case 'starts_with': - return strpos($current_url, $value) === 0; - case 'ends_with': - return substr($current_url, -strlen($value)) === $value; - default: - return false; - } - } - - private function check_date_range($value, $operator) { - $current_time = current_time('timestamp'); - $dates = explode('|', $value); - - if (count($dates) !== 2) { - return false; - } - - $start_date = strtotime($dates[0]); - $end_date = strtotime($dates[1] . ' 23:59:59'); - - $in_range = $current_time >= $start_date && $current_time <= $end_date; - - return $operator === 'not_in' ? !$in_range : $in_range; - } - - private function check_time($value, $operator) { - $current_time = current_time('H:i'); - $times = explode('|', $value); - - if (count($times) !== 2) { - return false; - } - - $in_range = $current_time >= $times[0] && $current_time <= $times[1]; - - return $operator === 'not_in' ? !$in_range : $in_range; - } - - private function check_day_of_week($value, $operator) { - $current_day = strtolower(current_time('l')); - $days = array_map('strtolower', explode(',', $value)); - - $is_day = in_array($current_day, $days); - - return $operator === 'not_in' ? !$is_day : $is_day; - } - - public function get_condition_types() { - return [ - 'page_type' => [ - 'label' => __('Page Type', 'wptag'), - 'values' => [ - 'home' => __('Home Page', 'wptag'), - 'single' => __('Single Post', 'wptag'), - 'page' => __('Page', 'wptag'), - 'archive' => __('Archive', 'wptag'), - 'category' => __('Category', 'wptag'), - 'tag' => __('Tag', 'wptag'), - 'search' => __('Search', 'wptag'), - '404' => __('404 Page', 'wptag') - ] - ], - 'user_status' => [ - 'label' => __('User Status', 'wptag'), - 'values' => [ - 'logged_in' => __('Logged In', 'wptag'), - 'logged_out' => __('Logged Out', 'wptag') - ] - ], - 'device_type' => [ - 'label' => __('Device Type', 'wptag'), - 'values' => [ - 'mobile' => __('Mobile', 'wptag'), - 'tablet' => __('Tablet', 'wptag'), - 'desktop' => __('Desktop', 'wptag') - ] - ] - ]; - } -} diff --git a/includes/class-wptag-core.php b/includes/class-wptag-core.php deleted file mode 100644 index eac4dbb..0000000 --- a/includes/class-wptag-core.php +++ /dev/null @@ -1,267 +0,0 @@ -load_dependencies(); - $this->init(); - } - - private function load_dependencies() { - require_once WPTAG_PLUGIN_DIR . 'includes/class-wptag-snippet-manager.php'; - require_once WPTAG_PLUGIN_DIR . 'includes/class-wptag-condition-engine.php'; - require_once WPTAG_PLUGIN_DIR . 'includes/class-wptag-output-controller.php'; - require_once WPTAG_PLUGIN_DIR . 'includes/class-wptag-template-manager.php'; - require_once WPTAG_PLUGIN_DIR . 'includes/class-wptag-cache-manager.php'; - - if (is_admin()) { - require_once WPTAG_PLUGIN_DIR . 'admin/class-wptag-admin-controller.php'; - require_once WPTAG_PLUGIN_DIR . 'admin/class-wptag-ajax-handler.php'; - require_once WPTAG_PLUGIN_DIR . 'admin/class-wptag-admin-interface.php'; - } - } - - private function init() { - $this->snippet_manager = new WPTag_Snippet_Manager(); - $this->condition_engine = new WPTag_Condition_Engine(); - $this->template_manager = new WPTag_Template_Manager(); - $this->cache_manager = new WPTag_Cache_Manager(); - $this->output_controller = new WPTag_Output_Controller( - $this->snippet_manager, - $this->condition_engine, - $this->cache_manager - ); - - if (is_admin()) { - new WPTag_Admin_Controller($this->snippet_manager, $this->template_manager); - new WPTag_Ajax_Handler($this->snippet_manager, $this->template_manager); - } else { - $this->register_output_hooks(); - } - - add_action('init', [$this, 'check_version']); - } - - private function register_output_hooks() { - add_action('wp_head', [$this->output_controller, 'render_head'], 1); - add_action('wp_footer', [$this->output_controller, 'render_footer'], 999); - add_filter('the_content', [$this->output_controller, 'filter_content'], 10); - } - - public function check_version() { - $installed_version = get_option('wptag_db_version'); - if ($installed_version !== WPTAG_DB_VERSION) { - self::create_tables(); - update_option('wptag_db_version', WPTAG_DB_VERSION); - } - } - - public static function activate() { - if (version_compare(PHP_VERSION, '8.0', '<')) { - deactivate_plugins(plugin_basename(WPTAG_PLUGIN_FILE)); - wp_die('WPTAG requires PHP 8.0 or higher.'); - } - - if (version_compare(get_bloginfo('version'), '6.8', '<')) { - deactivate_plugins(plugin_basename(WPTAG_PLUGIN_FILE)); - wp_die('WPTAG requires WordPress 6.8 or higher.'); - } - - self::create_tables(); - self::create_default_templates(); - flush_rewrite_rules(); - } - - public static function deactivate() { - wp_clear_scheduled_hook('wptag_cleanup_logs'); - flush_rewrite_rules(); - } - - private static function create_tables() { - global $wpdb; - $charset_collate = $wpdb->get_charset_collate(); - - $sql_snippets = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}wptag_snippets ( - id bigint(20) unsigned NOT NULL AUTO_INCREMENT, - name varchar(255) NOT NULL, - description text, - code longtext NOT NULL, - code_type varchar(50) DEFAULT 'html', - position varchar(100) NOT NULL, - category varchar(100) DEFAULT 'custom', - priority int(11) DEFAULT 10, - status tinyint(1) DEFAULT 1, - conditions longtext, - device_type varchar(50) DEFAULT 'all', - load_method varchar(50) DEFAULT 'normal', - created_by bigint(20) unsigned, - created_at datetime DEFAULT CURRENT_TIMESTAMP, - updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - last_modified_by bigint(20) unsigned, - PRIMARY KEY (id), - KEY idx_status_position (status, position), - KEY idx_category (category), - KEY idx_priority (priority) - ) $charset_collate;"; - - $sql_templates = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}wptag_templates ( - id bigint(20) unsigned NOT NULL AUTO_INCREMENT, - service_type varchar(100) NOT NULL, - service_name varchar(255) NOT NULL, - service_category varchar(100) NOT NULL, - config_fields longtext, - code_template longtext NOT NULL, - default_position varchar(100) NOT NULL, - is_active tinyint(1) DEFAULT 1, - version varchar(20) DEFAULT '1.0', - created_at datetime DEFAULT CURRENT_TIMESTAMP, - updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - PRIMARY KEY (id), - UNIQUE KEY idx_service_type (service_type), - KEY idx_category (service_category) - ) $charset_collate;"; - - $sql_logs = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}wptag_logs ( - id bigint(20) unsigned NOT NULL AUTO_INCREMENT, - user_id bigint(20) unsigned NOT NULL, - action varchar(100) NOT NULL, - object_type varchar(50) NOT NULL, - object_id bigint(20) unsigned, - old_value longtext, - new_value longtext, - ip_address varchar(45), - user_agent text, - created_at datetime DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (id), - KEY idx_user_action (user_id, action), - KEY idx_created_at (created_at) - ) $charset_collate;"; - - require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); - dbDelta($sql_snippets); - dbDelta($sql_templates); - dbDelta($sql_logs); - } - - private static function create_default_templates() { - global $wpdb; - $table = $wpdb->prefix . 'wptag_templates'; - - $templates = [ - [ - 'service_type' => 'google_analytics_4', - 'service_name' => 'Google Analytics 4', - 'service_category' => 'analytics', - 'config_fields' => json_encode([ - ['name' => 'measurement_id', 'label' => 'Measurement ID', 'type' => 'text', 'required' => true] - ]), - 'code_template' => ' -', - 'default_position' => 'head' - ], - [ - 'service_type' => 'facebook_pixel', - 'service_name' => 'Facebook Pixel', - 'service_category' => 'marketing', - 'config_fields' => json_encode([ - ['name' => 'pixel_id', 'label' => 'Pixel ID', 'type' => 'text', 'required' => true] - ]), - 'code_template' => ' -', - 'default_position' => 'head' - ], - [ - 'service_type' => 'google_ads', - 'service_name' => 'Google Ads Conversion', - 'service_category' => 'marketing', - 'config_fields' => json_encode([ - ['name' => 'conversion_id', 'label' => 'Conversion ID', 'type' => 'text', 'required' => true], - ['name' => 'conversion_label', 'label' => 'Conversion Label', 'type' => 'text', 'required' => true] - ]), - 'code_template' => ' -', - 'default_position' => 'head' - ], - [ - 'service_type' => 'google_search_console', - 'service_name' => 'Google Search Console', - 'service_category' => 'seo', - 'config_fields' => json_encode([ - ['name' => 'verification_code', 'label' => 'Verification Code', 'type' => 'text', 'required' => true] - ]), - 'code_template' => '', - 'default_position' => 'head' - ], - [ - 'service_type' => 'baidu_tongji', - 'service_name' => 'Baidu Tongji', - 'service_category' => 'analytics', - 'config_fields' => json_encode([ - ['name' => 'site_id', 'label' => 'Site ID', 'type' => 'text', 'required' => true] - ]), - 'code_template' => '', - 'default_position' => 'head' - ] - ]; - - foreach ($templates as $template) { - $exists = $wpdb->get_var($wpdb->prepare( - "SELECT COUNT(*) FROM $table WHERE service_type = %s", - $template['service_type'] - )); - - if (!$exists) { - $wpdb->insert($table, $template); - } - } - } -} diff --git a/includes/class-wptag-output-controller.php b/includes/class-wptag-output-controller.php deleted file mode 100644 index 4cdbbb9..0000000 --- a/includes/class-wptag-output-controller.php +++ /dev/null @@ -1,178 +0,0 @@ -snippet_manager = $snippet_manager; - $this->condition_engine = $condition_engine; - $this->cache_manager = $cache_manager; - } - - public function render_head() { - $this->render_snippets('head'); - } - - public function render_footer() { - $this->render_snippets('footer'); - } - - public function filter_content($content) { - if (!in_the_loop() || !is_main_query()) { - return $content; - } - - $before = $this->get_rendered_snippets('before_content'); - $after = $this->get_rendered_snippets('after_content'); - - return $before . $content . $after; - } - - private function render_snippets($position) { - echo $this->get_rendered_snippets($position); - } - - private function get_rendered_snippets($position) { - $cache_key = 'wptag_output_' . $position . '_' . $this->get_cache_context(); - $cached = $this->cache_manager->get($cache_key); - - if ($cached !== false && !$this->is_preview_mode()) { - return $cached; - } - - $snippets = $this->snippet_manager->get_active_snippets_by_position($position); - $output = ''; - - foreach ($snippets as $snippet) { - if ($this->should_render_snippet($snippet)) { - $output .= $this->render_single_snippet($snippet); - $this->rendered_snippets[] = $snippet['id']; - } - } - - if (!empty($output)) { - $output = "\n\n" . $output . "\n"; - } - - $this->cache_manager->set($cache_key, $output, 3600); - - return $output; - } - - private function should_render_snippet($snippet) { - if (in_array($snippet['id'], $this->rendered_snippets)) { - return false; - } - - if ($this->is_preview_mode() && !current_user_can('manage_options')) { - return false; - } - - if (!empty($snippet['device_type']) && $snippet['device_type'] !== 'all') { - $device_check = $this->condition_engine->evaluate_conditions([ - 'rules' => [[ - 'type' => 'device_type', - 'operator' => 'equals', - 'value' => $snippet['device_type'] - ]] - ]); - - if (!$device_check) { - return false; - } - } - - if (!empty($snippet['conditions'])) { - return $this->condition_engine->evaluate_conditions($snippet['conditions']); - } - - return true; - } - - private function render_single_snippet($snippet) { - $code = $snippet['code']; - - if ($snippet['load_method'] === 'async' && $snippet['code_type'] === 'javascript') { - $code = $this->wrap_async_script($code); - } elseif ($snippet['load_method'] === 'defer' && $snippet['code_type'] === 'javascript') { - $code = $this->wrap_defer_script($code); - } - - $code = apply_filters('wptag_snippet_output', $code, $snippet); - - if ($this->is_preview_mode() && current_user_can('manage_options')) { - $code = $this->wrap_preview_mode($code, $snippet); - } - - return $code . "\n"; - } - - private function wrap_async_script($code) { - if (strpos($code, ''; - } - - return str_replace(''; - } - - return str_replace('\n{$code}\n\n"; - } - - private function get_cache_context() { - $context = [ - 'type' => $this->get_page_type(), - 'id' => get_the_ID(), - 'user' => is_user_logged_in() ? 'logged_in' : 'logged_out' - ]; - - if (is_user_logged_in()) { - $user = wp_get_current_user(); - $context['roles'] = $user->roles; - } - - return md5(json_encode($context)); - } - - private function get_page_type() { - if (is_home() || is_front_page()) return 'home'; - if (is_single()) return 'single'; - if (is_page()) return 'page'; - if (is_category()) return 'category'; - if (is_tag()) return 'tag'; - if (is_archive()) return 'archive'; - if (is_search()) return 'search'; - if (is_404()) return '404'; - return 'other'; - } - - private function is_preview_mode() { - return isset($_GET['wptag_preview']) && $_GET['wptag_preview'] === '1'; - } - - public function clear_output_cache() { - $positions = ['head', 'footer', 'before_content', 'after_content']; - - foreach ($positions as $position) { - wp_cache_delete('wptag_output_' . $position, 'wptag'); - } - } -} diff --git a/includes/class-wptag-snippet-manager.php b/includes/class-wptag-snippet-manager.php deleted file mode 100644 index 13399a2..0000000 --- a/includes/class-wptag-snippet-manager.php +++ /dev/null @@ -1,264 +0,0 @@ -table_name = $wpdb->prefix . 'wptag_snippets'; - } - - public function get_snippet($id) { - global $wpdb; - $snippet = $wpdb->get_row($wpdb->prepare( - "SELECT * FROM {$this->table_name} WHERE id = %d", - $id - ), ARRAY_A); - - if ($snippet && !empty($snippet['conditions'])) { - $snippet['conditions'] = json_decode($snippet['conditions'], true); - } - - return $snippet; - } - - public function get_snippets($args = []) { - global $wpdb; - - $defaults = [ - 'status' => null, - 'position' => null, - 'category' => null, - 'search' => '', - 'orderby' => 'priority', - 'order' => 'ASC', - 'per_page' => 20, - 'page' => 1 - ]; - - $args = wp_parse_args($args, $defaults); - - $where = ['1=1']; - $where_values = []; - - if ($args['status'] !== null) { - $where[] = 'status = %d'; - $where_values[] = $args['status']; - } - - if (!empty($args['position'])) { - $where[] = 'position = %s'; - $where_values[] = $args['position']; - } - - if (!empty($args['category'])) { - $where[] = 'category = %s'; - $where_values[] = $args['category']; - } - - if (!empty($args['search'])) { - $where[] = '(name LIKE %s OR description LIKE %s)'; - $search_term = '%' . $wpdb->esc_like($args['search']) . '%'; - $where_values[] = $search_term; - $where_values[] = $search_term; - } - - $where_clause = implode(' AND ', $where); - $orderby = sanitize_sql_orderby($args['orderby'] . ' ' . $args['order']); - $offset = ($args['page'] - 1) * $args['per_page']; - - $query = "SELECT * FROM {$this->table_name} WHERE {$where_clause} ORDER BY {$orderby} LIMIT %d OFFSET %d"; - $where_values[] = $args['per_page']; - $where_values[] = $offset; - - $results = $wpdb->get_results($wpdb->prepare($query, $where_values), ARRAY_A); - - foreach ($results as &$result) { - if (!empty($result['conditions'])) { - $result['conditions'] = json_decode($result['conditions'], true); - } - } - - return $results; - } - - public function get_active_snippets_by_position($position) { - global $wpdb; - - $snippets = $wpdb->get_results($wpdb->prepare( - "SELECT * FROM {$this->table_name} - WHERE status = 1 AND position = %s - ORDER BY priority ASC, id ASC", - $position - ), ARRAY_A); - - foreach ($snippets as &$snippet) { - if (!empty($snippet['conditions'])) { - $snippet['conditions'] = json_decode($snippet['conditions'], true); - } - } - - return $snippets; - } - - public function create_snippet($data) { - global $wpdb; - - $snippet_data = $this->prepare_snippet_data($data); - $snippet_data['created_by'] = get_current_user_id(); - $snippet_data['created_at'] = current_time('mysql'); - - $result = $wpdb->insert($this->table_name, $snippet_data); - - if ($result === false) { - return new WP_Error('db_error', 'Failed to create snippet'); - } - - $snippet_id = $wpdb->insert_id; - $this->log_action('create', $snippet_id, null, $snippet_data); - - return $snippet_id; - } - - public function update_snippet($id, $data) { - global $wpdb; - - $old_snippet = $this->get_snippet($id); - if (!$old_snippet) { - return new WP_Error('not_found', 'Snippet not found'); - } - - $snippet_data = $this->prepare_snippet_data($data); - $snippet_data['last_modified_by'] = get_current_user_id(); - $snippet_data['updated_at'] = current_time('mysql'); - - $result = $wpdb->update( - $this->table_name, - $snippet_data, - ['id' => $id] - ); - - if ($result === false) { - return new WP_Error('db_error', 'Failed to update snippet'); - } - - $this->log_action('update', $id, $old_snippet, $snippet_data); - $this->clear_cache(); - - return true; - } - - public function delete_snippet($id) { - global $wpdb; - - $old_snippet = $this->get_snippet($id); - if (!$old_snippet) { - return new WP_Error('not_found', 'Snippet not found'); - } - - $result = $wpdb->delete($this->table_name, ['id' => $id]); - - if ($result === false) { - return new WP_Error('db_error', 'Failed to delete snippet'); - } - - $this->log_action('delete', $id, $old_snippet, null); - $this->clear_cache(); - - return true; - } - - public function toggle_status($id) { - global $wpdb; - - $snippet = $this->get_snippet($id); - if (!$snippet) { - return new WP_Error('not_found', 'Snippet not found'); - } - - $new_status = $snippet['status'] ? 0 : 1; - - $result = $wpdb->update( - $this->table_name, - ['status' => $new_status], - ['id' => $id] - ); - - if ($result === false) { - return new WP_Error('db_error', 'Failed to update status'); - } - - $this->clear_cache(); - - return $new_status; - } - - private function prepare_snippet_data($data) { - $prepared = [ - 'name' => sanitize_text_field($data['name'] ?? ''), - 'description' => sanitize_textarea_field($data['description'] ?? ''), - 'code' => $data['code'] ?? '', - 'code_type' => sanitize_key($data['code_type'] ?? 'html'), - 'position' => sanitize_key($data['position'] ?? 'head'), - 'category' => sanitize_key($data['category'] ?? 'custom'), - 'priority' => intval($data['priority'] ?? 10), - 'status' => isset($data['status']) ? intval($data['status']) : 1, - 'device_type' => sanitize_key($data['device_type'] ?? 'all'), - 'load_method' => sanitize_key($data['load_method'] ?? 'normal') - ]; - - if (!empty($data['conditions']) && is_array($data['conditions'])) { - $prepared['conditions'] = json_encode($data['conditions']); - } else { - $prepared['conditions'] = null; - } - - return $prepared; - } - - private function log_action($action, $object_id, $old_value = null, $new_value = null) { - global $wpdb; - - $log_data = [ - 'user_id' => get_current_user_id(), - 'action' => $action, - 'object_type' => 'snippet', - 'object_id' => $object_id, - 'old_value' => $old_value ? json_encode($old_value) : null, - 'new_value' => $new_value ? json_encode($new_value) : null, - 'ip_address' => $_SERVER['REMOTE_ADDR'] ?? '', - 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '', - 'created_at' => current_time('mysql') - ]; - - $wpdb->insert($wpdb->prefix . 'wptag_logs', $log_data); - } - - private function clear_cache() { - wp_cache_delete('wptag_active_snippets', 'wptag'); - wp_cache_delete('wptag_snippet_conditions', 'wptag'); - } - - public function get_categories() { - return [ - 'statistics' => __('Statistics', 'wptag'), - 'marketing' => __('Marketing', 'wptag'), - 'advertising' => __('Advertising', 'wptag'), - 'seo' => __('SEO', 'wptag'), - 'custom' => __('Custom', 'wptag') - ]; - } - - public function get_positions() { - return [ - 'head' => __('Site Header', 'wptag'), - 'footer' => __('Site Footer', 'wptag'), - 'before_content' => __('Before Post Content', 'wptag'), - 'after_content' => __('After Post Content', 'wptag') - ]; - } -} diff --git a/includes/class-wptag-template-manager.php b/includes/class-wptag-template-manager.php deleted file mode 100644 index 2936647..0000000 --- a/includes/class-wptag-template-manager.php +++ /dev/null @@ -1,271 +0,0 @@ -table_name = $wpdb->prefix . 'wptag_templates'; - } - - public function get_template($service_type) { - global $wpdb; - - $template = $wpdb->get_row($wpdb->prepare( - "SELECT * FROM {$this->table_name} WHERE service_type = %s AND is_active = 1", - $service_type - ), ARRAY_A); - - if ($template && !empty($template['config_fields'])) { - $template['config_fields'] = json_decode($template['config_fields'], true); - } - - return $template; - } - - public function get_templates($category = null) { - global $wpdb; - - $query = "SELECT * FROM {$this->table_name} WHERE is_active = 1"; - $params = []; - - if ($category) { - $query .= " AND service_category = %s"; - $params[] = $category; - } - - $query .= " ORDER BY service_category, service_name"; - - if (!empty($params)) { - $templates = $wpdb->get_results($wpdb->prepare($query, $params), ARRAY_A); - } else { - $templates = $wpdb->get_results($query, ARRAY_A); - } - - foreach ($templates as &$template) { - if (!empty($template['config_fields'])) { - $template['config_fields'] = json_decode($template['config_fields'], true); - } - } - - return $templates; - } - - public function get_categories() { - return [ - 'analytics' => __('Analytics & Statistics', 'wptag'), - 'marketing' => __('Marketing & Tracking', 'wptag'), - 'seo' => __('SEO Tools', 'wptag'), - 'support' => __('Customer Support', 'wptag'), - 'other' => __('Other Services', 'wptag') - ]; - } - - public function process_template_config($service_type, $config_data) { - $template = $this->get_template($service_type); - - if (!$template) { - return new WP_Error('template_not_found', 'Service template not found'); - } - - $validated_config = $this->validate_config($template['config_fields'], $config_data); - - if (is_wp_error($validated_config)) { - return $validated_config; - } - - $code = $this->render_template($template['code_template'], $validated_config); - - return [ - 'code' => $code, - 'position' => $template['default_position'], - 'name' => $template['service_name'] . ' - ' . ($validated_config['measurement_id'] ?? $validated_config['pixel_id'] ?? 'Config'), - 'category' => $template['service_category'] - ]; - } - - private function validate_config($fields, $data) { - $validated = []; - - foreach ($fields as $field) { - $field_name = $field['name']; - $field_value = $data[$field_name] ?? ''; - - if (!empty($field['required']) && empty($field_value)) { - return new WP_Error('missing_field', sprintf('Field %s is required', $field['label'])); - } - - if (!empty($field_value)) { - switch ($field['type']) { - case 'text': - $validated[$field_name] = sanitize_text_field($field_value); - break; - - case 'textarea': - $validated[$field_name] = sanitize_textarea_field($field_value); - break; - - case 'url': - $validated[$field_name] = esc_url_raw($field_value); - break; - - case 'number': - $validated[$field_name] = intval($field_value); - break; - - case 'select': - if (isset($field['options'][$field_value])) { - $validated[$field_name] = $field_value; - } - break; - - default: - $validated[$field_name] = sanitize_text_field($field_value); - } - } - } - - return $validated; - } - - private function render_template($template, $variables) { - $code = $template; - - foreach ($variables as $key => $value) { - $placeholder = '{{' . $key . '}}'; - $code = str_replace($placeholder, $value, $code); - } - - $code = preg_replace('/\{\{[^}]+\}\}/', '', $code); - - return trim($code); - } - - public function create_template($data) { - global $wpdb; - - $template_data = [ - 'service_type' => sanitize_key($data['service_type']), - 'service_name' => sanitize_text_field($data['service_name']), - 'service_category' => sanitize_key($data['service_category']), - 'config_fields' => json_encode($data['config_fields']), - 'code_template' => $data['code_template'], - 'default_position' => sanitize_key($data['default_position']), - 'is_active' => 1, - 'version' => sanitize_text_field($data['version'] ?? '1.0') - ]; - - $result = $wpdb->insert($this->table_name, $template_data); - - if ($result === false) { - return new WP_Error('db_error', 'Failed to create template'); - } - - return $wpdb->insert_id; - } - - public function update_template($service_type, $data) { - global $wpdb; - - $template_data = []; - - if (isset($data['service_name'])) { - $template_data['service_name'] = sanitize_text_field($data['service_name']); - } - - if (isset($data['service_category'])) { - $template_data['service_category'] = sanitize_key($data['service_category']); - } - - if (isset($data['config_fields'])) { - $template_data['config_fields'] = json_encode($data['config_fields']); - } - - if (isset($data['code_template'])) { - $template_data['code_template'] = $data['code_template']; - } - - if (isset($data['default_position'])) { - $template_data['default_position'] = sanitize_key($data['default_position']); - } - - if (isset($data['version'])) { - $template_data['version'] = sanitize_text_field($data['version']); - } - - $template_data['updated_at'] = current_time('mysql'); - - $result = $wpdb->update( - $this->table_name, - $template_data, - ['service_type' => $service_type] - ); - - if ($result === false) { - return new WP_Error('db_error', 'Failed to update template'); - } - - return true; - } - - public function delete_template($service_type) { - global $wpdb; - - $result = $wpdb->delete($this->table_name, ['service_type' => $service_type]); - - if ($result === false) { - return new WP_Error('db_error', 'Failed to delete template'); - } - - return true; - } - - public function export_templates($service_types = []) { - global $wpdb; - - if (empty($service_types)) { - $templates = $wpdb->get_results("SELECT * FROM {$this->table_name}", ARRAY_A); - } else { - $placeholders = array_fill(0, count($service_types), '%s'); - $query = $wpdb->prepare( - "SELECT * FROM {$this->table_name} WHERE service_type IN (" . implode(',', $placeholders) . ")", - $service_types - ); - $templates = $wpdb->get_results($query, ARRAY_A); - } - - return json_encode($templates, JSON_PRETTY_PRINT); - } - - public function import_templates($json_data) { - $templates = json_decode($json_data, true); - - if (!is_array($templates)) { - return new WP_Error('invalid_format', 'Invalid template format'); - } - - $imported = 0; - - foreach ($templates as $template) { - if (!isset($template['service_type']) || !isset($template['code_template'])) { - continue; - } - - $existing = $this->get_template($template['service_type']); - - if ($existing) { - $this->update_template($template['service_type'], $template); - } else { - $this->create_template($template); - } - - $imported++; - } - - return $imported; - } -} diff --git a/uninstall.php b/uninstall.php index ee4027f..0c4d6ac 100644 --- a/uninstall.php +++ b/uninstall.php @@ -1,41 +1,16 @@ prefix . 'wptag_snippets', - $wpdb->prefix . 'wptag_templates', - $wpdb->prefix . 'wptag_logs' -]; - -foreach ($tables as $table) { - $wpdb->query("DROP TABLE IF EXISTS {$table}"); -} - -$options = [ - 'wptag_db_version', - 'wptag_settings', - 'wptag_activated', - 'wptag_cache_cleared' -]; - -foreach ($options as $option) { - delete_option($option); -} - -delete_transient('wptag_admin_notice'); - -wp_clear_scheduled_hook('wptag_cleanup_logs'); -wp_clear_scheduled_hook('wptag_cache_cleanup'); - -wp_cache_flush(); +if ( is_multisite() ) { + $sites = get_sites(); + foreach ( $sites as $site ) { + switch_to_blog( $site->blog_id ); + delete_option( 'universal_tracking_codes_settings' ); + restore_current_blog(); + } +} \ No newline at end of file diff --git a/wptag-readme.md b/wptag-readme.md deleted file mode 100644 index 9c75226..0000000 --- a/wptag-readme.md +++ /dev/null @@ -1,155 +0,0 @@ -# WPTAG - WordPress Code Tag Manager - -Professional WordPress plugin for managing tracking codes, analytics scripts, and third-party integrations with advanced conditional loading. - -## Features - -### Core Functionality -- **Code Snippet Management**: Add, edit, and organize code snippets with categories -- **Multiple Insert Positions**: Head, footer, before/after content -- **Smart Conditional Loading**: Control when and where snippets appear -- **Service Templates**: Quick setup for popular services like Google Analytics, Facebook Pixel -- **Performance Optimization**: Built-in caching and code optimization -- **Import/Export**: Backup and migrate your snippets easily - -### Conditional Loading Options -- Page types (home, posts, pages, archives, etc.) -- User status (logged in/out, user roles) -- Device types (desktop, mobile, tablet) -- Specific posts/pages -- Categories and tags -- URL patterns -- Date/time ranges -- Custom conditions via filters - -### Supported Services Templates -- Google Analytics 4 -- Facebook Pixel -- Google Ads Conversion -- Google Search Console -- Baidu Analytics -- And more... - -## Installation - -1. Upload the `wptag` folder to `/wp-content/plugins/` -2. Activate the plugin through the 'Plugins' menu in WordPress -3. Navigate to WPTAG in your WordPress admin - -## Usage - -### Creating a Snippet -1. Go to WPTAG > Code Snippets -2. Click "Add New" -3. Enter snippet details: - - Name and description - - Code content - - Position (head/footer/content) - - Category and priority - - Conditions (optional) -4. Save and activate - -### Using Templates -1. Go to WPTAG > Service Templates -2. Select a service (e.g., Google Analytics) -3. Enter your configuration (e.g., Tracking ID) -4. Click "Create Snippet" - -### Setting Conditions -- Add multiple conditions to control snippet visibility -- Combine conditions with AND/OR logic -- Test conditions in preview mode - -## Requirements - -- WordPress 6.8+ -- PHP 8.0+ -- MySQL 5.7+ or MariaDB 10.3+ - -## Database Tables - -The plugin creates three tables: -- `wp_wptag_snippets` - Stores code snippets -- `wp_wptag_templates` - Service templates -- `wp_wptag_logs` - Activity logs - -## Hooks and Filters - -### Actions -- `wptag_before_render_snippet` - Before snippet output -- `wptag_after_render_snippet` - After snippet output - -### Filters -- `wptag_snippet_output` - Modify snippet output -- `wptag_custom_condition` - Add custom conditions -- `wptag_cache_ttl` - Modify cache duration - -## Performance - -- Intelligent caching reduces database queries -- Conditional pre-processing for faster page loads -- Code minification option -- Compatible with popular caching plugins - -## Security - -- Input validation and sanitization -- XSS protection -- SQL injection prevention -- User capability checks -- Nonce verification for all actions - -## Troubleshooting - -### Snippets not appearing -1. Check if snippet is active -2. Verify conditions are met -3. Clear cache (WPTAG Settings > Clear Cache) -4. Enable debug mode for detailed output - -### Performance issues -1. Reduce number of active snippets -2. Enable caching -3. Optimize conditions -4. Use priority settings wisely - -## Developer Documentation - -### Adding Custom Conditions - -```php -add_filter('wptag_custom_condition', function($result, $type, $value, $operator, $context) { - if ($type === 'my_custom_condition') { - // Your condition logic here - return $result; - } - return $result; -}, 10, 5); -``` - -### Modifying Snippet Output - -```php -add_filter('wptag_snippet_output', function($code, $snippet) { - // Modify code before output - return $code; -}, 10, 2); -``` - -## Support - -For support and documentation, visit [wptag.com](https://wptag.com) - -## License - -GPL v2 or later - -## Changelog - -### 1.0.0 -- Initial release -- Core snippet management -- Conditional loading engine -- Service templates -- Caching system -- Import/export functionality diff --git a/wptag.php b/wptag.php index 5e26197..86f69cf 100644 --- a/wptag.php +++ b/wptag.php @@ -1,17 +1,21 @@ define_constants(); + $this->load_dependencies(); + $this->init_hooks(); + } -add_action('plugins_loaded', function() { - load_plugin_textdomain('wptag', false, dirname(plugin_basename(__FILE__)) . '/languages'); - WPTag_Core::get_instance(); -}); + private function define_constants() { + if (!defined('WPTAG_ABSPATH')) { + define('WPTAG_ABSPATH', dirname(WPTAG_PLUGIN_FILE) . '/'); + } + } + + private function load_dependencies() { + require_once WPTAG_PLUGIN_DIR . 'includes/class-config.php'; + require_once WPTAG_PLUGIN_DIR . 'includes/class-validator.php'; + require_once WPTAG_PLUGIN_DIR . 'includes/class-output-manager.php'; + + $this->config = new Config(); + + if (is_admin()) { + require_once WPTAG_PLUGIN_DIR . 'includes/class-admin.php'; + $this->admin = new Admin($this->config); + } + + if (!is_admin() || wp_doing_ajax()) { + require_once WPTAG_PLUGIN_DIR . 'includes/class-frontend.php'; + $this->frontend = new Frontend($this->config); + } + } + + private function init_hooks() { + register_activation_hook(WPTAG_PLUGIN_FILE, array($this, 'activate')); + register_deactivation_hook(WPTAG_PLUGIN_FILE, array($this, 'deactivate')); + + add_action('init', array($this, 'init'), 0); + add_action('plugins_loaded', array($this, 'plugins_loaded')); + } + + public function activate() { + if (!get_option('wptag_version')) { + $this->config->install_default_settings(); + add_option('wptag_version', WPTAG_VERSION); + } + + flush_rewrite_rules(); + } + + public function deactivate() { + flush_rewrite_rules(); + } + + public function init() { + do_action('wptag_init'); + } + + public function plugins_loaded() { + do_action('wptag_loaded'); + } + + public function get_config() { + return $this->config; + } + + public function get_version() { + return WPTAG_VERSION; + } +} + +function wptag() { + return WPTag::instance(); +} + +wptag(); \ No newline at end of file diff --git a/wptag.pot b/wptag.pot deleted file mode 100644 index 692bd2f..0000000 --- a/wptag.pot +++ /dev/null @@ -1,298 +0,0 @@ -# WPTAG WordPress Plugin -# Copyright (C) 2024 WPTAG Team -# This file is distributed under the GPL v2 or later. -msgid "" -msgstr "" -"Project-Id-Version: WPTAG 1.0.0\n" -"Report-Msgid-Bugs-To: https://wptag.com/support\n" -"POT-Creation-Date: 2024-01-01 00:00:00+00:00\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" - -#: admin/partials/dashboard.php:15 -msgid "WPTAG Dashboard" -msgstr "" - -#: admin/partials/dashboard.php:22 -msgid "Total Snippets" -msgstr "" - -#: admin/partials/dashboard.php:27 -msgid "Active Snippets" -msgstr "" - -#: admin/partials/dashboard.php:32 -msgid "Inactive Snippets" -msgstr "" - -#: admin/partials/dashboard.php:37 -msgid "Success Rate" -msgstr "" - -#: admin/partials/snippets.php:15 -msgid "Code Snippets" -msgstr "" - -#: admin/partials/snippets.php:17 -msgid "Add New" -msgstr "" - -#: admin/partials/snippets.php:25 -msgid "Search" -msgstr "" - -#: admin/partials/snippets.php:26 -msgid "Search snippets..." -msgstr "" - -#: admin/partials/snippets.php:31 -msgid "Category" -msgstr "" - -#: admin/partials/snippets.php:33 -msgid "All Categories" -msgstr "" - -#: admin/partials/snippets.php:43 -msgid "Position" -msgstr "" - -#: admin/partials/snippets.php:45 -msgid "All Positions" -msgstr "" - -#: admin/partials/snippets.php:55 -msgid "Status" -msgstr "" - -#: admin/partials/snippets.php:57 -msgid "All Status" -msgstr "" - -#: admin/partials/snippets.php:58 -msgid "Active" -msgstr "" - -#: admin/partials/snippets.php:59 -msgid "Inactive" -msgstr "" - -#: admin/partials/snippets.php:71 -msgid "No snippets found" -msgstr "" - -#: admin/partials/snippets.php:72 -msgid "Get started by creating your first code snippet." -msgstr "" - -#: admin/partials/snippets.php:74 -msgid "Create Snippet" -msgstr "" - -#: admin/partials/snippets.php:81 -msgid "Name" -msgstr "" - -#: admin/partials/snippets.php:82 -msgid "Priority" -msgstr "" - -#: admin/partials/snippets.php:83 -msgid "Actions" -msgstr "" - -#: admin/partials/snippets.php:115 -msgid "Edit" -msgstr "" - -#: admin/partials/snippets.php:119 -msgid "Enable" -msgstr "" - -#: admin/partials/snippets.php:119 -msgid "Disable" -msgstr "" - -#: admin/partials/snippets.php:122 -msgid "Delete" -msgstr "" - -#: admin/partials/templates.php:11 -msgid "Service Templates" -msgstr "" - -#: admin/partials/templates.php:17 -msgid "Service templates help you quickly add popular services to your site. Simply select a template, enter your configuration details, and a snippet will be created automatically." -msgstr "" - -#: admin/partials/templates.php:22 -msgid "All Templates" -msgstr "" - -#: admin/partials/templates.php:48 -msgid "No templates found" -msgstr "" - -#: admin/partials/templates.php:49 -msgid "No templates available in this category." -msgstr "" - -#: admin/partials/templates.php:68 -msgid "Use This Template" -msgstr "" - -#: admin/partials/settings.php:11 -msgid "WPTAG Settings" -msgstr "" - -#: admin/partials/settings.php:20 -msgid "Cache Settings" -msgstr "" - -#: admin/partials/settings.php:23 -msgid "Enable Cache" -msgstr "" - -#: admin/partials/settings.php:27 -msgid "Enable caching for better performance" -msgstr "" - -#: admin/partials/settings.php:30 -msgid "Caching stores processed snippets and conditions to improve page load times." -msgstr "" - -#: admin/partials/settings.php:37 -msgid "Cache Duration" -msgstr "" - -#: admin/partials/settings.php:42 -msgid "seconds" -msgstr "" - -#: admin/partials/settings.php:44 -msgid "How long to keep cached data. Default is 3600 seconds (1 hour)." -msgstr "" - -#: admin/partials/settings.php:50 -msgid "Cache Status" -msgstr "" - -#: admin/partials/settings.php:54 -msgid "Cache Enabled:" -msgstr "" - -#: admin/partials/settings.php:55 -msgid "Yes" -msgstr "" - -#: admin/partials/settings.php:55 -msgid "No" -msgstr "" - -#: admin/partials/settings.php:64 -msgid "Clear Cache Now" -msgstr "" - -#: admin/partials/settings.php:71 -msgid "Debug Settings" -msgstr "" - -#: admin/partials/settings.php:74 -msgid "Debug Mode" -msgstr "" - -#: admin/partials/settings.php:78 -msgid "Enable debug mode" -msgstr "" - -#: admin/partials/settings.php:81 -msgid "Show additional information in HTML comments for troubleshooting." -msgstr "" - -#: admin/partials/settings.php:87 -msgid "Uninstall Settings" -msgstr "" - -#: admin/partials/settings.php:90 -msgid "Data Cleanup" -msgstr "" - -#: admin/partials/settings.php:94 -msgid "Remove all data when uninstalling the plugin" -msgstr "" - -#: admin/partials/settings.php:97 -msgid "Warning: This will permanently delete all snippets, templates, and settings when the plugin is uninstalled." -msgstr "" - -#: admin/partials/settings.php:103 -msgid "Import/Export" -msgstr "" - -#: admin/partials/settings.php:106 -msgid "Export Snippets" -msgstr "" - -#: admin/partials/settings.php:108 -msgid "Export all your snippets to a JSON file for backup or migration." -msgstr "" - -#: admin/partials/settings.php:110 -msgid "Export All Snippets" -msgstr "" - -#: admin/partials/settings.php:116 -msgid "Import Snippets" -msgstr "" - -#: admin/partials/settings.php:118 -msgid "Import snippets from a JSON file." -msgstr "" - -#: admin/partials/settings.php:121 -msgid "Import Snippets" -msgstr "" - -#: admin/partials/settings.php:129 -msgid "Save Settings" -msgstr "" - -#: includes/class-wptag-snippet-manager.php:355 -msgid "Statistics" -msgstr "" - -#: includes/class-wptag-snippet-manager.php:356 -msgid "Marketing" -msgstr "" - -#: includes/class-wptag-snippet-manager.php:357 -msgid "Advertising" -msgstr "" - -#: includes/class-wptag-snippet-manager.php:358 -msgid "SEO" -msgstr "" - -#: includes/class-wptag-snippet-manager.php:359 -msgid "Custom" -msgstr "" - -#: includes/class-wptag-snippet-manager.php:365 -msgid "Site Header" -msgstr "" - -#: includes/class-wptag-snippet-manager.php:366 -msgid "Site Footer" -msgstr "" - -#: includes/class-wptag-snippet-manager.php:367 -msgid "Before Post Content" -msgstr "" - -#: includes/class-wptag-snippet-manager.php:368 -msgid "After Post Content" -msgstr ""