Browse files

MDL-39087 Implement a common interface for uninstalling general plugin

Plugins may use this general tool for uninstallation and eventually
removal of the deployed source code. At the moment, this is implemented
as a wrapper for the core function uninstall_plugin() with an extra hook
in the relevant plugin info subclass.

For non-standard add-ons, the tool can remove the deployed plugin source
code as well, if the web server has required write permissions. Ideally,
all add-ons installed via the new tool_installaddon should be removable
via the web interface as well.
  • Loading branch information...
1 parent 0b733dd commit 436d94478d4959cb9fcd2b16e462d7b85dbd1b75 @mudrd8mz mudrd8mz committed Apr 10, 2013
Showing with 340 additions and 2 deletions.
  1. +81 −2 admin/plugins.php
  2. +95 −0 admin/renderer.php
  3. +5 −0 lang/en/plugin.php
  4. +157 −0 lib/pluginlib.php
  5. +2 −0 theme/base/style/admin.css
View
83 admin/plugins.php
@@ -27,15 +27,96 @@
require_once(dirname(dirname(__FILE__)) . '/config.php');
require_once($CFG->libdir . '/adminlib.php');
require_once($CFG->libdir . '/pluginlib.php');
+require_once($CFG->libdir . '/filelib.php');
admin_externalpage_setup('pluginsoverview');
require_capability('moodle/site:config', context_system::instance());
$fetchremote = optional_param('fetchremote', false, PARAM_BOOL);
$updatesonly = optional_param('updatesonly', false, PARAM_BOOL);
$contribonly = optional_param('contribonly', false, PARAM_BOOL);
+$uninstall = optional_param('uninstall', '', PARAM_COMPONENT);
+$delete = optional_param('delete', '', PARAM_COMPONENT);
+$confirmed = optional_param('confirm', false, PARAM_BOOL);
+
+$output = $PAGE->get_renderer('core', 'admin');
$pluginman = plugin_manager::instance();
+
+if ($uninstall) {
+ require_sesskey();
+ $pluginfo = $pluginman->get_plugin_info($uninstall);
+
+ if (is_null($pluginfo)) {
+ throw new moodle_exception('err_uninstalling_unknown_plugin', 'core_plugin', '', array('plugin' => $uninstall),
+ 'plugin_manager::get_plugin_info() returned null for the plugin to be uninstalled');
+ }
+
+ $requiredby = $pluginman->other_plugins_that_require($pluginfo->component);
+ if (!empty($requiredby)) {
+ throw new moodle_exception('err_uninstalling_required_plugin', 'core_plugin', '',
+ array('plugin' => $pluginfo->component, 'requiredby' => implode(', ', $requiredby)),
+ 'plugin_manager::other_plugins_that_require() returned non-empty array');
+ }
+
+ if (!$confirmed) {
+ $continueurl = new moodle_url($PAGE->url, array('uninstall' => $pluginfo->component, 'sesskey' => sesskey(), 'confirm' => 1));
+ echo $output->plugin_uninstall_confirm_page($pluginman, $pluginfo, $continueurl);
+ exit();
+
+ } else {
+ $messages = array(); // Collect uninstall process messages here.
+ $pluginman->uninstall_plugin($pluginfo->component, $messages);
+
+ if ($pluginman->is_plugin_folder_removable($pluginfo->component)) {
+ $continueurl = new moodle_url($PAGE->url, array('delete' => $pluginfo->component, 'sesskey' => sesskey(), 'confirm' => 1));
+ echo $output->plugin_uninstall_results_removable_page($pluginman, $pluginfo, $messages, $continueurl);
+ exit();
+
+ } else {
+ echo $output->plugin_uninstall_results_page($pluginman, $pluginfo, $messages);
+ exit();
+ }
+ }
+}
+
+if ($delete and $confirmed) {
+ require_sesskey();
+ $pluginfo = $pluginman->get_plugin_info($delete);
+
+ // Make sure we know the plugin.
+ if (is_null($pluginfo)) {
+ throw new moodle_exception('err_removing_unknown_plugin', 'core_plugin', '', array('plugin' => $delete),
+ 'plugin_manager::get_plugin_info() returned null for the plugin to be deleted');
+ }
+
+ // Make sure it is not installed.
+ if (!is_null($pluginfo->versiondb)) {
+ throw new moodle_exception('err_removing_installed_plugin', 'core_plugin', '',
+ array('plugin' => $pluginfo->component, 'versiondb' => $pluginfo->versiondb),
+ 'plugin_manager::get_plugin_info() returned not-null versiondb for the plugin to be deleted');
+ }
+
+ // Make sure the folder is removable.
+ if (!$pluginman->is_plugin_folder_removable($pluginfo->component)) {
+ throw new moodle_exception('err_removing_unremovable_folder', 'core_plugin', '',
+ array('plugin' => $pluginfo->component, 'rootdir' => $pluginfo->rootdir),
+ 'plugin root folder is not removable as expected');
+ }
+
+ // Make sure the folder is within Moodle installation tree.
+ if (strpos($pluginfo->rootdir, $CFG->dirroot) !== 0) {
+ throw new moodle_exception('err_unexpected_plugin_rootdir', 'core_plugin', '',
+ array('plugin' => $pluginfo->component, 'rootdir' => $pluginfo->rootdir, 'dirroot' => $CFG->dirroot),
+ 'plugin root folder not in the moodle dirroot');
+ }
+
+ // So long, and thanks for all the bugs.
+ fulldelete($pluginfo->rootdir);
+ cache::make('core', 'pluginlist')->purge();
+ redirect($PAGE->url);
+}
+
$checker = available_update_checker::instance();
// Filtering options.
@@ -50,8 +131,6 @@
redirect(new moodle_url($PAGE->url, $options));
}
-$output = $PAGE->get_renderer('core', 'admin');
-
$deployer = available_update_deployer::instance();
if ($deployer->enabled()) {
$myurl = new moodle_url($PAGE->url, array('updatesonly' => $updatesonly, 'contribonly' => $contribonly));
View
95 admin/renderer.php
@@ -372,6 +372,101 @@ public function plugin_management_page(plugin_manager $pluginman, available_upda
}
/**
+ * Display a page to confirm the plugin uninstallation.
+ *
+ * @param plugin_manager $pluginman
+ * @param plugin_info $pluginfo
+ * @param moodle_url $continueurl URL to continue after confirmation
+ * @return string
+ */
+ public function plugin_uninstall_confirm_page(plugin_manager $pluginman, plugininfo_base $pluginfo, moodle_url $continueurl) {
+ $output = '';
+
+ $pluginname = $pluginman->plugin_name($pluginfo->component);
+
+ $this->page->set_title($pluginname);
+ $this->page->navbar->add(get_string('uninstalling', 'core_plugin', array('name' => $pluginname)));
+
+ $output .= $this->output->header();
+ $output .= $this->output->heading(get_string('uninstalling', 'core_plugin', array('name' => $pluginname)));
+ $output .= $this->output->confirm(get_string('uninstallconfirm', 'core_plugin', array('name' => $pluginname)),
+ $continueurl, $this->page->url);
+ $output .= $this->output->footer();
+
+ return $output;
+ }
+
+ /**
+ * Display a page with results of plugin uninstallation and offer removal of plugin files.
+ *
+ * @param plugin_manager $pluginman
+ * @param plugin_info $pluginfo
+ * @param array $messages list of strings, the log of the process
+ * @param moodle_url $continueurl URL to continue to remove the plugin folder
+ * @return string
+ */
+ public function plugin_uninstall_results_removable_page(plugin_manager $pluginman, plugininfo_base $pluginfo,
+ array $messages = array(), moodle_url $continueurl) {
+ $output = '';
+
+ $pluginname = $pluginman->plugin_name($pluginfo->component);
+
+ $this->page->set_title($pluginname);
+ $this->page->navbar->add(get_string('uninstalling', 'core_plugin', array('name' => $pluginname)));
+
+ $output .= $this->output->header();
+ $output .= $this->output->heading(get_string('uninstalling', 'core_plugin', array('name' => $pluginname)));
+
+ foreach ($messages as $message) {
+ $output .= $this->output->box($message, 'generalbox uninstallresultmessage');
+ }
+
+ $confirm = $this->output->container(get_string('uninstalldeleteconfirm', 'core_plugin',
+ array('name' => $pluginname, 'rootdir' => $pluginfo->rootdir)), 'uninstalldeleteconfirm');
+
+ if ($repotype = $pluginman->plugin_external_source($pluginfo->component)) {
+ $confirm .= $this->output->container(get_string('uninstalldeleteconfirmexternal', 'core_plugin', $repotype),
+ 'uninstalldeleteconfirmexternal');
+ }
+
+ $output .= $this->output->confirm($confirm, $continueurl, $this->page->url);
+ $output .= $this->output->footer();
+
+ return $output;
+ }
+
+ /**
+ * Display a page with results of plugin uninstallation and inform about the need to remove plugin files manually.
+ *
+ * @param plugin_manager $pluginman
+ * @param plugin_info $pluginfo
+ * @param array $messages list of strings, the log of the process
+ * @return string
+ */
+ public function plugin_uninstall_results_page(plugin_manager $pluginman, plugininfo_base $pluginfo, array $messages = array()) {
+ $output = '';
+
+ $pluginname = $pluginman->plugin_name($pluginfo->component);
+
+ $this->page->set_title($pluginname);
+ $this->page->navbar->add(get_string('uninstalling', 'core_plugin', array('name' => $pluginname)));
+
+ $output .= $this->output->header();
+ $output .= $this->output->heading(get_string('uninstalling', 'core_plugin', array('name' => $pluginname)));
+
+ foreach ($messages as $message) {
+ $output .= $this->output->box($message, 'generalbox uninstallresultmessage');
+ }
+
+ $output .= $this->output->box(get_string('uninstalldelete', 'core_plugin',
+ array('name' => $pluginname, 'rootdir' => $pluginfo->rootdir)), 'generalbox uninstalldelete');
+ $output .= $this->output->continue_button($this->page->url);
+ $output .= $this->output->footer();
+
+ return $output;
+ }
+
+ /**
* Display the plugin management page (admin/environment.php).
* @param array $versions
* @param string $version
View
5 lang/en/plugin.php
@@ -147,6 +147,11 @@
$string['updatepluginconfirmexternal'] = 'It appears that the current version of the plugin has been obtained via source code management system ({$a}) checkout. If you install this update, you will no longer be able to obtain plugin updates from the source code management system. Please ensure that you definitely want to update the plugin before continuing.';
$string['updatepluginconfirmwarning'] = 'Please note that Moodle will not automatically make a backup of your database before the upgrade. We strongly recommend that you make a full snapshot backup now, to cope with the rare case that the new code has bugs that make your site unavailable or even corrupts your database. Proceed at your own risk.';
$string['uninstall'] = 'Uninstall';
+$string['uninstallconfirm'] = 'You are about to uninstall the plugin <em>{$a->name}</em>. This will completely delete everything in the database associated with this plugin, including its configuration, log records, user files managed by the plugin etc. There is no way back and Moodle itself does not create any recovery backup. Are you SURE you want to continue?';
+$string['uninstalldelete'] = 'All data associated with the plugin <em>{$a->name}</em> has been deleted from the database. To prevent the plugin re-installing itself, its folder <em>{$a->rootdir}</em> must be manually removed from your server now. Moodle itself cannot remove the folder due to write permissions.';
+$string['uninstalldeleteconfirm'] = 'All data associated with the plugin <em>{$a->name}</em> has been deleted from the database. To prevent the plugin re-installing itself, its folder <em>{$a->rootdir}</em> must be removed from your server. Do you want to remove the plugin folder now?';
+$string['uninstalldeleteconfirmexternal'] = 'It appears that the current version of the plugin has been obtained via source code management system ({$a}) checkout. If you remove the plugin folder, you may loose important local modifications of the code. Please ensure that you definitely want to remove the plugin folder before continuing.';
+$string['uninstalling'] = 'Uninstalling {$a->name}';
$string['version'] = 'Version';
$string['versiondb'] = 'Current version';
$string['versiondisk'] = 'New version';
View
157 lib/pluginlib.php
@@ -302,6 +302,38 @@ public function get_plugin_info($component) {
}
/**
+ * Check to see if the current version of the plugin seems to be a checkout of an external repository.
+ *
+ * @see available_update_deployer::plugin_external_source()
+ * @param string $component frankenstyle component name
+ * @return false|string
+ */
+ public function plugin_external_source($component) {
+
+ $plugininfo = $this->get_plugin_info($component);
+
+ if (is_null($plugininfo)) {
+ return false;
+ }
+
+ $pluginroot = $plugininfo->rootdir;
+
+ if (is_dir($pluginroot.'/.git')) {
+ return 'git';
+ }
+
+ if (is_dir($pluginroot.'/CVS')) {
+ return 'cvs';
+ }
+
+ if (is_dir($pluginroot.'/.svn')) {
+ return 'svn';
+ }
+
+ return false;
+ }
+
+ /**
* Get a list of any other plugins that require this one.
* @param string $component frankenstyle component name.
* @return array of frankensyle component names that require this one.
@@ -372,6 +404,42 @@ public function all_plugins_ok($moodleversion, &$failedplugins = array()) {
}
/**
+ * Uninstall the given plugin.
+ *
+ * Automatically cleans-up all remaining configuration data, log records, events,
+ * files from the file pool etc.
+ *
+ * In the future, the functionality of {@link uninstall_plugin()} function may be moved
+ * into this method and all the code should be refactored to use it. At the moment, we
+ * mimic this future behaviour by wrapping that function call.
+ *
+ * @param string $component
+ * @param array $messages log of the process is returned via this array
+ * @return bool true on success, false on errors/problems
+ */
+ public function uninstall_plugin($component, array &$messages) {
+
+ $pluginfo = $this->get_plugin_info($component);
+
+ if (is_null($pluginfo)) {
+ return false;
+ }
+
+ // Give the pluginfo class a perform some steps.
+ $result = $pluginfo->uninstall($messages);
+ if (!$result) {
+ return false;
+ }
+
+ // Call the legacy core function to uninstall the plugin.
+ ob_start();
+ uninstall_plugin($pluginfo->type, $pluginfo->name);
+ $messages[] = ob_get_clean();
+
+ return true;
+ }
+
+ /**
* Checks if there are some plugins with a known available update
*
* @return bool true if there is at least one available update
@@ -389,6 +457,36 @@ public function some_plugins_updatable() {
}
/**
+ * Check to see if the given plugin folder can be removed by the web server process.
+ *
+ * This is intended to be used for installed add-ons mainly. For standard plugins,
+ * false is always returned for now.
+ *
+ * @param string $component full frankenstyle component
+ * @return bool
+ */
+ public function is_plugin_folder_removable($component) {
+
+ $pluginfo = $this->get_plugin_info($component);
+
+ if (is_null($pluginfo)) {
+ return false;
+ }
+
+ if ($pluginfo->is_standard()) {
+ return false;
+ }
+
+ // To be able to remove the plugin folder, its parent must be writable, too.
+ if (!is_writable(dirname($pluginfo->rootdir))) {
+ return false;
+ }
+
+ // Check that the folder and all its content is writable (thence removable).
+ return $this->is_directory_removable($pluginfo->rootdir);
+ }
+
+ /**
* Defines a list of all plugins that were originally shipped in the standard Moodle distribution,
* but are not anymore and are deleted during upgrades.
*
@@ -670,6 +768,50 @@ protected function reorder_plugin_types(array $types) {
}
return $fix;
}
+
+ /**
+ * Check if the given directory can be removed by the web server process.
+ *
+ * This recursively checks that the given directory and all its contents
+ * it writable.
+ *
+ * @param string $fullpath
+ * @return boolean
+ */
+ protected function is_directory_removable($fullpath) {
+
+ if (!is_writable($fullpath)) {
+ return false;
+ }
+
+ if (is_dir($fullpath)) {
+ $handle = opendir($fullpath);
+ } else {
+ return false;
+ }
+
+ $result = true;
+
+ while ($filename = readdir($handle)) {
+
+ if ($filename === '.' or $filename === '..') {
+ continue;
+ }
+
+ $subfilepath = $fullpath.'/'.$filename;
+
+ if (is_dir($subfilepath)) {
+ $result = $result && $this->is_directory_removable($subfilepath);
+
+ } else {
+ $result = $result && is_writable($subfilepath);
+ }
+ }
+
+ closedir($handle);
+
+ return $result;
+ }
}
@@ -1662,6 +1804,7 @@ public function deployment_impediments(available_update_info $info) {
/**
* Check to see if the current version of the plugin seems to be a checkout of an external repository.
*
+ * @see plugin_manager::plugin_external_source()
* @param available_update_info $info
* @return false|string
*/
@@ -2537,6 +2680,20 @@ public function get_dir() {
}
/**
+ * Hook method to implement certain steps when uninstalling the plugin.
+ *
+ * This hook is called by {@link plugin_manager::uninstall_plugin()} so
+ * it is basically usable only for those plugin types that use the default
+ * uninstall tool provided by {@link self::get_default_uninstall_url()}.
+ *
+ * @param array $messages list of uninstall log messages
+ * @return bool true on success, false on failure
+ */
+ public function uninstall(array &$messages) {
+ return true;
+ }
+
+ /**
* Returns URL to a script that handles common plugin uninstall procedure.
*
* This URL is suitable for plugins that do not have their own UI
View
2 theme/base/style/admin.css
@@ -152,6 +152,8 @@
#page-admin-index .updateplugin .updatepluginconfirmexternal,
#page-admin-plugins .updateplugin .updatepluginconfirmexternal {padding:1em;background-color:#ffd3d9;border:1px solid #EEAAAA}
+#page-admin-plugins .uninstalldeleteconfirmexternal {margin:1em auto;padding:1em;background-color:#ffd3d9;border:1px solid #EEAAAA}
+
#page-admin-user-user_bulk #users .fgroup {white-space: nowrap;}
#page-admin-report-stats-index .graph {text-align: center;margin-bottom: 1em;}
#page-admin-report-courseoverview-index .graph {text-align: center;margin-bottom: 1em;}

0 comments on commit 436d944

Please sign in to comment.