MDL-45536 atto_html: Update the textarea size when switching to HTML view
[moodle.git] / lib / classes / plugin_manager.php
blob78e32ab2f30d4e9b300452bc0860fa7078e2967e
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17 /**
18 * Defines classes used for plugins management
20 * This library provides a unified interface to various plugin types in
21 * Moodle. It is mainly used by the plugins management admin page and the
22 * plugins check page during the upgrade.
24 * @package core
25 * @copyright 2011 David Mudrak <david@moodle.com>
26 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
29 defined('MOODLE_INTERNAL') || die();
31 /**
32 * Singleton class providing general plugins management functionality.
34 class core_plugin_manager {
36 /** the plugin is shipped with standard Moodle distribution */
37 const PLUGIN_SOURCE_STANDARD = 'std';
38 /** the plugin is added extension */
39 const PLUGIN_SOURCE_EXTENSION = 'ext';
41 /** the plugin uses neither database nor capabilities, no versions */
42 const PLUGIN_STATUS_NODB = 'nodb';
43 /** the plugin is up-to-date */
44 const PLUGIN_STATUS_UPTODATE = 'uptodate';
45 /** the plugin is about to be installed */
46 const PLUGIN_STATUS_NEW = 'new';
47 /** the plugin is about to be upgraded */
48 const PLUGIN_STATUS_UPGRADE = 'upgrade';
49 /** the standard plugin is about to be deleted */
50 const PLUGIN_STATUS_DELETE = 'delete';
51 /** the version at the disk is lower than the one already installed */
52 const PLUGIN_STATUS_DOWNGRADE = 'downgrade';
53 /** the plugin is installed but missing from disk */
54 const PLUGIN_STATUS_MISSING = 'missing';
56 /** @var core_plugin_manager holds the singleton instance */
57 protected static $singletoninstance;
58 /** @var array of raw plugins information */
59 protected $pluginsinfo = null;
60 /** @var array of raw subplugins information */
61 protected $subpluginsinfo = null;
62 /** @var array list of installed plugins $name=>$version */
63 protected $installedplugins = null;
64 /** @var array list of all enabled plugins $name=>$name */
65 protected $enabledplugins = null;
66 /** @var array list of all enabled plugins $name=>$diskversion */
67 protected $presentplugins = null;
68 /** @var array reordered list of plugin types */
69 protected $plugintypes = null;
71 /**
72 * Direct initiation not allowed, use the factory method {@link self::instance()}
74 protected function __construct() {
77 /**
78 * Sorry, this is singleton
80 protected function __clone() {
83 /**
84 * Factory method for this class
86 * @return core_plugin_manager the singleton instance
88 public static function instance() {
89 if (is_null(self::$singletoninstance)) {
90 self::$singletoninstance = new self();
92 return self::$singletoninstance;
95 /**
96 * Reset all caches.
97 * @param bool $phpunitreset
99 public static function reset_caches($phpunitreset = false) {
100 if ($phpunitreset) {
101 self::$singletoninstance = null;
102 } else {
103 if (self::$singletoninstance) {
104 self::$singletoninstance->pluginsinfo = null;
105 self::$singletoninstance->subpluginsinfo = null;
106 self::$singletoninstance->installedplugins = null;
107 self::$singletoninstance->enabledplugins = null;
108 self::$singletoninstance->presentplugins = null;
109 self::$singletoninstance->plugintypes = null;
112 $cache = cache::make('core', 'plugin_manager');
113 $cache->purge();
117 * Returns the result of {@link core_component::get_plugin_types()} ordered for humans
119 * @see self::reorder_plugin_types()
120 * @return array (string)name => (string)location
122 public function get_plugin_types() {
123 if (func_num_args() > 0) {
124 if (!func_get_arg(0)) {
125 throw coding_exception('core_plugin_manager->get_plugin_types() does not support relative paths.');
128 if ($this->plugintypes) {
129 return $this->plugintypes;
132 $this->plugintypes = $this->reorder_plugin_types(core_component::get_plugin_types());
133 return $this->plugintypes;
137 * Load list of installed plugins,
138 * always call before using $this->installedplugins.
140 * This method is caching results for all plugins.
142 protected function load_installed_plugins() {
143 global $DB, $CFG;
145 if ($this->installedplugins) {
146 return;
149 if (empty($CFG->version)) {
150 // Nothing installed yet.
151 $this->installedplugins = array();
152 return;
155 $cache = cache::make('core', 'plugin_manager');
156 $installed = $cache->get('installed');
158 if (is_array($installed)) {
159 $this->installedplugins = $installed;
160 return;
163 $this->installedplugins = array();
165 // TODO: Delete this block once Moodle 2.6 or later becomes minimum required version to upgrade.
166 if ($CFG->version < 2013092001.02) {
167 // We did not upgrade the database yet.
168 $modules = $DB->get_records('modules', array(), 'name ASC', 'id, name, version');
169 foreach ($modules as $module) {
170 $this->installedplugins['mod'][$module->name] = $module->version;
172 $blocks = $DB->get_records('block', array(), 'name ASC', 'id, name, version');
173 foreach ($blocks as $block) {
174 $this->installedplugins['block'][$block->name] = $block->version;
178 $versions = $DB->get_records('config_plugins', array('name'=>'version'));
179 foreach ($versions as $version) {
180 $parts = explode('_', $version->plugin, 2);
181 if (!isset($parts[1])) {
182 // Invalid component, there must be at least one "_".
183 continue;
185 // Do not verify here if plugin type and name are valid.
186 $this->installedplugins[$parts[0]][$parts[1]] = $version->value;
189 foreach ($this->installedplugins as $key => $value) {
190 ksort($this->installedplugins[$key]);
193 $cache->set('installed', $this->installedplugins);
197 * Return list of installed plugins of given type.
198 * @param string $type
199 * @return array $name=>$version
201 public function get_installed_plugins($type) {
202 $this->load_installed_plugins();
203 if (isset($this->installedplugins[$type])) {
204 return $this->installedplugins[$type];
206 return array();
210 * Load list of all enabled plugins,
211 * call before using $this->enabledplugins.
213 * This method is caching results from individual plugin info classes.
215 protected function load_enabled_plugins() {
216 global $CFG;
218 if ($this->enabledplugins) {
219 return;
222 if (empty($CFG->version)) {
223 $this->enabledplugins = array();
224 return;
227 $cache = cache::make('core', 'plugin_manager');
228 $enabled = $cache->get('enabled');
230 if (is_array($enabled)) {
231 $this->enabledplugins = $enabled;
232 return;
235 $this->enabledplugins = array();
237 require_once($CFG->libdir.'/adminlib.php');
239 $plugintypes = core_component::get_plugin_types();
240 foreach ($plugintypes as $plugintype => $fulldir) {
241 $plugininfoclass = self::resolve_plugininfo_class($plugintype);
242 if (class_exists($plugininfoclass)) {
243 $enabled = $plugininfoclass::get_enabled_plugins();
244 if (!is_array($enabled)) {
245 continue;
247 $this->enabledplugins[$plugintype] = $enabled;
251 $cache->set('enabled', $this->enabledplugins);
255 * Get list of enabled plugins of given type,
256 * the result may contain missing plugins.
258 * @param string $type
259 * @return array|null list of enabled plugins of this type, null if unknown
261 public function get_enabled_plugins($type) {
262 $this->load_enabled_plugins();
263 if (isset($this->enabledplugins[$type])) {
264 return $this->enabledplugins[$type];
266 return null;
270 * Load list of all present plugins - call before using $this->presentplugins.
272 protected function load_present_plugins() {
273 if ($this->presentplugins) {
274 return;
277 $cache = cache::make('core', 'plugin_manager');
278 $present = $cache->get('present');
280 if (is_array($present)) {
281 $this->presentplugins = $present;
282 return;
285 $this->presentplugins = array();
287 $plugintypes = core_component::get_plugin_types();
288 foreach ($plugintypes as $type => $typedir) {
289 $plugs = core_component::get_plugin_list($type);
290 foreach ($plugs as $plug => $fullplug) {
291 $plugin = new stdClass();
292 $plugin->version = null;
293 $module = $plugin;
294 @include($fullplug.'/version.php');
295 $this->presentplugins[$type][$plug] = $plugin;
299 $cache->set('present', $this->presentplugins);
303 * Get list of present plugins of given type.
305 * @param string $type
306 * @return array|null list of presnet plugins $name=>$diskversion, null if unknown
308 public function get_present_plugins($type) {
309 $this->load_present_plugins();
310 if (isset($this->presentplugins[$type])) {
311 return $this->presentplugins[$type];
313 return null;
317 * Returns a tree of known plugins and information about them
319 * @return array 2D array. The first keys are plugin type names (e.g. qtype);
320 * the second keys are the plugin local name (e.g. multichoice); and
321 * the values are the corresponding objects extending {@link \core\plugininfo\base}
323 public function get_plugins() {
324 $this->init_pluginsinfo_property();
326 // Make sure all types are initialised.
327 foreach ($this->pluginsinfo as $plugintype => $list) {
328 if ($list === null) {
329 $this->get_plugins_of_type($plugintype);
333 return $this->pluginsinfo;
337 * Returns list of known plugins of the given type.
339 * This method returns the subset of the tree returned by {@link self::get_plugins()}.
340 * If the given type is not known, empty array is returned.
342 * @param string $type plugin type, e.g. 'mod' or 'workshopallocation'
343 * @return \core\plugininfo\base[] (string)plugin name (e.g. 'workshop') => corresponding subclass of {@link \core\plugininfo\base}
345 public function get_plugins_of_type($type) {
346 global $CFG;
348 $this->init_pluginsinfo_property();
350 if (!array_key_exists($type, $this->pluginsinfo)) {
351 return array();
354 if (is_array($this->pluginsinfo[$type])) {
355 return $this->pluginsinfo[$type];
358 $types = core_component::get_plugin_types();
360 if (!isset($types[$type])) {
361 // Orphaned subplugins!
362 $plugintypeclass = self::resolve_plugininfo_class($type);
363 $this->pluginsinfo[$type] = $plugintypeclass::get_plugins($type, null, $plugintypeclass);
364 return $this->pluginsinfo[$type];
367 /** @var \core\plugininfo\base $plugintypeclass */
368 $plugintypeclass = self::resolve_plugininfo_class($type);
369 $plugins = $plugintypeclass::get_plugins($type, $types[$type], $plugintypeclass);
370 $this->pluginsinfo[$type] = $plugins;
372 if (empty($CFG->disableupdatenotifications) and !during_initial_install()) {
373 // Append the information about available updates provided by {@link \core\update\checker()}.
374 $provider = \core\update\checker::instance();
375 foreach ($plugins as $plugininfoholder) {
376 $plugininfoholder->check_available_updates($provider);
380 return $this->pluginsinfo[$type];
384 * Init placeholder array for plugin infos.
386 protected function init_pluginsinfo_property() {
387 if (is_array($this->pluginsinfo)) {
388 return;
390 $this->pluginsinfo = array();
392 $plugintypes = $this->get_plugin_types();
394 foreach ($plugintypes as $plugintype => $plugintyperootdir) {
395 $this->pluginsinfo[$plugintype] = null;
398 // Add orphaned subplugin types.
399 $this->load_installed_plugins();
400 foreach ($this->installedplugins as $plugintype => $unused) {
401 if (!isset($plugintypes[$plugintype])) {
402 $this->pluginsinfo[$plugintype] = null;
408 * Find the plugin info class for given type.
410 * @param string $type
411 * @return string name of pluginfo class for give plugin type
413 public static function resolve_plugininfo_class($type) {
414 $plugintypes = core_component::get_plugin_types();
415 if (!isset($plugintypes[$type])) {
416 return '\core\plugininfo\orphaned';
419 $parent = core_component::get_subtype_parent($type);
421 if ($parent) {
422 $class = '\\'.$parent.'\plugininfo\\' . $type;
423 if (class_exists($class)) {
424 $plugintypeclass = $class;
425 } else {
426 if ($dir = core_component::get_component_directory($parent)) {
427 // BC only - use namespace instead!
428 if (file_exists("$dir/adminlib.php")) {
429 global $CFG;
430 include_once("$dir/adminlib.php");
432 if (class_exists('plugininfo_' . $type)) {
433 $plugintypeclass = 'plugininfo_' . $type;
434 debugging('Class "'.$plugintypeclass.'" is deprecated, migrate to "'.$class.'"', DEBUG_DEVELOPER);
435 } else {
436 debugging('Subplugin type "'.$type.'" should define class "'.$class.'"', DEBUG_DEVELOPER);
437 $plugintypeclass = '\core\plugininfo\general';
439 } else {
440 $plugintypeclass = '\core\plugininfo\general';
443 } else {
444 $class = '\core\plugininfo\\' . $type;
445 if (class_exists($class)) {
446 $plugintypeclass = $class;
447 } else {
448 debugging('All standard types including "'.$type.'" should have plugininfo class!', DEBUG_DEVELOPER);
449 $plugintypeclass = '\core\plugininfo\general';
453 if (!in_array('core\plugininfo\base', class_parents($plugintypeclass))) {
454 throw new coding_exception('Class ' . $plugintypeclass . ' must extend \core\plugininfo\base');
457 return $plugintypeclass;
461 * Returns list of all known subplugins of the given plugin.
463 * For plugins that do not provide subplugins (i.e. there is no support for it),
464 * empty array is returned.
466 * @param string $component full component name, e.g. 'mod_workshop'
467 * @return array (string) component name (e.g. 'workshopallocation_random') => subclass of {@link \core\plugininfo\base}
469 public function get_subplugins_of_plugin($component) {
471 $pluginfo = $this->get_plugin_info($component);
473 if (is_null($pluginfo)) {
474 return array();
477 $subplugins = $this->get_subplugins();
479 if (!isset($subplugins[$pluginfo->component])) {
480 return array();
483 $list = array();
485 foreach ($subplugins[$pluginfo->component] as $subdata) {
486 foreach ($this->get_plugins_of_type($subdata->type) as $subpluginfo) {
487 $list[$subpluginfo->component] = $subpluginfo;
491 return $list;
495 * Returns list of plugins that define their subplugins and the information
496 * about them from the db/subplugins.php file.
498 * @return array with keys like 'mod_quiz', and values the data from the
499 * corresponding db/subplugins.php file.
501 public function get_subplugins() {
503 if (is_array($this->subpluginsinfo)) {
504 return $this->subpluginsinfo;
507 $plugintypes = core_component::get_plugin_types();
509 $this->subpluginsinfo = array();
510 foreach (core_component::get_plugin_types_with_subplugins() as $type => $ignored) {
511 foreach (core_component::get_plugin_list($type) as $plugin => $componentdir) {
512 $component = $type.'_'.$plugin;
513 $subplugins = core_component::get_subplugins($component);
514 if (!$subplugins) {
515 continue;
517 $this->subpluginsinfo[$component] = array();
518 foreach ($subplugins as $subplugintype => $ignored) {
519 $subplugin = new stdClass();
520 $subplugin->type = $subplugintype;
521 $subplugin->typerootdir = $plugintypes[$subplugintype];
522 $this->subpluginsinfo[$component][$subplugintype] = $subplugin;
526 return $this->subpluginsinfo;
530 * Returns the name of the plugin that defines the given subplugin type
532 * If the given subplugin type is not actually a subplugin, returns false.
534 * @param string $subplugintype the name of subplugin type, eg. workshopform or quiz
535 * @return false|string the name of the parent plugin, eg. mod_workshop
537 public function get_parent_of_subplugin($subplugintype) {
538 $parent = core_component::get_subtype_parent($subplugintype);
539 if (!$parent) {
540 return false;
542 return $parent;
546 * Returns a localized name of a given plugin
548 * @param string $component name of the plugin, eg mod_workshop or auth_ldap
549 * @return string
551 public function plugin_name($component) {
553 $pluginfo = $this->get_plugin_info($component);
555 if (is_null($pluginfo)) {
556 throw new moodle_exception('err_unknown_plugin', 'core_plugin', '', array('plugin' => $component));
559 return $pluginfo->displayname;
563 * Returns a localized name of a plugin typed in singular form
565 * Most plugin types define their names in core_plugin lang file. In case of subplugins,
566 * we try to ask the parent plugin for the name. In the worst case, we will return
567 * the value of the passed $type parameter.
569 * @param string $type the type of the plugin, e.g. mod or workshopform
570 * @return string
572 public function plugintype_name($type) {
574 if (get_string_manager()->string_exists('type_' . $type, 'core_plugin')) {
575 // For most plugin types, their names are defined in core_plugin lang file.
576 return get_string('type_' . $type, 'core_plugin');
578 } else if ($parent = $this->get_parent_of_subplugin($type)) {
579 // If this is a subplugin, try to ask the parent plugin for the name.
580 if (get_string_manager()->string_exists('subplugintype_' . $type, $parent)) {
581 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type, $parent);
582 } else {
583 return $this->plugin_name($parent) . ' / ' . $type;
586 } else {
587 return $type;
592 * Returns a localized name of a plugin type in plural form
594 * Most plugin types define their names in core_plugin lang file. In case of subplugins,
595 * we try to ask the parent plugin for the name. In the worst case, we will return
596 * the value of the passed $type parameter.
598 * @param string $type the type of the plugin, e.g. mod or workshopform
599 * @return string
601 public function plugintype_name_plural($type) {
603 if (get_string_manager()->string_exists('type_' . $type . '_plural', 'core_plugin')) {
604 // For most plugin types, their names are defined in core_plugin lang file.
605 return get_string('type_' . $type . '_plural', 'core_plugin');
607 } else if ($parent = $this->get_parent_of_subplugin($type)) {
608 // If this is a subplugin, try to ask the parent plugin for the name.
609 if (get_string_manager()->string_exists('subplugintype_' . $type . '_plural', $parent)) {
610 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type . '_plural', $parent);
611 } else {
612 return $this->plugin_name($parent) . ' / ' . $type;
615 } else {
616 return $type;
621 * Returns information about the known plugin, or null
623 * @param string $component frankenstyle component name.
624 * @return \core\plugininfo\base|null the corresponding plugin information.
626 public function get_plugin_info($component) {
627 list($type, $name) = core_component::normalize_component($component);
628 $plugins = $this->get_plugins_of_type($type);
629 if (isset($plugins[$name])) {
630 return $plugins[$name];
631 } else {
632 return null;
637 * Check to see if the current version of the plugin seems to be a checkout of an external repository.
639 * @see \core\update\deployer::plugin_external_source()
640 * @param string $component frankenstyle component name
641 * @return false|string
643 public function plugin_external_source($component) {
645 $plugininfo = $this->get_plugin_info($component);
647 if (is_null($plugininfo)) {
648 return false;
651 $pluginroot = $plugininfo->rootdir;
653 if (is_dir($pluginroot.'/.git')) {
654 return 'git';
657 if (is_dir($pluginroot.'/CVS')) {
658 return 'cvs';
661 if (is_dir($pluginroot.'/.svn')) {
662 return 'svn';
665 if (is_dir($pluginroot.'/.hg')) {
666 return 'mercurial';
669 return false;
673 * Get a list of any other plugins that require this one.
674 * @param string $component frankenstyle component name.
675 * @return array of frankensyle component names that require this one.
677 public function other_plugins_that_require($component) {
678 $others = array();
679 foreach ($this->get_plugins() as $type => $plugins) {
680 foreach ($plugins as $plugin) {
681 $required = $plugin->get_other_required_plugins();
682 if (isset($required[$component])) {
683 $others[] = $plugin->component;
687 return $others;
691 * Check a dependencies list against the list of installed plugins.
692 * @param array $dependencies compenent name to required version or ANY_VERSION.
693 * @return bool true if all the dependencies are satisfied.
695 public function are_dependencies_satisfied($dependencies) {
696 foreach ($dependencies as $component => $requiredversion) {
697 $otherplugin = $this->get_plugin_info($component);
698 if (is_null($otherplugin)) {
699 return false;
702 if ($requiredversion != ANY_VERSION and $otherplugin->versiondisk < $requiredversion) {
703 return false;
707 return true;
711 * Checks all dependencies for all installed plugins
713 * This is used by install and upgrade. The array passed by reference as the second
714 * argument is populated with the list of plugins that have failed dependencies (note that
715 * a single plugin can appear multiple times in the $failedplugins).
717 * @param int $moodleversion the version from version.php.
718 * @param array $failedplugins to return the list of plugins with non-satisfied dependencies
719 * @return bool true if all the dependencies are satisfied for all plugins.
721 public function all_plugins_ok($moodleversion, &$failedplugins = array()) {
723 $return = true;
724 foreach ($this->get_plugins() as $type => $plugins) {
725 foreach ($plugins as $plugin) {
727 if (!$plugin->is_core_dependency_satisfied($moodleversion)) {
728 $return = false;
729 $failedplugins[] = $plugin->component;
732 if (!$this->are_dependencies_satisfied($plugin->get_other_required_plugins())) {
733 $return = false;
734 $failedplugins[] = $plugin->component;
739 return $return;
743 * Is it possible to uninstall the given plugin?
745 * False is returned if the plugininfo subclass declares the uninstall should
746 * not be allowed via {@link \core\plugininfo\base::is_uninstall_allowed()} or if the
747 * core vetoes it (e.g. becase the plugin or some of its subplugins is required
748 * by some other installed plugin).
750 * @param string $component full frankenstyle name, e.g. mod_foobar
751 * @return bool
753 public function can_uninstall_plugin($component) {
755 $pluginfo = $this->get_plugin_info($component);
757 if (is_null($pluginfo)) {
758 return false;
761 if (!$this->common_uninstall_check($pluginfo)) {
762 return false;
765 // Verify only if something else requires the subplugins, do not verify their common_uninstall_check()!
766 $subplugins = $this->get_subplugins_of_plugin($pluginfo->component);
767 foreach ($subplugins as $subpluginfo) {
768 // Check if there are some other plugins requiring this subplugin
769 // (but the parent and siblings).
770 foreach ($this->other_plugins_that_require($subpluginfo->component) as $requiresme) {
771 $ismyparent = ($pluginfo->component === $requiresme);
772 $ismysibling = in_array($requiresme, array_keys($subplugins));
773 if (!$ismyparent and !$ismysibling) {
774 return false;
779 // Check if there are some other plugins requiring this plugin
780 // (but its subplugins).
781 foreach ($this->other_plugins_that_require($pluginfo->component) as $requiresme) {
782 $ismysubplugin = in_array($requiresme, array_keys($subplugins));
783 if (!$ismysubplugin) {
784 return false;
788 return true;
792 * Returns uninstall URL if exists.
794 * @param string $component
795 * @param string $return either 'overview' or 'manage'
796 * @return moodle_url uninstall URL, null if uninstall not supported
798 public function get_uninstall_url($component, $return = 'overview') {
799 if (!$this->can_uninstall_plugin($component)) {
800 return null;
803 $pluginfo = $this->get_plugin_info($component);
805 if (is_null($pluginfo)) {
806 return null;
809 if (method_exists($pluginfo, 'get_uninstall_url')) {
810 debugging('plugininfo method get_uninstall_url() is deprecated, all plugins should be uninstalled via standard URL only.');
811 return $pluginfo->get_uninstall_url($return);
814 return $pluginfo->get_default_uninstall_url($return);
818 * Uninstall the given plugin.
820 * Automatically cleans-up all remaining configuration data, log records, events,
821 * files from the file pool etc.
823 * In the future, the functionality of {@link uninstall_plugin()} function may be moved
824 * into this method and all the code should be refactored to use it. At the moment, we
825 * mimic this future behaviour by wrapping that function call.
827 * @param string $component
828 * @param progress_trace $progress traces the process
829 * @return bool true on success, false on errors/problems
831 public function uninstall_plugin($component, progress_trace $progress) {
833 $pluginfo = $this->get_plugin_info($component);
835 if (is_null($pluginfo)) {
836 return false;
839 // Give the pluginfo class a chance to execute some steps.
840 $result = $pluginfo->uninstall($progress);
841 if (!$result) {
842 return false;
845 // Call the legacy core function to uninstall the plugin.
846 ob_start();
847 uninstall_plugin($pluginfo->type, $pluginfo->name);
848 $progress->output(ob_get_clean());
850 return true;
854 * Checks if there are some plugins with a known available update
856 * @return bool true if there is at least one available update
858 public function some_plugins_updatable() {
859 foreach ($this->get_plugins() as $type => $plugins) {
860 foreach ($plugins as $plugin) {
861 if ($plugin->available_updates()) {
862 return true;
867 return false;
871 * Check to see if the given plugin folder can be removed by the web server process.
873 * @param string $component full frankenstyle component
874 * @return bool
876 public function is_plugin_folder_removable($component) {
878 $pluginfo = $this->get_plugin_info($component);
880 if (is_null($pluginfo)) {
881 return false;
884 // To be able to remove the plugin folder, its parent must be writable, too.
885 if (!is_writable(dirname($pluginfo->rootdir))) {
886 return false;
889 // Check that the folder and all its content is writable (thence removable).
890 return $this->is_directory_removable($pluginfo->rootdir);
894 * Defines a list of all plugins that were originally shipped in the standard Moodle distribution,
895 * but are not anymore and are deleted during upgrades.
897 * The main purpose of this list is to hide missing plugins during upgrade.
899 * @param string $type plugin type
900 * @param string $name plugin name
901 * @return bool
903 public static function is_deleted_standard_plugin($type, $name) {
904 // Do not include plugins that were removed during upgrades to versions that are
905 // not supported as source versions for upgrade any more. For example, at MOODLE_23_STABLE
906 // branch, listed should be no plugins that were removed at 1.9.x - 2.1.x versions as
907 // Moodle 2.3 supports upgrades from 2.2.x only.
908 $plugins = array(
909 'qformat' => array('blackboard'),
910 'enrol' => array('authorize'),
911 'tool' => array('bloglevelupgrade', 'qeupgradehelper'),
912 'theme' => array('mymobile', 'afterburner', 'anomaly', 'arialist', 'binarius', 'boxxie', 'brick', 'formal_white',
913 'formfactor', 'fusion', 'leatherbound', 'magazine', 'nimble', 'nonzero', 'overlay', 'serenity', 'sky_high',
914 'splash', 'standard', 'standardold'),
917 if (!isset($plugins[$type])) {
918 return false;
920 return in_array($name, $plugins[$type]);
924 * Defines a white list of all plugins shipped in the standard Moodle distribution
926 * @param string $type
927 * @return false|array array of standard plugins or false if the type is unknown
929 public static function standard_plugins_list($type) {
931 $standard_plugins = array(
933 'atto' => array(
934 'accessibilitychecker', 'accessibilityhelper', 'align',
935 'backcolor', 'bold', 'charmap', 'clear', 'collapse', 'emoticon',
936 'equation', 'fontcolor', 'html', 'image', 'indent', 'italic',
937 'link', 'managefiles', 'media', 'noautolink', 'orderedlist',
938 'rtl', 'strike', 'subscript', 'superscript', 'table', 'title',
939 'underline', 'undo', 'unorderedlist'
942 'assignment' => array(
943 'offline', 'online', 'upload', 'uploadsingle'
946 'assignsubmission' => array(
947 'comments', 'file', 'onlinetext'
950 'assignfeedback' => array(
951 'comments', 'file', 'offline', 'editpdf'
954 'auth' => array(
955 'cas', 'db', 'email', 'fc', 'imap', 'ldap', 'manual', 'mnet',
956 'nntp', 'nologin', 'none', 'pam', 'pop3', 'radius',
957 'shibboleth', 'webservice'
960 'availability' => array(
961 'completion', 'date', 'grade', 'group', 'grouping', 'profile'
964 'block' => array(
965 'activity_modules', 'admin_bookmarks', 'badges', 'blog_menu',
966 'blog_recent', 'blog_tags', 'calendar_month',
967 'calendar_upcoming', 'comments', 'community',
968 'completionstatus', 'course_list', 'course_overview',
969 'course_summary', 'feedback', 'glossary_random', 'html',
970 'login', 'mentees', 'messages', 'mnet_hosts', 'myprofile',
971 'navigation', 'news_items', 'online_users', 'participants',
972 'private_files', 'quiz_results', 'recent_activity',
973 'rss_client', 'search_forums', 'section_links',
974 'selfcompletion', 'settings', 'site_main_menu',
975 'social_activities', 'tag_flickr', 'tag_youtube', 'tags'
978 'booktool' => array(
979 'exportimscp', 'importhtml', 'print'
982 'cachelock' => array(
983 'file'
986 'cachestore' => array(
987 'file', 'memcache', 'memcached', 'mongodb', 'session', 'static'
990 'calendartype' => array(
991 'gregorian'
994 'coursereport' => array(
995 // Deprecated!
998 'datafield' => array(
999 'checkbox', 'date', 'file', 'latlong', 'menu', 'multimenu',
1000 'number', 'picture', 'radiobutton', 'text', 'textarea', 'url'
1003 'datapreset' => array(
1004 'imagegallery'
1007 'editor' => array(
1008 'atto', 'textarea', 'tinymce'
1011 'enrol' => array(
1012 'category', 'cohort', 'database', 'flatfile',
1013 'guest', 'imsenterprise', 'ldap', 'manual', 'meta', 'mnet',
1014 'paypal', 'self'
1017 'filter' => array(
1018 'activitynames', 'algebra', 'censor', 'emailprotect',
1019 'emoticon', 'mathjaxloader', 'mediaplugin', 'multilang', 'tex', 'tidy',
1020 'urltolink', 'data', 'glossary'
1023 'format' => array(
1024 'singleactivity', 'social', 'topics', 'weeks'
1027 'gradeexport' => array(
1028 'ods', 'txt', 'xls', 'xml'
1031 'gradeimport' => array(
1032 'csv', 'xml'
1035 'gradereport' => array(
1036 'grader', 'outcomes', 'overview', 'user'
1039 'gradingform' => array(
1040 'rubric', 'guide'
1043 'local' => array(
1046 'logstore' => array(
1047 'database', 'legacy', 'standard',
1050 'message' => array(
1051 'airnotifier', 'email', 'jabber', 'popup'
1054 'mnetservice' => array(
1055 'enrol'
1058 'mod' => array(
1059 'assign', 'assignment', 'book', 'chat', 'choice', 'data', 'feedback', 'folder',
1060 'forum', 'glossary', 'imscp', 'label', 'lesson', 'lti', 'page',
1061 'quiz', 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop'
1064 'plagiarism' => array(
1067 'portfolio' => array(
1068 'boxnet', 'download', 'flickr', 'googledocs', 'mahara', 'picasa'
1071 'profilefield' => array(
1072 'checkbox', 'datetime', 'menu', 'text', 'textarea'
1075 'qbehaviour' => array(
1076 'adaptive', 'adaptivenopenalty', 'deferredcbm',
1077 'deferredfeedback', 'immediatecbm', 'immediatefeedback',
1078 'informationitem', 'interactive', 'interactivecountback',
1079 'manualgraded', 'missing'
1082 'qformat' => array(
1083 'aiken', 'blackboard_six', 'examview', 'gift',
1084 'learnwise', 'missingword', 'multianswer', 'webct',
1085 'xhtml', 'xml'
1088 'qtype' => array(
1089 'calculated', 'calculatedmulti', 'calculatedsimple',
1090 'description', 'essay', 'match', 'missingtype', 'multianswer',
1091 'multichoice', 'numerical', 'random', 'randomsamatch',
1092 'shortanswer', 'truefalse'
1095 'quiz' => array(
1096 'grading', 'overview', 'responses', 'statistics'
1099 'quizaccess' => array(
1100 'delaybetweenattempts', 'ipaddress', 'numattempts', 'openclosedate',
1101 'password', 'safebrowser', 'securewindow', 'timelimit'
1104 'report' => array(
1105 'backups', 'completion', 'configlog', 'courseoverview', 'eventlist',
1106 'log', 'loglive', 'outline', 'participation', 'progress', 'questioninstances', 'security', 'stats', 'performance'
1109 'repository' => array(
1110 'alfresco', 'areafiles', 'boxnet', 'coursefiles', 'dropbox', 'equella', 'filesystem',
1111 'flickr', 'flickr_public', 'googledocs', 'local', 'merlot',
1112 'picasa', 'recent', 'skydrive', 's3', 'upload', 'url', 'user', 'webdav',
1113 'wikimedia', 'youtube'
1116 'scormreport' => array(
1117 'basic',
1118 'interactions',
1119 'graphs',
1120 'objectives'
1123 'tinymce' => array(
1124 'ctrlhelp', 'dragmath', 'managefiles', 'moodleemoticon', 'moodleimage',
1125 'moodlemedia', 'moodlenolink', 'pdw', 'spellchecker', 'wrap'
1128 'theme' => array(
1129 'base', 'bootstrapbase', 'canvas', 'clean', 'more'
1132 'tool' => array(
1133 'assignmentupgrade', 'availabilityconditions', 'behat', 'capability', 'customlang',
1134 'dbtransfer', 'generator', 'health', 'innodb', 'installaddon',
1135 'langimport', 'log', 'multilangupgrade', 'phpunit', 'profiling',
1136 'replace', 'spamcleaner', 'task', 'timezoneimport',
1137 'unittest', 'uploadcourse', 'uploaduser', 'unsuproles', 'xmldb'
1140 'webservice' => array(
1141 'amf', 'rest', 'soap', 'xmlrpc'
1144 'workshopallocation' => array(
1145 'manual', 'random', 'scheduled'
1148 'workshopeval' => array(
1149 'best'
1152 'workshopform' => array(
1153 'accumulative', 'comments', 'numerrors', 'rubric'
1157 if (isset($standard_plugins[$type])) {
1158 return $standard_plugins[$type];
1159 } else {
1160 return false;
1165 * Reorders plugin types into a sequence to be displayed
1167 * For technical reasons, plugin types returned by {@link core_component::get_plugin_types()} are
1168 * in a certain order that does not need to fit the expected order for the display.
1169 * Particularly, activity modules should be displayed first as they represent the
1170 * real heart of Moodle. They should be followed by other plugin types that are
1171 * used to build the courses (as that is what one expects from LMS). After that,
1172 * other supportive plugin types follow.
1174 * @param array $types associative array
1175 * @return array same array with altered order of items
1177 protected function reorder_plugin_types(array $types) {
1178 $fix = array('mod' => $types['mod']);
1179 foreach (core_component::get_plugin_list('mod') as $plugin => $fulldir) {
1180 if (!$subtypes = core_component::get_subplugins('mod_'.$plugin)) {
1181 continue;
1183 foreach ($subtypes as $subtype => $ignored) {
1184 $fix[$subtype] = $types[$subtype];
1188 $fix['mod'] = $types['mod'];
1189 $fix['block'] = $types['block'];
1190 $fix['qtype'] = $types['qtype'];
1191 $fix['qbehaviour'] = $types['qbehaviour'];
1192 $fix['qformat'] = $types['qformat'];
1193 $fix['filter'] = $types['filter'];
1195 $fix['editor'] = $types['editor'];
1196 foreach (core_component::get_plugin_list('editor') as $plugin => $fulldir) {
1197 if (!$subtypes = core_component::get_subplugins('editor_'.$plugin)) {
1198 continue;
1200 foreach ($subtypes as $subtype => $ignored) {
1201 $fix[$subtype] = $types[$subtype];
1205 $fix['enrol'] = $types['enrol'];
1206 $fix['auth'] = $types['auth'];
1207 $fix['tool'] = $types['tool'];
1208 foreach (core_component::get_plugin_list('tool') as $plugin => $fulldir) {
1209 if (!$subtypes = core_component::get_subplugins('tool_'.$plugin)) {
1210 continue;
1212 foreach ($subtypes as $subtype => $ignored) {
1213 $fix[$subtype] = $types[$subtype];
1217 foreach ($types as $type => $path) {
1218 if (!isset($fix[$type])) {
1219 $fix[$type] = $path;
1222 return $fix;
1226 * Check if the given directory can be removed by the web server process.
1228 * This recursively checks that the given directory and all its contents
1229 * it writable.
1231 * @param string $fullpath
1232 * @return boolean
1234 protected function is_directory_removable($fullpath) {
1236 if (!is_writable($fullpath)) {
1237 return false;
1240 if (is_dir($fullpath)) {
1241 $handle = opendir($fullpath);
1242 } else {
1243 return false;
1246 $result = true;
1248 while ($filename = readdir($handle)) {
1250 if ($filename === '.' or $filename === '..') {
1251 continue;
1254 $subfilepath = $fullpath.'/'.$filename;
1256 if (is_dir($subfilepath)) {
1257 $result = $result && $this->is_directory_removable($subfilepath);
1259 } else {
1260 $result = $result && is_writable($subfilepath);
1264 closedir($handle);
1266 return $result;
1270 * Helper method that implements common uninstall prerequisites
1272 * @param \core\plugininfo\base $pluginfo
1273 * @return bool
1275 protected function common_uninstall_check(\core\plugininfo\base $pluginfo) {
1277 if (!$pluginfo->is_uninstall_allowed()) {
1278 // The plugin's plugininfo class declares it should not be uninstalled.
1279 return false;
1282 if ($pluginfo->get_status() === self::PLUGIN_STATUS_NEW) {
1283 // The plugin is not installed. It should be either installed or removed from the disk.
1284 // Relying on this temporary state may be tricky.
1285 return false;
1288 if (method_exists($pluginfo, 'get_uninstall_url') and is_null($pluginfo->get_uninstall_url())) {
1289 // Backwards compatibility.
1290 debugging('\core\plugininfo\base subclasses should use is_uninstall_allowed() instead of returning null in get_uninstall_url()',
1291 DEBUG_DEVELOPER);
1292 return false;
1295 return true;