commit 5c837b9d31789f1ef504682db5d8b6966e3824a2 Author: Jean-Christian Denis Date: Sat Apr 15 17:38:25 2023 +0200 first public release 0.1 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0080322 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +0.1 - 2023.04.15 +- require Dotclear 2.26 +- require PHP >= 8.1 \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d511905 --- /dev/null +++ b/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) 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 +this service 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 make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. 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. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +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 +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the 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 a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE 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. + + 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 +convey 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 2 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, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision 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, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This 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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7a7f12d --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# README + +[![Release](https://img.shields.io/github/v/release/JcDenis/Uninstaller)](https://github.com/JcDenis/Uninstaller/releases) +[![Date](https://img.shields.io/github/release-date/JcDenis/Uninstaller)](https://github.com/JcDenis/Uninstaller/releases) +[![Issues](https://img.shields.io/github/issues/JcDenis/Uninstaller)](https://github.com/JcDenis/Uninstaller/issues) +[![Dotclear](https://img.shields.io/badge/dotclear-v2.26-blue.svg)](https://fr.dotclear.org/download) +[![Dotaddict](https://img.shields.io/badge/dotaddict-official-green.svg)](https://plugins.dotaddict.org/dc2/details/Uninstaller) +[![License](https://img.shields.io/github/license/JcDenis/Uninstaller)](https://github.com/JcDenis/Uninstaller/blob/master/LICENSE) + +## WHAT IS UNINSTALLER ? + +_Uninstaller_ is a plugin for the open-source +web publishing software called Dotclear. + +It adds habitlity to uninstall modules using their feature... + +## REQUIREMENTS + +_Uninstaller_ requires: + + * super admin permissions to uninstall modules + * Dotclear 2.26 + * PHP >= 8.1 + +## USAGE + +First install _Uninstaller_, manualy from a zip package or from +Dotaddict repository. (See Dotclear's documentation to know how do this) + +Once it's done you can uninstall modules form plugins list or theme list. + +## LINKS + + * License : [GNU GPL v2](https://www.gnu.org/licenses/old-licenses/lgpl-2.0.html) + * Source & contribution : [GitHub Page](https://github.com/JcDenis/Uninstall) + * Packages & details: [Dotaddict Page](https://plugins.dotaddict.org/dc2/details/Uninstall) + +## CONTRIBUTORS + + * Jean-Christian Denis + + You are welcome to contribute to this code. \ No newline at end of file diff --git a/_define.php b/_define.php new file mode 100644 index 0000000..aff9389 --- /dev/null +++ b/_define.php @@ -0,0 +1,30 @@ +registerModule( + 'Uninstaller', + 'Uninstall cleanly plugins and themes', + 'Jean-Christian Denis and Contributors', + '0.1', + [ + 'requires' => [['core', '2.26']], + 'permissions' => null, + 'type' => 'plugin', + 'support' => 'https://github.com/JcDenis/' . basename(__DIR__), + 'details' => 'https://plugins.dotaddict.org/dc2/details/' . basename(__DIR__), + 'repository' => 'https://raw.githubusercontent.com/JcDenis/' . basename(__DIR__) . '/master/dcstore.xml', + ] +); diff --git a/dcstore.xml b/dcstore.xml new file mode 100644 index 0000000..5f73772 --- /dev/null +++ b/dcstore.xml @@ -0,0 +1,13 @@ + + + + Uninstaller + 0.1 + Jean-Christian Denis and Contributors + Uninstall cleanly plugins and themes + https://github.com/JcDenis/Uninstaller/releases/download/v0.1/plugin-Uninstaller.zip + 2.26 + https://plugins.dotaddict.org/dc2/details/Uninstaller + https://github.com/JcDenis/Uninstaller + + diff --git a/js/backend.js b/js/backend.js new file mode 100644 index 0000000..30db1ce --- /dev/null +++ b/js/backend.js @@ -0,0 +1,10 @@ +/*global $, dotclear */ +'use strict'; + +Object.assign(dotclear.msg, dotclear.getData('uninstaller')); + +$(() => { + $('#uninstall-form').on('submit', function () { + return window.confirm(dotclear.msg.confirm_uninstall); + }); +}); \ No newline at end of file diff --git a/locales/fr/main.lang.php b/locales/fr/main.lang.php new file mode 100644 index 0000000..03b4483 --- /dev/null +++ b/locales/fr/main.lang.php @@ -0,0 +1,69 @@ + 1);\n" + +#: src/Backend.php:45 +msgid "Uninstall" +msgstr "Désinstaller" + +#: src/Backend.php:71 +msgid "Plugin has been successfully uninstalled." +msgstr "Plugin désinstallé avec succès" + +#: src/Cleaner/Caches.php:32 +msgid "Folders from cache directory" +msgstr "Dossiers du répertoire de cache" + +#: src/Cleaner/Caches.php:41 +msgid "delete \"%s\" cache directory" +msgstr "supprimer le répertoire de cache \"%s\"" + +#: src/Cleaner/Caches.php:42 +msgid "\"%s\" cache directory deleted" +msgstr "répertoire de cache \"%s\" supprimé" + +#: src/Cleaner/Caches.php:43 +msgid "Failed to delete \"%s\" cache directory" +msgstr "Impossible de supprimer le répertoire de cache \"%s\"" + +#: src/Cleaner/Caches.php:47 +msgid "empty \"%s\" cache directory" +msgstr "vider le répertoire de cache \"%s\"" + +#: src/Cleaner/Caches.php:48 +msgid "\"%s\" cache directory emptied" +msgstr "répertoire de cache \"%s\" vidé" + +#: src/Cleaner/Caches.php:49 +msgid "Failed to empty \"%s\" cache directory" +msgstr "Impossible de vider le répertoire de cache \"ùs\"" + +#: src/Cleaner/Plugins.php:32 +msgid "Folders from plugins directories" +msgstr "Dossiers des répertoires des plugins" + +#: src/Cleaner/Plugins.php:41 +msgid "delete \"%s\" plugin directory" +msgstr "supprimer le répertoire \"%s\" du plugin" + +#: src/Cleaner/Plugins.php:42 +msgid "\"%s\" plugin directory deleted" +msgstr "répertoire \"%s\" du plugin supprimé" + +#: src/Cleaner/Plugins.php:43 +msgid "Failed to delete \"%s\" plugin directory" +msgstr "Impossible de supprimer le répertoire \"%s\" du plugin" + +#: src/Cleaner/Settings.php:31 +msgid "Namespaces registered in dcSettings" +msgstr "Espaces de nom (namespaces) enregistrés dans dcSettings" + +#: src/Cleaner/Settings.php:40 +msgid "delete \"%s\" global settings" +msgstr "supprimer les paramètres \"%s\" globaux" + +#: src/Cleaner/Settings.php:41 +msgid "\"%s\" global settings deleted" +msgstr "paramètres \"%s\" globaux supprimés" + +#: src/Cleaner/Settings.php:42 +msgid "Failed to delete \"%s\" global settings" +msgstr "Impossible de supprimer les paramètres \"%s\" globaux" + +#: src/Cleaner/Settings.php:46 +msgid "delete \"%s\" blog settings" +msgstr "supprimer les paramètres \"%s\" du blog" + +#: src/Cleaner/Settings.php:47 +msgid "\"%s\" blog settings deleted" +msgstr "paramètres \"%s\" du blog supprimés" + +#: src/Cleaner/Settings.php:48 +msgid "Failed to delete \"%s\" blog settings" +msgstr "Impossible de supprimer les paramètres \"%s\" du blog" + +#: src/Cleaner/Settings.php:52 +msgid "delete \"%s\" settings" +msgstr "supprimer les paramètres \"%s\"" + +#: src/Cleaner/Settings.php:53 +msgid "\"%s\" settings deleted" +msgstr "paramètres \"%s\" supprimés" + +#: src/Cleaner/Settings.php:54 +msgid "Failed to delete \"%s\" settings" +msgstr "Impossible de supprimer les paramètres \"%s\"" + +#: src/Cleaner/Tables.php:31 +msgid "All database tables of Dotclear" +msgstr "Toutes les tables Dotclear de la base de données" + +#: src/Cleaner/Tables.php:40 +msgid "delete \"%s\" table" +msgstr "Supprimer la table \"%s\"" + +#: src/Cleaner/Tables.php:41 +msgid "\"%s\" table deleted" +msgstr "table \"%s\" supprimée" + +#: src/Cleaner/Tables.php:42 +msgid "Failed to delete \"%s\" table" +msgstr "Impossible de supprimer la table \"%s\"" + +#: src/Cleaner/Tables.php:46 +msgid "empty \"%s\" table" +msgstr "vider la table \"%s\"" + +#: src/Cleaner/Tables.php:47 +msgid "\"%s\" table emptied" +msgstr "table \"%s\" vidée" + +#: src/Cleaner/Tables.php:48 +msgid "Failed to empty \"%s\" table" +msgstr "Impossible de vider la table \"%s\"" + +#: src/Cleaner/Themes.php:33 +msgid "Folders from blog themes directory" +msgstr "Dossiers du répertoire des thèmes du blog" + +#: src/Cleaner/Themes.php:42 +msgid "delete \"%s\" theme directory" +msgstr "supprimer le répertoire \"%s\" du thème" + +#: src/Cleaner/Themes.php:43 +msgid "\"%s\" theme directory deleted" +msgstr "répertoire \"%s\" du thème supprimer" + +#: src/Cleaner/Themes.php:44 +msgid "Failed to delete \"%s\" theme directory" +msgstr "Impossible de supprimer le répertoire \"%s\" du thème" + +#: src/Cleaner/Vars.php:32 +msgid "Folders from Dotclear VAR directory" +msgstr "Dossiers du répertoire VAR de Dotclear" + +#: src/Cleaner/Vars.php:41 +msgid "delete \"%s\" var directory" +msgstr "Supprimer le répertoire var \"%s\"" + +#: src/Cleaner/Vars.php:42 +msgid "\"%s\" var directory deleted" +msgstr "répertoire var \"%s\" supprimé" + +#: src/Cleaner/Vars.php:43 +msgid "Failed to delete \"%s\" var directory" +msgstr "Impossible de supprimer le répertoire var \"%s\"" + +#: src/Cleaner/Versions.php:30 +msgid "Versions registered in table \"version\" of Dotclear" +msgstr "Versions enregistrées dans la table \"version\" de Dotclear" + +#: src/Cleaner/Versions.php:39 +msgid "delete \"%s\" version number" +msgstr "supprimer le numéro de version de \"%s\"" + +#: src/Cleaner/Versions.php:40 +msgid "\"%s\" version number deleted" +msgstr "numéro de version de \"%s\" supprimé" + +#: src/Cleaner/Versions.php:41 +msgid "Failed to delete \"%s\" version number" +msgstr "Impossible de supprimer le numéro de version de \"%s\"" + +#: src/Cleaners.php:84 +msgid "Unknown cleaner \"%s\"" +msgstr "Nettoyeur \"%s\" inconnu" + +#: src/Cleaners.php:87 +msgid "Unsintaller can't remove itself" +msgstr "Le désinstalleur ne peut pas se supprimer lui-même" + +#: src/Cleaners.php:98 +msgid "Unknown error" +msgstr "Erreur inconnue" + +#: src/Manage.php:64 +msgid "Unknown module id to uninstall" +msgstr "Module a désinstaller inconnu" + +#: src/Manage.php:71 +msgid "There are no uninstall actions for this module" +msgstr "Il n'y a aucune action pour ce module" + +#: src/Manage.php:93 +msgid "Uninstall action successfuly excecuted" +msgstr "Actions de désinstallation exécutées avec succès" + +#: src/Manage.php:96 +msgid "No uninstall action done" +msgstr "Aucune action de désinstallation exécutée" + +#: src/Manage.php:125 +msgid "Are you sure you perform these ations?" +msgstr "Êtes-vous sûre de vouloir effectuer ces actions ?" + +#: src/Manage.php:154 +msgid "Perform selected actions" +msgstr "Effectuer les actions sélectionnées" + +#: src/Manage.php:160 +msgid "Uninstall theme \"%s\"" +msgstr "Désinstaller le thème \"%s\"" + +#: src/Manage.php:160 +msgid "Uninstall plugin \"%s\"" +msgstr "Désinstaller le plugin \"%s\"" + +#: src/Manage.php:161 +msgid "The module \"%s %s\" offers advanced unsintall process:" +msgstr "Le module \"%s %s\" propose un processus de désinstallation avancée :" + +msgid "Uninstaller" +msgstr "Désinstalleur" + +msgid "Uninstall cleanly plugins and themes" +msgstr "Désinstalle proprement les plugins et thèmes" + diff --git a/src/AbstractCleaner.php b/src/AbstractCleaner.php new file mode 100644 index 0000000..98ac6e1 --- /dev/null +++ b/src/AbstractCleaner.php @@ -0,0 +1,106 @@ + $actions The cleaner actions decriptions */ + public readonly array $actions; + + /** + * Constructor set up a Cleaner. + */ + final public function __construct() + { + $properties = $this->properties(); + $this->id = $properties['id'] ?? 'undefined'; + $this->name = $properties['name'] ?? 'undefined'; + $this->desc = $properties['desc'] ?? 'undefined'; + + $actions = []; + foreach ($this->actions() as $descriptor) { + if (is_a($descriptor, ActionDescriptor::class) && $descriptor->id != 'undefined') { + $actions[$descriptor->id] = $descriptor; + } + } + $this->actions = $actions; + } + + /** + * Create and add the Cleaner to a stack. + * + * @param ArrayObject $stack The cleaners stack + */ + public static function create(ArrayObject $stack): void + { + $class = static::class; + $stack->append(new $class()); + } + + /** + * Initialize Cleaner properties. + * + * @return array The Cleaner properties [id=>,name=>,desc=>,] + */ + abstract protected function properties(): array; + + /** + * Initialize Cleaner actions. + * + * @return array The Cleaner actions definitions + */ + abstract protected function actions(): array; + + /** + * Get list of distirbuted values for the cleaner. + * + * @return array The values [value,] + */ + abstract public function distributed(): array; + + /** + * Get all values from the cleaner. + * + * @return array> The values. + */ + abstract public function values(): array; + + /** + * Execute action on an value. + * + * @param string $action The action id + * @param string $ns The value. + * + * @return bool The success + */ + abstract public function execute(string $action, string $ns): bool; +} diff --git a/src/ActionDescriptor.php b/src/ActionDescriptor.php new file mode 100644 index 0000000..4dedee0 --- /dev/null +++ b/src/ActionDescriptor.php @@ -0,0 +1,59 @@ +id = (string) ($description['id'] ?? 'undefined'); + $this->query = (string) ($description['query'] ?? 'undefined'); + $this->success = (string) ($description['success'] ?? 'undefined'); + $this->error = (string) ($description['error'] ?? 'undefined'); + } + + /** + * Get descriptor properties. + * + * @return array The properties + */ + public function dump(): array + { + return [ + 'id' => $this->id, + 'query' => $this->query, + 'success' => $this->success, + 'error' => $this->error, + ]; + } +} diff --git a/src/Backend.php b/src/Backend.php new file mode 100644 index 0000000..d284b04 --- /dev/null +++ b/src/Backend.php @@ -0,0 +1,87 @@ +addBehaviors([ + // add "unsinstall" button to modules list + 'adminModulesListGetActionsV2' => function (adminModulesList $list, dcModuleDefine $define): string { + return empty(Uninstaller::instance()->loadModules([$define])->getUserActions($define->getId())) ? '' : + sprintf( + ' ' . __('Uninstall') . '', + dcCore::app()->adminurl->get('admin.plugin.' . My::id(), ['type' => $define->get('type'), 'id' => $define->getId()]) + ); + }, + // perform direct action on module deletion + 'pluginBeforeDeleteV2' => function (dcModuleDefine $define): void { + if (dcCore::app()->blog->settings->get('system')->get('no_uninstall_direct')) { + return; + } + + try { + $uninstaller = Uninstaller::instance()->loadModules([$define]); + + $done = []; + foreach ($uninstaller->getDirectActions($define->getId()) as $cleaner => $stack) { + foreach ($stack as $action) { + if ($uninstaller->execute($cleaner, $action['action'], $action['ns'])) { + $done[] = sprintf($action['success'], $action['ns']); + } else { + dcCore::app()->error->add(sprintf($action['error'], $action['ns'])); + } + } + } + + // if direct actions are made, do not execute dotclear delete action. + if (!empty($done)) { + array_unshift($done, __('Plugin has been successfully uninstalled.')); + dcPage::addSuccessNotice(implode('
', $done)); + if ($define->get('type') == 'theme') { + dcCore::app()->adminurl->redirect('blog.themes', [], '#themes'); + } else { + dcCore::app()->adminurl->redirect('admin.plugins', [], '#plugins'); + } + } + } catch (Exception $e) { + dcCore::app()->error->add($e->getMessage()); + } + }, + ]); + + return true; + } +} diff --git a/src/Cleaner/Caches.php b/src/Cleaner/Caches.php new file mode 100644 index 0000000..078d518 --- /dev/null +++ b/src/Cleaner/Caches.php @@ -0,0 +1,79 @@ + 'caches', + 'name' => __('Cache'), + 'desc' => __('Folders from cache directory'), + ]; + } + + protected function actions(): array + { + return [ + new ActionDescriptor([ + 'id' => 'delete', + 'query' => __('delete "%s" cache directory'), + 'success' => __('"%s" cache directory deleted'), + 'error' => __('Failed to delete "%s" cache directory'), + ]), + new ActionDescriptor([ + 'id' => 'empty', + 'query' => __('empty "%s" cache directory'), + 'success' => __('"%s" cache directory emptied'), + 'error' => __('Failed to empty "%s" cache directory'), + ]), + ]; + } + + public function distributed(): array + { + return ['cbfeed', 'cbtpl', 'dcrepo', 'versions']; + } + + public function values(): array + { + return self::getDirs(DC_TPL_CACHE); + } + + public function execute(string $action, string $ns): bool + { + if ($action == 'empty') { + self::delDir(DC_TPL_CACHE, $ns, false); + + return true; + } + if ($action == 'delete') { + self::delDir(DC_TPL_CACHE, $ns, true); + + return true; + } + + return false; + } +} diff --git a/src/Cleaner/Plugins.php b/src/Cleaner/Plugins.php new file mode 100644 index 0000000..b52d7f3 --- /dev/null +++ b/src/Cleaner/Plugins.php @@ -0,0 +1,72 @@ + 'plugins', + 'name' => __('Plugins'), + 'desc' => __('Folders from plugins directories'), + ]; + } + + protected function actions(): array + { + return [ + new ActionDescriptor([ + 'id' => 'delete', + 'query' => __('delete "%s" plugin directory'), + 'success' => __('"%s" plugin directory deleted'), + 'error' => __('Failed to delete "%s" plugin directory'), + ]), + ]; + } + + public function distributed(): array + { + return explode(',', DC_DISTRIB_PLUGINS); + } + + public function values(): array + { + $res = self::getDirs(explode(PATH_SEPARATOR, DC_PLUGINS_ROOT)); + sort($res); + + return $res; + } + + public function execute(string $action, string $ns): bool + { + if ($action == 'delete') { + $res = explode(PATH_SEPARATOR, DC_PLUGINS_ROOT); + self::delDir($res, $ns, true); + + return true; + } + + return false; + } +} diff --git a/src/Cleaner/Settings.php b/src/Cleaner/Settings.php new file mode 100644 index 0000000..7b026b0 --- /dev/null +++ b/src/Cleaner/Settings.php @@ -0,0 +1,134 @@ + 'settings', + 'name' => __('Settings'), + 'desc' => __('Namespaces registered in dcSettings'), + ]; + } + + protected function actions(): array + { + return [ + new ActionDescriptor([ + 'id' => 'delete_global', + 'query' => __('delete "%s" global settings'), + 'success' => __('"%s" global settings deleted'), + 'error' => __('Failed to delete "%s" global settings'), + ]), + new ActionDescriptor([ + 'id' => 'delete_local', + 'query' => __('delete "%s" blog settings'), + 'success' => __('"%s" blog settings deleted'), + 'error' => __('Failed to delete "%s" blog settings'), + ]), + new ActionDescriptor([ + 'id' => 'delete_all', + 'query' => __('delete "%s" settings'), + 'success' => __('"%s" settings deleted'), + 'error' => __('Failed to delete "%s" settings'), + ]), + ]; + } + + public function distributed(): array + { + return [ + 'akismet', + 'antispam', + 'breadcrumb', + 'dcckeditor', + 'dclegacyeditor', + 'maintenance', + 'pages', + 'pings', + 'system', + 'themes', + 'widgets', + ]; + } + + public function values(): array + { + $res = dcCore::app()->con->select( + 'SELECT setting_ns ' . + 'FROM ' . dcCore::app()->prefix . dcNamespace::NS_TABLE_NAME . ' ' . + 'WHERE blog_id IS NULL ' . + 'OR blog_id IS NOT NULL ' . + 'GROUP BY setting_ns' + ); + + $rs = []; + $i = 0; + while ($res->fetch()) { + $rs[$i]['key'] = $res->f('setting_ns'); + $rs[$i]['value'] = dcCore::app()->con->select( + 'SELECT count(*) FROM ' . dcCore::app()->prefix . dcNamespace::NS_TABLE_NAME . ' ' . + "WHERE setting_ns = '" . $res->f('setting_ns') . "' " . + 'AND (blog_id IS NULL OR blog_id IS NOT NULL) ' . + 'GROUP BY setting_ns ' + )->f(0); + $i++; + } + + return $rs; + } + + public function execute(string $action, string $ns): bool + { + if ($action == 'delete_global') { + dcCore::app()->con->execute( + 'DELETE FROM ' . dcCore::app()->prefix . dcNamespace::NS_TABLE_NAME . ' ' . + 'WHERE blog_id IS NULL ' . + "AND setting_ns = '" . dcCore::app()->con->escapeStr((string) $ns) . "' " + ); + + return true; + } + if ($action == 'delete_local') { + dcCore::app()->con->execute( + 'DELETE FROM ' . dcCore::app()->prefix . dcNamespace::NS_TABLE_NAME . ' ' . + "WHERE blog_id = '" . dcCore::app()->con->escapeStr((string) dcCore::app()->blog->id) . "' " . + "AND setting_ns = '" . dcCore::app()->con->escapeStr((string) $ns) . "' " + ); + + return true; + } + if ($action == 'delete_all') { + dcCore::app()->con->execute( + 'DELETE FROM ' . dcCore::app()->prefix . dcNamespace::NS_TABLE_NAME . ' ' . + "WHERE setting_ns = '" . dcCore::app()->con->escapeStr((string) $ns) . "' " . + "AND (blog_id IS NULL OR blog_id != '') " + ); + + return true; + } + + return false; + } +} diff --git a/src/Cleaner/Tables.php b/src/Cleaner/Tables.php new file mode 100644 index 0000000..8747802 --- /dev/null +++ b/src/Cleaner/Tables.php @@ -0,0 +1,118 @@ + 'tables', + 'name' => __('Tables'), + 'desc' => __('All database tables of Dotclear'), + ]; + } + + protected function actions(): array + { + return [ + new ActionDescriptor([ + 'id' => 'delete', + 'query' => __('delete "%s" table'), + 'success' => __('"%s" table deleted'), + 'error' => __('Failed to delete "%s" table'), + ]), + new ActionDescriptor([ + 'id' => 'empty', + 'query' => __('empty "%s" table'), + 'success' => __('"%s" table emptied'), + 'error' => __('Failed to empty "%s" table'), + ]), + ]; + } + + public function distributed(): array + { + return [ + 'blog', + 'category', + 'comment', + 'link', + 'log', + 'media', + 'meta', + 'permissions', + 'ping', + 'post', + 'post_media', + 'pref', + 'session', + 'setting', + 'spamrule', + 'user', + 'version', + ]; + } + + public function values(): array + { + $object = dbSchema::init(dcCore::app()->con); + $res = $object->getTables(); + + $rs = []; + $i = 0; + foreach ($res as $k => $v) { + if ('' != dcCore::app()->prefix) { + if (!preg_match('/^' . preg_quote(dcCore::app()->prefix) . '(.*?)$/', $v, $m)) { + continue; + } + $v = $m[1]; + } + $rs[$i]['key'] = $v; + $rs[$i]['value'] = dcCore::app()->con->select('SELECT count(*) FROM ' . $res[$k])->f(0); + $i++; + } + + return $rs; + } + + public function execute(string $action, string $ns): bool + { + if (in_array($action, ['empty', 'delete'])) { + dcCore::app()->con->execute( + 'DELETE FROM ' . dcCore::app()->con->escapeSystem(dcCore::app()->prefix . $ns) + ); + } + if ($action == 'empty') { + return true; + } + if ($action == 'delete') { + dcCore::app()->con->execute( + 'DROP TABLE ' . dcCore::app()->con->escapeSystem(dcCore::app()->prefix . $ns) + ); + + return true; + } + + return false; + } +} diff --git a/src/Cleaner/Themes.php b/src/Cleaner/Themes.php new file mode 100644 index 0000000..45de3b5 --- /dev/null +++ b/src/Cleaner/Themes.php @@ -0,0 +1,72 @@ + 'themes', + 'name' => __('Themes'), + 'desc' => __('Folders from blog themes directory'), + ]; + } + + protected function actions(): array + { + return [ + new ActionDescriptor([ + 'id' => 'delete', + 'query' => __('delete "%s" theme directory'), + 'success' => __('"%s" theme directory deleted'), + 'error' => __('Failed to delete "%s" theme directory'), + ]), + ]; + } + + public function distributed(): array + { + return explode(',', DC_DISTRIB_THEMES); + } + + public function values(): array + { + $res = self::getDirs(dcCore::app()->blog->themes_path); + sort($res); + + return $res; + } + + public function execute(string $action, string $ns): bool + { + if ($action == 'delete') { + self::delDir(dcCore::app()->blog->themes_path, $ns, true); + + return true; + } + + return false; + } +} diff --git a/src/Cleaner/Vars.php b/src/Cleaner/Vars.php new file mode 100644 index 0000000..c30d062 --- /dev/null +++ b/src/Cleaner/Vars.php @@ -0,0 +1,68 @@ + 'vars', + 'name' => __('Var'), + 'desc' => __('Folders from Dotclear VAR directory'), + ]; + } + + protected function actions(): array + { + return [ + new ActionDescriptor([ + 'id' => 'delete', + 'query' => __('delete "%s" var directory'), + 'success' => __('"%s" var directory deleted'), + 'error' => __('Failed to delete "%s" var directory'), + ]), + ]; + } + + public function distributed(): array + { + return []; + } + + public function values(): array + { + return self::getDirs(DC_VAR); + } + + public function execute(string $action, string $ns): bool + { + if ($action == 'delete') { + self::delDir(DC_VAR, $ns, true); + + return true; + } + + return false; + } +} diff --git a/src/Cleaner/Versions.php b/src/Cleaner/Versions.php new file mode 100644 index 0000000..b9aee02 --- /dev/null +++ b/src/Cleaner/Versions.php @@ -0,0 +1,91 @@ + 'versions', + 'name' => __('Versions'), + 'desc' => __('Versions registered in table "version" of Dotclear'), + ]; + } + + protected function actions(): array + { + return [ + new ActionDescriptor([ + 'id' => 'delete', + 'query' => __('delete "%s" version number'), + 'success' => __('"%s" version number deleted'), + 'error' => __('Failed to delete "%s" version number'), + ]), + ]; + } + + public function distributed(): array + { + return [ + 'antispam', + 'blogroll', + 'blowupConfig', + 'core', + 'dcCKEditor', + 'dcLegacyEditor', + 'pages', + 'pings', + 'simpleMenu', + 'tags', + 'widgets', + ]; + } + + public function values(): array + { + $res = dcCore::app()->con->select('SELECT * FROM ' . dcCore::app()->prefix . dcCore::VERSION_TABLE_NAME); + + $rs = []; + $i = 0; + while ($res->fetch()) { + $rs[$i]['key'] = $res->f('module'); + $rs[$i]['value'] = $res->f('version'); + $i++; + } + + return $rs; + } + + public function execute(string $action, string $ns): bool + { + if ($action == 'delete') { + dcCore::app()->con->execute( + 'DELETE FROM ' . dcCore::app()->prefix . dcCore::VERSION_TABLE_NAME . ' ' . + "WHERE module = '" . dcCore::app()->con->escapeStr((string) $ns) . "' " + ); + + return true; + } + + return false; + } +} diff --git a/src/Cleaners.php b/src/Cleaners.php new file mode 100644 index 0000000..36b211b --- /dev/null +++ b/src/Cleaners.php @@ -0,0 +1,103 @@ + $cleaners The cleaner stack */ + private array $cleaners = []; + + /** + * Constructor register the cleaners. + */ + public function __construct() + { + $cleaners = new ArrayObject(); + + try { + # --BEHAVIOR-- UninstallerAddCleaner: ArrayObject + dcCore::app()->callBehavior('UninstallerAddCleaner', $cleaners); + + foreach ($cleaners as $cleaner) { + if (is_a($cleaner, AbstractCleaner::class) && !isset($this->cleaners[$cleaner->id])) { + $this->cleaners[$cleaner->id] = $cleaner; + } + } + } catch (Exception $e) { + dcCore::app()->error->add($e->getMessage()); + } + } + + /** + * Get all clearners. + * + * @return array The cleaners + */ + public function dump(): array + { + return $this->cleaners; + } + + /** + * Get a cleaner. + * + * @param string $id The cleaner id + * + * @return null|AbstractCleaner The cleaner + */ + public function get(string $id): ?AbstractCleaner + { + return $this->cleaners[$id] ?? null; + } + + /** + * Execute cleaner action on an value. + * + * @param string $id The cleaner id + * @param string $action The action id + * @param string $ns The value + * + * @return bool The success + */ + public function execute(string $id, string $action, string $ns): bool + { + if (!isset($this->cleaners[$id])) { + throw new Exception(sprintf(__('Unknown cleaner "%s"'), $id)); + } + if ($ns == My::root()) { + throw new Exception(__("Unsintaller can't remove itself")); + } + + # --BEHAVIOR-- UninstallerBeforeAction: string, string, string + dcCore::app()->callBehavior('UninstallerBeforeAction', $id, $action, $ns); + + $ret = $this->cleaners[$id]->execute($action, $ns); + + if ($ret === false) { + $msg = $this->cleaners[$id]->actions[$action]->error; + + throw new Exception($msg ?: __('Unknown error')); + } + + return true; + } +} diff --git a/src/Install.php b/src/Install.php new file mode 100644 index 0000000..a0907db --- /dev/null +++ b/src/Install.php @@ -0,0 +1,55 @@ +newVersion(My::id(), dcCore::app()->plugins->moduleInfo(My::id(), 'version')); + + return static::$init; + } + + public static function process(): bool + { + if (!static::$init) { + return false; + } + + try { + dcCore::app()->blog->settings->get('system')->put( + 'no_direct_uninstall', + false, + 'boolean', + 'Disabled uninstall actions on module deletion', + false, + true + ); + + return true; + } catch (Exception $e) { + dcCore::app()->error->add($e->getMessage()); + + return false; + } + } +} diff --git a/src/Manage.php b/src/Manage.php new file mode 100644 index 0000000..9fcefb2 --- /dev/null +++ b/src/Manage.php @@ -0,0 +1,167 @@ +auth?->isSuperAdmin() + && My::phpCompliant(); + + return static::$init; + } + + public static function process(): bool + { + if (!static::$init) { + return false; + } + + $type = ($_REQUEST['type'] ?? 'theme') == 'theme' ? 'themes' : 'plugins'; + $redir = $type == 'themes' ? ['blog.themes', [], '#themes'] : ['admin.plugins', [], '#plugins']; + + if (empty($_REQUEST['id'])) { + dcCore::app()->adminurl->redirect($redir[0], $redir[1], $redir[2]); + } + + if ($type == 'themes' && !is_a(dcCore::app()->themes, 'dcThemes')) { + dcCore::app()->themes = new dcThemes(); + dcCore::app()->themes->loadModules(dcCore::app()->blog->themes_path, null); + } + + $define = dcCore::app()->{$type}->getDefine($_REQUEST['id']); + if (!$define->isDefined()) { + dcCore::app()->error->add(__('Unknown module id to uninstall')); + dcCore::app()->adminurl->redirect($redir[0], $redir[1], $redir[2]); + } + + $uninstaller = Uninstaller::instance()->loadModules([$define]); + $actions = $uninstaller->getUserActions($define->getId()); + if (empty($actions)) { + dcCore::app()->error->add(__('There are no uninstall actions for this module')); + dcCore::app()->adminurl->redirect($redir[0], $redir[1], $redir[2]); + } + + if (empty($_POST)) { + return true; + } + + try { + $done = []; + foreach ($actions as $cleaner => $stack) { + foreach ($stack as $action) { + if (isset($_POST['action'][$cleaner]) && isset($_POST['action'][$cleaner][$action['action']])) { + if ($uninstaller->execute($cleaner, $action['action'], $_POST['action'][$cleaner][$action['action']])) { + $done[] = sprintf($action['success'], $_POST['action'][$cleaner][$action['action']]); + } else { + dcCore::app()->error->add(sprintf($action['error'], $_POST['action'][$cleaner][$action['action']])); + } + } + } + } + if (!empty($done)) { + array_unshift($done, __('Uninstall action successfuly excecuted')); + dcPage::addSuccessNotice(implode('
', $done)); + } else { + dcPage::addWarningNotice(__('No uninstall action done')); + } + dcCore::app()->adminurl->redirect($redir[0], $redir[1], $redir[2]); + } catch (Exception $e) { + dcCore::app()->error->add($e->getMessage()); + } + + return true; + } + + public static function render(): void + { + if (!static::$init) { + return; + } + + $type = $_REQUEST['type'] == 'theme' ? 'themes' : 'plugins'; + $redir = $type == 'themes' ? ['blog.themes', [], '#themes'] : ['admin.plugins', [], '#plugins']; + $define = dcCore::app()->{$type}->getDefine($_REQUEST['id']); + $uninstaller = Uninstaller::instance()->loadModules([$define]); + $fields = []; + + // custom actions form fields + if ($uninstaller->hasRender($define->getId())) { + $fields[] = (new Text('', $uninstaller->render($define->getId()))); + } + + dcPage::openModule( + My::name(), + dcPage::jsJson('uninstaller', ['confirm_uninstall' => __('Are you sure you perform these ations?')]) . + dcPage::jsModuleLoad(My::id() . '/js/backend.js') . + + # --BEHAVIOR-- UninstallerHeader + dcCore::app()->callBehavior('UninstallerHeader') + ); + + echo + dcPage::breadcrumb([ + __('System') => '', + My::name() => '', + ]) . + dcPage::notices(); + + // user actions form fields + foreach ($uninstaller->getUserActions($define->getId()) as $cleaner => $stack) { + foreach ($stack as $action) { + $fields[] = (new Para())->items([ + (new Checkbox(['action[' . $cleaner . '][' . $action['action'] . ']', 'action_' . $cleaner . '_' . $action['action']], true))->value($action['ns']), + (new Label(sprintf($action['query'], $action['ns']), Label::OUTSIDE_LABEL_AFTER))->for('action_' . $cleaner . '_' . $action['action'])->class('classic'), + ]); + } + } + + // submit + $fields[] = (new Para())->items([ + dcCore::app()->formNonce(false), + (new Hidden(['type'], $type)), + (new Hidden(['id'], $define->getId())), + (new Submit(['do']))->value(__('Perform selected actions'))->class('delete'), + (new Text('', ' ' . __('Cancel') . '')), + ]); + + // display form + echo (new Div())->items([ + (new Text('h3', sprintf(($type == 'themes' ? __('Uninstall theme "%s"') : __('Uninstall plugin "%s"')), __($define->get('name'))))), + (new Text('p', sprintf(__('The module "%s %s" offers advanced unsintall process:'), $define->getId(), $define->get('version')))), + (new Form('uninstall-form'))->method('post')->action(dcCore::app()->adminurl->get('admin.plugin.' . My::id()))->fields($fields), + ])->render(); + + dcPage::closeModule(); + } +} diff --git a/src/My.php b/src/My.php new file mode 100644 index 0000000..ee90d46 --- /dev/null +++ b/src/My.php @@ -0,0 +1,58 @@ +plugins->moduleInfo(self::id(), 'name')); + } + + /** + * This plugin root + */ + public static function root(): string + { + return dirname(__DIR__); + } + + /** + * Check php version + */ + public static function phpCompliant(): bool + { + return version_compare(phpversion(), self::PHP_MIN, '>='); + } +} diff --git a/src/Prepend.php b/src/Prepend.php new file mode 100644 index 0000000..a214dbe --- /dev/null +++ b/src/Prepend.php @@ -0,0 +1,48 @@ +auth->isSuperAdmin(); + + return static::$init; + } + + public static function process(): bool + { + if (!static::$init) { + return false; + } + + // Add cleaners to Uninstaller + dcCore::app()->addBehavior('UninstallerAddCleaner', [Cleaner\Settings::class, 'create']); + dcCore::app()->addBehavior('UninstallerAddCleaner', [Cleaner\Tables::class, 'create']); + dcCore::app()->addBehavior('UninstallerAddCleaner', [Cleaner\Versions::class, 'create']); + dcCore::app()->addBehavior('UninstallerAddCleaner', [Cleaner\Caches::class, 'create']); + dcCore::app()->addBehavior('UninstallerAddCleaner', [Cleaner\Vars::class, 'create']); + dcCore::app()->addBehavior('UninstallerAddCleaner', [Cleaner\Themes::class, 'create']); + dcCore::app()->addBehavior('UninstallerAddCleaner', [Cleaner\Plugins::class, 'create']); + + return true; + } +} diff --git a/src/TraitCleanerDir.php b/src/TraitCleanerDir.php new file mode 100644 index 0000000..32afeac --- /dev/null +++ b/src/TraitCleanerDir.php @@ -0,0 +1,130 @@ +read()) !== false) { + if ($entryname != '.' && $entryname != '..') { + if (is_dir($dir . DIRECTORY_SEPARATOR . $entryname)) { + if (!self::delTree($dir . DIRECTORY_SEPARATOR . $entryname)) { + return false; + } + } else { + if (!@unlink($dir . DIRECTORY_SEPARATOR . $entryname)) { + return false; + } + } + } + } + $d->close(); + + if ($delroot) { + return @rmdir($dir); + } + + return true; + } +} diff --git a/src/Uninstaller.php b/src/Uninstaller.php new file mode 100644 index 0000000..9c1f9a8 --- /dev/null +++ b/src/Uninstaller.php @@ -0,0 +1,303 @@ + $modules Loaded modules stack */ + private array $modules = []; + + /** @var array List of modules with custom actions render */ + private array $renders = []; + + /** @var array List of registered actions */ + private array $actions = ['user' => [], 'direct' => []]; + + /** + * Constructor load cleaners. + */ + public function __construct() + { + $this->cleaners = new Cleaners(); + } + + /** + * Get singleton instance. + * + * @return Uninstaller Uninstaller instance + */ + public static function instance(): Uninstaller + { + if (!is_a(self::$uninstaller, Uninstaller::class)) { + self::$uninstaller = new Uninstaller(); + } + + return self::$uninstaller; + } + + /** + * Load modules. + * + * Load modules resets previously loaded modules and actions. + * + * @param array $modules List of modules Define + * + * @return Uninstaller Uninstaller instance + */ + public function loadModules(array $modules): Uninstaller + { + // reset unsintaller + $this->module = null; + $this->modules = []; + $this->renders = []; + $this->actions = ['user' => [], 'direct' => []]; + + foreach ($modules as $module) { + if (!($module instanceof dcModuleDefine)) { + continue; + } + $class = $module->get('namespace') . '\\Uninstall'; + if ($module->getId() != My::id() && is_a($class, dcNsProcess::class, true)) { + $this->modules[$module->getId()] = $this->module = $module; + if ($class::init()) { + if ($class::process()) { + $this->renders[] = $module->getId(); + } + $this->module = null; + } + } + } + uasort( + $this->modules, + fn ($a, $b) => dcUtils::removeDiacritics(mb_strtolower($a->get('name'))) <=> dcUtils::removeDiacritics(mb_strtolower($b->get('name'))) + ); + + return $this; + } + + /** + * Get a module $id Define if it exists. + * + * @param string $id The module ID + * + * @return dcModuleDefine Module Define + */ + public function getModule(string $id): ?dcModuleDefine + { + return $this->modules[$id] ?? null; + } + + /** + * Get all modules Define. + * + * @return array Modules Define + */ + public function getModules(): array + { + return $this->modules; + } + + /** + * Check if the module $id exists. + * + * @param string $id Module ID + * + * @return boolean Success + */ + public function hasModule(string $id): bool + { + return isset($this->modules[$id]); + } + + /** + * Check if the module $id has action custom fields. + * + * @param string $id Module ID + * + * @return boolean Success + */ + public function hasRender(string $id): bool + { + return isset($this->modules[$id]) && in_array($id, $this->renders); + } + + /** + * Add a predefined action to user unsintall features. + * + * This method should be called from module Uninstall::proces() method. + * User will be prompted before doing these actions. + * + * @param string $cleaner The cleaner ID + * @param string $action The action ID + * @param string $ns Name of setting related to module + * + * @return Uninstaller Uninstaller instance + */ + public function addUserAction(string $cleaner, string $action, string $ns): Uninstaller + { + $this->addAction('user', $cleaner, $action, $ns); + + return $this; + } + + /** + * Add a predefined action to direct unsintall features. + * + * This method should be called from module Uninstall::proces() method. + * Direct actions will be called from behavior xxxBeforeDelete and + * user will NOT be prompted before these actions execution. + * Note: If module is disabled, direct actions are not executed. + * + * @param string $cleaner The cleaner ID + * @param string $action The action ID + * @param string $ns Name of setting related to module. + * + * @return Uninstaller Uninstaller instance + */ + public function addDirectAction(string $cleaner, string $action, string $ns): Uninstaller + { + $this->addAction('direct', $cleaner, $action, $ns); + + return $this; + } + + /** + * Get modules $id predefined user actions associative array + * + * @param string $id The module ID + * @return array Modules id + */ + public function getUserActions(string $id): array + { + return $this->getActions('user', $id); + } + + /** + * Get modules $id predefined direct actions associative array + * + * @param string $id The module ID + * @return array Modules id + */ + public function getDirectActions(string $id): array + { + return $this->getActions('direct', $id); + } + + /** + * Get module $id custom actions fields. + * + * @param string $id The module ID + * @return string HTML render of custom form fields + */ + public function render(string $id): string + { + $output = ''; + if ($this->hasRender($id)) { + $class = $this->getModule($id)->get('namespace') . '\\Uninstall'; + + ob_start(); + + try { + $class::render(); + $output = (string) ob_get_contents(); + } catch (Exception $e) { + } + ob_end_clean(); + } + + return $output; + } + + /** + * Execute a predifined action. + * + * This function call dcAdvancedCleaner to do actions. + * + * @param string $cleaner The cleaner ID + * @param string $action The action ID + * @param string $ns Name of setting related to module. + * + * @return boolean Success + */ + public function execute(string $cleaner, string $action, string $ns): bool + { + if (!isset($this->cleaners->get($cleaner)->actions[$action]) || empty($ns)) { + return false; + } + $this->cleaners->execute($cleaner, $action, $ns); + + return true; + } + + private function addAction(string $group, string $cleaner, string $action, string $ns): void + { + if (!self::group($group) || null === $this->module) { + return; + } + if (empty($cleaner) || empty($ns)) { + return; + } + if (!isset($this->cleaners->get($cleaner)->actions[$action])) { + return; + } + $this->actions[$group][$this->module->getId()][$cleaner][] = array_merge( + [ + 'ns' => $ns, + 'action' => $action, + ], + $this->cleaners->get($cleaner)->actions[$action]->dump() + ); + } + + private function getActions(string $group, string $id): array + { + if (!self::group($group) || !isset($this->actions[$group][$id])) { + return []; + } + $res = []; + foreach ($this->cleaners->dump() as $cleaner) { + if (!isset($this->actions[$group][$id][$cleaner->id])) { + continue; + } + $res[$cleaner->id] = $this->actions[$group][$id][$cleaner->id]; + } + + return $res; + } + + private function group(string $group): bool + { + return in_array($group, ['user', 'direct']); + } +}