PageRenderTime 41ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 1ms

/plugins/Installer/src/Shell/Task/PluginUninstallTask.php

http://github.com/QuickAppsCMS/QuickApps-CMS
PHP | 288 lines | 179 code | 47 blank | 62 comment | 27 complexity | 8be052c7df3e99fa1708b6c9fd0a723e MD5 | raw file
Possible License(s): LGPL-2.1, MPL-2.0-no-copyleft-exception, GPL-3.0
  1. <?php
  2. /**
  3. * Licensed under The GPL-3.0 License
  4. * For full copyright and license information, please see the LICENSE.txt
  5. * Redistributions of files must retain the above copyright notice.
  6. *
  7. * @since 2.0.0
  8. * @author Christopher Castro <chris@quickapps.es>
  9. * @link http://www.quickappscms.org
  10. * @license http://opensource.org/licenses/gpl-3.0.html GPL-3.0 License
  11. */
  12. namespace Installer\Shell\Task;
  13. use Cake\Console\Shell;
  14. use Cake\Datasource\ConnectionManager;
  15. use Cake\Filesystem\Folder;
  16. use CMS\Core\Package\PluginPackage;
  17. use CMS\Core\Plugin;
  18. use CMS\Event\EventDispatcherTrait;
  19. use Installer\Shell\Task\ListenerHandlerTrait;
  20. use User\Utility\AcoManager;
  21. /**
  22. * Plugin uninstaller.
  23. *
  24. * @property \System\Model\Table\PluginsTable $Plugins
  25. * @property \System\Model\Table\OptionsTable $Options
  26. */
  27. class PluginUninstallTask extends Shell
  28. {
  29. use EventDispatcherTrait;
  30. /**
  31. * The plugin being managed by this task.
  32. *
  33. * @var \CMS\Core\Package\PluginPackage
  34. */
  35. protected $_plugin = null;
  36. /**
  37. * Removes the welcome message.
  38. *
  39. * @return void
  40. */
  41. public function startup()
  42. {
  43. }
  44. /**
  45. * Gets the option parser instance and configures it.
  46. *
  47. * @return \Cake\Console\ConsoleOptionParser
  48. */
  49. public function getOptionParser()
  50. {
  51. $parser = parent::getOptionParser();
  52. $parser
  53. ->description(__d('installer', 'Uninstall an existing plugin.'))
  54. ->addOption('plugin', [
  55. 'short' => 'p',
  56. 'help' => __d('installer', 'Name of the plugin to uninstall.'),
  57. ])
  58. ->addOption('no-callbacks', [
  59. 'short' => 'c',
  60. 'help' => __d('installer', 'Plugin events will not be trigged.'),
  61. 'boolean' => true,
  62. 'default' => false,
  63. ]);
  64. return $parser;
  65. }
  66. /**
  67. * Task main method.
  68. *
  69. * @return bool
  70. */
  71. public function main()
  72. {
  73. $connection = ConnectionManager::get('default');
  74. $result = $connection->transactional(function ($conn) {
  75. try {
  76. $result = $this->_runTransactional();
  77. } catch (\Exception $ex) {
  78. $this->err(__d('installer', 'Something went wrong. Details: {0}', $ex->getMessage()));
  79. $result = false;
  80. }
  81. return $result;
  82. });
  83. // ensure snapshot
  84. snapshot();
  85. return $result;
  86. }
  87. /**
  88. * Runs uninstallation logic inside a safe transactional thread. This prevent
  89. * DB inconsistencies on uninstall failure.
  90. *
  91. * @return bool True on success, false otherwise
  92. */
  93. protected function _runTransactional()
  94. {
  95. // to avoid any possible issue
  96. snapshot();
  97. if (!is_writable(TMP)) {
  98. $this->err(__d('installer', 'Enable write permissions in /tmp directory before uninstall any plugin or theme.'));
  99. return false;
  100. }
  101. if (!$this->params['plugin']) {
  102. $this->err(__d('installer', 'No plugin/theme was given to remove.'));
  103. return false;
  104. }
  105. $this->loadModel('System.Plugins');
  106. try {
  107. $plugin = plugin($this->params['plugin']);
  108. $pluginEntity = $this->Plugins
  109. ->find()
  110. ->where(['name' => $this->params['plugin']])
  111. ->limit(1)
  112. ->first();
  113. } catch (\Exception $ex) {
  114. $plugin = $pluginEntity = false;
  115. }
  116. if (!$plugin || !$pluginEntity) {
  117. $this->err(__d('installer', 'Plugin "{0}" was not found.', $this->params['plugin']));
  118. return false;
  119. }
  120. $this->_plugin = $plugin;
  121. $type = $plugin->isTheme ? 'theme' : 'plugin';
  122. if ($plugin->isTheme && in_array($plugin->name, [option('front_theme'), option('back_theme')])) {
  123. $this->err(__d('installer', '{0} "{1}" is currently being used and cannot be removed.', ($type == 'plugin' ? __d('installer', 'The plugin') : __d('installer', 'The theme')), $plugin->humanName));
  124. return false;
  125. }
  126. $requiredBy = Plugin::checkReverseDependency($this->params['plugin']);
  127. if (!empty($requiredBy)) {
  128. $names = [];
  129. foreach ($requiredBy as $p) {
  130. $names[] = $p->name();
  131. }
  132. $this->err(__d('installer', '{0} "{1}" cannot be removed as it is required by: {2}', ($type == 'plugin' ? __d('installer', 'The plugin') : __d('installer', 'The theme')), $plugin->humanName, implode(', ', $names)));
  133. return false;
  134. }
  135. if (!$this->_canBeDeleted($plugin->path)) {
  136. return false;
  137. }
  138. if (!$this->params['no-callbacks']) {
  139. try {
  140. $event = $this->trigger("Plugin.{$plugin->name}.beforeUninstall");
  141. if ($event->isStopped() || $event->result === false) {
  142. $this->err(__d('installer', 'Task was explicitly rejected by {0}.', ($type == 'plugin' ? __d('installer', 'the plugin') : __d('installer', 'the theme'))));
  143. return false;
  144. }
  145. } catch (\Exception $e) {
  146. $this->err(__d('installer', 'Internal error, {0} did not respond to "beforeUninstall" callback correctly.', ($type == 'plugin' ? __d('installer', 'the plugin') : __d('installer', 'the theme'))));
  147. return false;
  148. }
  149. }
  150. if (!$this->Plugins->delete($pluginEntity)) {
  151. $this->err(__d('installer', '{0} "{1}" could not be unregistered from DB.', ($type == 'plugin' ? __d('installer', 'The plugin') : __d('installer', 'The theme')), $plugin->humanName));
  152. return false;
  153. }
  154. $this->_removeOptions();
  155. $this->_clearAcoPaths();
  156. $folder = new Folder($plugin->path);
  157. $folder->delete();
  158. snapshot();
  159. if (!$this->params['no-callbacks']) {
  160. try {
  161. $this->trigger("Plugin.{$plugin->name}.afterUninstall");
  162. } catch (\Exception $e) {
  163. $this->err(__d('installer', '{0} did not respond to "afterUninstall" callback.', ($type == 'plugin' ? __d('installer', 'The plugin') : __d('installer', 'The theme'))));
  164. }
  165. }
  166. Plugin::unload($plugin->name);
  167. Plugin::dropCache();
  168. return true;
  169. }
  170. /**
  171. * Removes from "options" table any entry registered by the plugin.
  172. *
  173. * @return void
  174. */
  175. protected function _removeOptions()
  176. {
  177. $options = [];
  178. if (!empty($this->_plugin->composer['extra']['options'])) {
  179. $this->loadModel('System.Options');
  180. $options = $this->_plugin->composer['extra']['options'];
  181. }
  182. foreach ($options as $option) {
  183. if (!empty($option['name'])) {
  184. $this->Options->deleteAll(['name' => $option['name']]);
  185. }
  186. }
  187. }
  188. /**
  189. * Removes all ACOs created by the plugin being uninstall.
  190. *
  191. * @return void
  192. */
  193. protected function _clearAcoPaths()
  194. {
  195. $this->loadModel('User.Acos');
  196. $nodes = $this->Acos
  197. ->find()
  198. ->where(['plugin' => $this->params['plugin']])
  199. ->order(['lft' => 'ASC'])
  200. ->all();
  201. foreach ($nodes as $node) {
  202. $this->Acos->removeFromTree($node);
  203. $this->Acos->delete($node);
  204. }
  205. AcoManager::buildAcos(null, true); // clear anything else
  206. }
  207. /**
  208. * Recursively checks if the given directory (and its content) can be deleted.
  209. *
  210. * This method automatically registers an error message if validation fails.
  211. *
  212. * @param string $path Directory to check
  213. * @return bool
  214. */
  215. protected function _canBeDeleted($path)
  216. {
  217. $type = $this->_plugin->isTheme ? 'theme' : 'plugin';
  218. if (!file_exists($path) || !is_dir($path)) {
  219. $this->err(__d('installer', "{0} directory was not found: {1}", ($type == 'plugin' ? __d('installer', "Plugin's") : __d('installer', "Theme's")), $path));
  220. return false;
  221. }
  222. $folder = new Folder($path);
  223. $folderContent = $folder->tree();
  224. $notWritable = [];
  225. foreach ($folderContent as $foldersOrFiles) {
  226. foreach ($foldersOrFiles as $element) {
  227. if (!is_writable($element)) {
  228. $notWritable[] = $element;
  229. }
  230. }
  231. }
  232. if (!empty($notWritable)) {
  233. $this->err(__d('installer', "{0} files or directories cannot be removed from your server, please check write permissions of:", ($type == 'plugin' ? __d('installer', "Some plugin's") : __d('installer', "Some theme's"))));
  234. foreach ($notWritable as $path) {
  235. $this->err(__d('installer', ' - {0}', [$path]));
  236. }
  237. return false;
  238. }
  239. return true;
  240. }
  241. }