Merge branch 'MDL-39087-plugins-uninstall' of git://github.com/mudrd8mz/moodle

This commit is contained in:
Dan Poltawski
2013-04-17 12:09:20 +01:00
22 changed files with 1128 additions and 72 deletions
+446 -27
View File
@@ -107,6 +107,27 @@ class plugin_manager {
return $this->reorder_plugin_types(get_plugin_types($fullpaths));
}
/**
* Returns list of known plugins of the given type
*
* This method returns the subset of the tree returned by {@link self::get_plugins()}.
* If the given type is not known, empty array is returned.
*
* @param string $type plugin type, e.g. 'mod' or 'workshopallocation'
* @param bool $disablecache force reload, cache can be used otherwise
* @return array (string)plugin name (e.g. 'workshop') => corresponding subclass of {@link plugininfo_base}
*/
public function get_plugins_of_type($type, $disablecache=false) {
$plugins = $this->get_plugins($disablecache);
if (!isset($plugins[$type])) {
return array();
}
return $plugins[$type];
}
/**
* Returns a tree of known plugins and information about them
*
@@ -161,6 +182,41 @@ class plugin_manager {
return $this->pluginsinfo;
}
/**
* Returns list of all known subplugins of the given plugin
*
* For plugins that do not provide subplugins (i.e. there is no support for it),
* empty array is returned.
*
* @param string $component full component name, e.g. 'mod_workshop'
* @param bool $disablecache force reload, cache can be used otherwise
* @return array (string) component name (e.g. 'workshopallocation_random') => subclass of {@link plugininfo_base}
*/
public function get_subplugins_of_plugin($component, $disablecache=false) {
$pluginfo = $this->get_plugin_info($component, $disablecache);
if (is_null($pluginfo)) {
return array();
}
$subplugins = $this->get_subplugins($disablecache);
if (!isset($subplugins[$pluginfo->component])) {
return array();
}
$list = array();
foreach ($subplugins[$pluginfo->component] as $subdata) {
foreach ($this->get_plugins_of_type($subdata->type) as $subpluginfo) {
$list[$subpluginfo->component] = $subpluginfo;
}
}
return $list;
}
/**
* Returns list of plugins that define their subplugins and the information
* about them from the db/subplugins.php file.
@@ -221,12 +277,18 @@ class plugin_manager {
/**
* Returns a localized name of a given plugin
*
* @param string $plugin name of the plugin, eg mod_workshop or auth_ldap
* @param string $component name of the plugin, eg mod_workshop or auth_ldap
* @return string
*/
public function plugin_name($plugin) {
list($type, $name) = normalize_component($plugin);
return $this->pluginsinfo[$type][$name]->displayname;
public function plugin_name($component) {
$pluginfo = $this->get_plugin_info($component);
if (is_null($pluginfo)) {
throw new moodle_exception('err_unknown_plugin', 'core_plugin', '', array('plugin' => $component));
}
return $pluginfo->displayname;
}
/**
@@ -288,12 +350,15 @@ class plugin_manager {
}
/**
* Returns information about the known plugin, or null
*
* @param string $component frankenstyle component name.
* @param bool $disablecache force reload, cache can be used otherwise
* @return plugininfo_base|null the corresponding plugin information.
*/
public function get_plugin_info($component) {
list($type, $name) = normalize_component($component);
$plugins = $this->get_plugins();
public function get_plugin_info($component, $disablecache=false) {
list($type, $name) = $this->normalize_component($component);
$plugins = $this->get_plugins($disablecache);
if (isset($plugins[$type][$name])) {
return $plugins[$type][$name];
} else {
@@ -301,6 +366,38 @@ class plugin_manager {
}
}
/**
* 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.
@@ -371,6 +468,94 @@ class plugin_manager {
return $return;
}
/**
* Is it possible to uninstall the given plugin?
*
* False is returned if the plugininfo subclass declares the uninstall should
* not be allowed via {@link plugininfo_base::is_uninstall_allowed()} or if the
* core vetoes it (e.g. becase the plugin or some of its subplugins is required
* by some other installed plugin).
*
* @param string $component full frankenstyle name, e.g. mod_foobar
* @return bool
*/
public function can_uninstall_plugin($component) {
$pluginfo = $this->get_plugin_info($component);
if (is_null($pluginfo)) {
return false;
}
if (!$this->common_uninstall_check($pluginfo)) {
return false;
}
// If it has subplugins, check they can be uninstalled too.
$subplugins = $this->get_subplugins_of_plugin($pluginfo->component);
foreach ($subplugins as $subpluginfo) {
if (!$this->common_uninstall_check($subpluginfo)) {
return false;
}
// Check if there are some other plugins requiring this subplugin
// (but the parent and siblings).
foreach ($this->other_plugins_that_require($subpluginfo->component) as $requiresme) {
$ismyparent = ($pluginfo->component === $requiresme);
$ismysibling = in_array($requiresme, array_keys($subplugins));
if (!$ismyparent and !$ismysibling) {
return false;
}
}
}
// Check if there are some other plugins requiring this plugin
// (but its subplugins).
foreach ($this->other_plugins_that_require($pluginfo->component) as $requiresme) {
$ismysubplugin = in_array($requiresme, array_keys($subplugins));
if (!$ismysubplugin) {
return false;
}
}
return true;
}
/**
* 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 progress_trace $progress traces the process
* @return bool true on success, false on errors/problems
*/
public function uninstall_plugin($component, progress_trace $progress) {
$pluginfo = $this->get_plugin_info($component);
if (is_null($pluginfo)) {
return false;
}
// Give the pluginfo class a chance to execute some steps.
$result = $pluginfo->uninstall($progress);
if (!$result) {
return false;
}
// Call the legacy core function to uninstall the plugin.
ob_start();
uninstall_plugin($pluginfo->type, $pluginfo->name);
$progress->output(ob_get_clean());
return true;
}
/**
* Checks if there are some plugins with a known available update
*
@@ -388,6 +573,29 @@ class plugin_manager {
return false;
}
/**
* Check to see if the given plugin folder can be removed by the web server process.
*
* @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;
}
// 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.
@@ -640,6 +848,18 @@ class plugin_manager {
}
}
/**
* Wrapper for the core function {@link normalize_component()}.
*
* This is here just to make it possible to mock it in unit tests.
*
* @param string $component
* @return array
*/
protected function normalize_component($component) {
return normalize_component($component);
}
/**
* Reorders plugin types into a sequence to be displayed
*
@@ -670,6 +890,73 @@ class plugin_manager {
}
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;
}
/**
* Helper method that implements common uninstall prerequisities
*
* @param plugininfo_base $pluginfo
* @return bool
*/
protected function common_uninstall_check(plugininfo_base $pluginfo) {
if (!$pluginfo->is_uninstall_allowed()) {
// The plugin's plugininfo class declares it should not be uninstalled.
return false;
}
if (is_null($pluginfo->get_uninstall_url())) {
// Backwards compatibility.
debugging('plugininfo_base subclasses should use is_uninstall_allowed() instead of returning null in get_uninstall_url()',
DEBUG_DEVELOPER);
return false;
}
return true;
}
}
@@ -1662,6 +1949,7 @@ class available_update_deployer {
/**
* 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
*/
@@ -2287,6 +2575,24 @@ abstract class plugininfo_base {
return $this->dependencies;
}
/**
* Is this is a subplugin?
*
* @return boolean
*/
public function is_subplugin() {
return ($this->get_parent_plugin() !== false);
}
/**
* If I am a subplugin, return the name of my parent plugin.
*
* @return string|bool false if not a subplugin, name of the parent otherwise
*/
public function get_parent_plugin() {
return $this->get_plugin_manager()->get_parent_of_subplugin($this->type);
}
/**
* Sets {@link $versiondb} property to a numerical value representing the
* currently installed version of the plugin.
@@ -2496,19 +2802,43 @@ abstract class plugininfo_base {
public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
}
/**
* Should there be a way to uninstall the plugin via the administration UI
*
* By default, uninstallation is allowed for all non-standard add-ons. Subclasses
* may want to override this to allow uninstallation of all plugins (simply by
* returning true unconditionally). Subplugins follow their parent plugin's
* decision by default.
*
* Note that even if true is returned, the core may still prohibit the uninstallation,
* e.g. in case there are other plugins that depend on this one.
*
* @return boolean
*/
public function is_uninstall_allowed() {
if ($this->is_subplugin()) {
return $this->get_plugin_manager()->get_plugin_info($this->get_parent_plugin())->is_uninstall_allowed();
}
if ($this->is_standard()) {
return false;
}
return true;
}
/**
* Returns the URL of the screen where this plugin can be uninstalled
*
* Visiting that URL must be safe, that is a manual confirmation is needed
* for actual uninstallation of the plugin. Null value means that the
* plugin either does not support uninstallation, or does not require any
* database cleanup or the location of the screen is not available via this
* library.
* for actual uninstallation of the plugin. By default, URL to a common
* uninstalling tool is returned.
*
* @return null|moodle_url
* @return moodle_url
*/
public function get_uninstall_url() {
return null;
return $this->get_default_uninstall_url();
}
/**
@@ -2522,6 +2852,36 @@ abstract class plugininfo_base {
return substr($this->rootdir, strlen($CFG->dirroot));
}
/**
* 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 progress_trace $progress traces the process
* @return bool true on success, false on failure
*/
public function uninstall(progress_trace $progress) {
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
* for uninstalling.
*
* @return moodle_url
*/
protected final function get_default_uninstall_url() {
return new moodle_url('/admin/plugins.php', array(
'sesskey' => sesskey(),
'uninstall' => $this->component,
'confirm' => 0,
));
}
/**
* Provides access to plugin versions from the {config_plugins} table
*
@@ -2552,6 +2912,15 @@ abstract class plugininfo_base {
return false;
}
}
/**
* Provides access to the plugin_manager singleton.
*
* @return plugin_manmager
*/
protected function get_plugin_manager() {
return plugin_manager::instance();
}
}
@@ -2678,8 +3047,11 @@ class plugininfo_block extends plugininfo_base {
}
}
public function get_uninstall_url() {
public function is_uninstall_allowed() {
return true;
}
public function get_uninstall_url() {
$blocksinfo = self::get_blocks_info();
return new moodle_url('/admin/blocks.php', array('delete' => $blocksinfo[$this->name]->id, 'sesskey' => sesskey()));
}
@@ -2814,6 +3186,10 @@ class plugininfo_filter extends plugininfo_base {
}
}
public function is_uninstall_allowed() {
return true;
}
public function get_uninstall_url() {
return new moodle_url('/admin/filters.php', array('sesskey' => sesskey(), 'filterpath' => $this->name, 'action' => 'delete'));
}
@@ -2997,15 +3373,24 @@ class plugininfo_mod extends plugininfo_base {
}
}
public function get_uninstall_url() {
/**
* Allow all activity modules but Forum to be uninstalled.
if ($this->name !== 'forum') {
return new moodle_url('/admin/modules.php', array('delete' => $this->name, 'sesskey' => sesskey()));
* This exception for the Forum has been hard-coded in Moodle since ages,
* we may want to re-think it one day.
*/
public function is_uninstall_allowed() {
if ($this->name === 'forum') {
return false;
} else {
return null;
return true;
}
}
public function get_uninstall_url() {
return new moodle_url('/admin/modules.php', array('delete' => $this->name, 'sesskey' => sesskey()));
}
/**
* Provides access to the records in {modules} table
*
@@ -3039,6 +3424,10 @@ class plugininfo_mod extends plugininfo_base {
*/
class plugininfo_qbehaviour extends plugininfo_base {
public function is_uninstall_allowed() {
return true;
}
public function get_uninstall_url() {
return new moodle_url('/admin/qbehaviours.php',
array('delete' => $this->name, 'sesskey' => sesskey()));
@@ -3051,6 +3440,10 @@ class plugininfo_qbehaviour extends plugininfo_base {
*/
class plugininfo_qtype extends plugininfo_base {
public function is_uninstall_allowed() {
return true;
}
public function get_uninstall_url() {
return new moodle_url('/admin/qtypes.php',
array('delete' => $this->name, 'sesskey' => sesskey()));
@@ -3175,6 +3568,10 @@ class plugininfo_enrol extends plugininfo_base {
}
}
public function is_uninstall_allowed() {
return true;
}
public function get_uninstall_url() {
return new moodle_url('/admin/enrol.php', array('action' => 'uninstall', 'enrol' => $this->name, 'sesskey' => sesskey()));
}
@@ -3225,16 +3622,21 @@ class plugininfo_message extends plugininfo_base {
}
}
public function is_uninstall_allowed() {
$processors = get_message_processors();
if (isset($processors[$this->name])) {
return true;
} else {
return false;
}
}
/**
* @see plugintype_interface::get_uninstall_url()
*/
public function get_uninstall_url() {
$processors = get_message_processors();
if (isset($processors[$this->name])) {
return new moodle_url('/admin/message.php', array('uninstall' => $processors[$this->name]->id, 'sesskey' => sesskey()));
} else {
return parent::get_uninstall_url();
}
return new moodle_url('/admin/message.php', array('uninstall' => $processors[$this->name]->id, 'sesskey' => sesskey()));
}
}
@@ -3379,6 +3781,10 @@ class plugininfo_mnetservice extends plugininfo_base {
*/
class plugininfo_tool extends plugininfo_base {
public function is_uninstall_allowed() {
return true;
}
public function get_uninstall_url() {
return new moodle_url('/admin/tools.php', array('delete' => $this->name, 'sesskey' => sesskey()));
}
@@ -3390,6 +3796,10 @@ class plugininfo_tool extends plugininfo_base {
*/
class plugininfo_report extends plugininfo_base {
public function is_uninstall_allowed() {
return true;
}
public function get_uninstall_url() {
return new moodle_url('/admin/reports.php', array('delete' => $this->name, 'sesskey' => sesskey()));
}
@@ -3513,6 +3923,10 @@ class plugininfo_webservice extends plugininfo_base {
return false;
}
public function is_uninstall_allowed() {
return true;
}
public function get_uninstall_url() {
return new moodle_url('/admin/webservice/protocols.php',
array('sesskey' => sesskey(), 'action' => 'uninstall', 'webservice' => $this->name));
@@ -3568,11 +3982,16 @@ class plugininfo_format extends plugininfo_base {
return !get_config($this->component, 'disabled');
}
public function get_uninstall_url() {
public function is_uninstall_allowed() {
if ($this->name !== get_config('moodlecourse', 'format') && $this->name !== 'site') {
return new moodle_url('/admin/courseformats.php',
array('sesskey' => sesskey(), 'action' => 'uninstall', 'format' => $this->name));
return true;
} else {
return false;
}
return parent::get_uninstall_url();
}
public function get_uninstall_url() {
return new moodle_url('/admin/courseformats.php',
array('sesskey' => sesskey(), 'action' => 'uninstall', 'format' => $this->name));
}
}