2 // This file is part of Moodle - http://moodle.org/
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.
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/>.
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.
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();
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 /** the given requirement/dependency is fulfilled */
57 const REQUIREMENT_STATUS_OK = 'ok';
58 /** the plugin requires higher core/other plugin version than is currently installed */
59 const REQUIREMENT_STATUS_OUTDATED = 'outdated';
60 /** the required dependency is not installed */
61 const REQUIREMENT_STATUS_MISSING = 'missing';
63 /** the required dependency is available in the plugins directory */
64 const REQUIREMENT_AVAILABLE = 'available';
65 /** the required dependency is available in the plugins directory */
66 const REQUIREMENT_UNAVAILABLE = 'unavailable';
68 /** @var core_plugin_manager holds the singleton instance */
69 protected static $singletoninstance;
70 /** @var array of raw plugins information */
71 protected $pluginsinfo = null;
72 /** @var array of raw subplugins information */
73 protected $subpluginsinfo = null;
74 /** @var array cache information about availability in the plugins directory if requesting "at least" version */
75 protected $remotepluginsinfoatleast = null;
76 /** @var array cache information about availability in the plugins directory if requesting exact version */
77 protected $remotepluginsinfoexact = null;
78 /** @var array list of installed plugins $name=>$version */
79 protected $installedplugins = null;
80 /** @var array list of all enabled plugins $name=>$name */
81 protected $enabledplugins = null;
82 /** @var array list of all enabled plugins $name=>$diskversion */
83 protected $presentplugins = null;
84 /** @var array reordered list of plugin types */
85 protected $plugintypes = null;
86 /** @var \core\update\code_manager code manager to use for plugins code operations */
87 protected $codemanager = null;
88 /** @var \core\update\api client instance to use for accessing download.moodle.org/api/ */
89 protected $updateapiclient = null;
92 * Direct initiation not allowed, use the factory method {@link self::instance()}
94 protected function __construct() {
98 * Sorry, this is singleton
100 protected function __clone() {
104 * Factory method for this class
106 * @return core_plugin_manager the singleton instance
108 public static function instance() {
109 if (is_null(static::$singletoninstance)) {
110 static::$singletoninstance = new static();
112 return static::$singletoninstance;
117 * @param bool $phpunitreset
119 public static function reset_caches($phpunitreset = false) {
121 static::$singletoninstance = null;
123 if (static::$singletoninstance) {
124 static::$singletoninstance->pluginsinfo = null;
125 static::$singletoninstance->subpluginsinfo = null;
126 static::$singletoninstance->remotepluginsinfoatleast = null;
127 static::$singletoninstance->remotepluginsinfoexact = null;
128 static::$singletoninstance->installedplugins = null;
129 static::$singletoninstance->enabledplugins = null;
130 static::$singletoninstance->presentplugins = null;
131 static::$singletoninstance->plugintypes = null;
132 static::$singletoninstance->codemanager = null;
133 static::$singletoninstance->updateapiclient = null;
136 $cache = cache::make('core', 'plugin_manager');
141 * Returns the result of {@link core_component::get_plugin_types()} ordered for humans
143 * @see self::reorder_plugin_types()
144 * @return array (string)name => (string)location
146 public function get_plugin_types() {
147 if (func_num_args() > 0) {
148 if (!func_get_arg(0)) {
149 throw coding_exception('core_plugin_manager->get_plugin_types() does not support relative paths.');
152 if ($this->plugintypes) {
153 return $this->plugintypes;
156 $this->plugintypes = $this->reorder_plugin_types(core_component::get_plugin_types());
157 return $this->plugintypes;
161 * Load list of installed plugins,
162 * always call before using $this->installedplugins.
164 * This method is caching results for all plugins.
166 protected function load_installed_plugins() {
169 if ($this->installedplugins) {
173 if (empty($CFG->version)) {
174 // Nothing installed yet.
175 $this->installedplugins = array();
179 $cache = cache::make('core', 'plugin_manager');
180 $installed = $cache->get('installed');
182 if (is_array($installed)) {
183 $this->installedplugins = $installed;
187 $this->installedplugins = array();
189 // TODO: Delete this block once Moodle 2.6 or later becomes minimum required version to upgrade.
190 if ($CFG->version < 2013092001.02) {
191 // We did not upgrade the database yet.
192 $modules = $DB->get_records('modules', array(), 'name ASC', 'id, name, version');
193 foreach ($modules as $module) {
194 $this->installedplugins['mod'][$module->name] = $module->version;
196 $blocks = $DB->get_records('block', array(), 'name ASC', 'id, name, version');
197 foreach ($blocks as $block) {
198 $this->installedplugins['block'][$block->name] = $block->version;
202 $versions = $DB->get_records('config_plugins', array('name'=>'version'));
203 foreach ($versions as $version) {
204 $parts = explode('_', $version->plugin, 2);
205 if (!isset($parts[1])) {
206 // Invalid component, there must be at least one "_".
209 // Do not verify here if plugin type and name are valid.
210 $this->installedplugins[$parts[0]][$parts[1]] = $version->value;
213 foreach ($this->installedplugins as $key => $value) {
214 ksort($this->installedplugins[$key]);
217 $cache->set('installed', $this->installedplugins);
221 * Return list of installed plugins of given type.
222 * @param string $type
223 * @return array $name=>$version
225 public function get_installed_plugins($type) {
226 $this->load_installed_plugins();
227 if (isset($this->installedplugins[$type])) {
228 return $this->installedplugins[$type];
234 * Load list of all enabled plugins,
235 * call before using $this->enabledplugins.
237 * This method is caching results from individual plugin info classes.
239 protected function load_enabled_plugins() {
242 if ($this->enabledplugins) {
246 if (empty($CFG->version)) {
247 $this->enabledplugins = array();
251 $cache = cache::make('core', 'plugin_manager');
252 $enabled = $cache->get('enabled');
254 if (is_array($enabled)) {
255 $this->enabledplugins = $enabled;
259 $this->enabledplugins = array();
261 require_once($CFG->libdir.'/adminlib.php');
263 $plugintypes = core_component::get_plugin_types();
264 foreach ($plugintypes as $plugintype => $fulldir) {
265 $plugininfoclass = static::resolve_plugininfo_class($plugintype);
266 if (class_exists($plugininfoclass)) {
267 $enabled = $plugininfoclass::get_enabled_plugins();
268 if (!is_array($enabled)) {
271 $this->enabledplugins[$plugintype] = $enabled;
275 $cache->set('enabled', $this->enabledplugins);
279 * Get list of enabled plugins of given type,
280 * the result may contain missing plugins.
282 * @param string $type
283 * @return array|null list of enabled plugins of this type, null if unknown
285 public function get_enabled_plugins($type) {
286 $this->load_enabled_plugins();
287 if (isset($this->enabledplugins[$type])) {
288 return $this->enabledplugins[$type];
294 * Load list of all present plugins - call before using $this->presentplugins.
296 protected function load_present_plugins() {
297 if ($this->presentplugins) {
301 $cache = cache::make('core', 'plugin_manager');
302 $present = $cache->get('present');
304 if (is_array($present)) {
305 $this->presentplugins = $present;
309 $this->presentplugins = array();
311 $plugintypes = core_component::get_plugin_types();
312 foreach ($plugintypes as $type => $typedir) {
313 $plugs = core_component::get_plugin_list($type);
314 foreach ($plugs as $plug => $fullplug) {
315 $module = new stdClass();
316 $plugin = new stdClass();
317 $plugin->version = null;
318 include($fullplug.'/version.php');
320 // Check if the legacy $module syntax is still used.
321 if (!is_object($module) or (count((array)$module) > 0)) {
322 debugging('Unsupported $module syntax detected in version.php of the '.$type.'_'.$plug.' plugin.');
326 // Check if the component is properly declared.
327 if (empty($plugin->component) or ($plugin->component !== $type.'_'.$plug)) {
328 debugging('Plugin '.$type.'_'.$plug.' does not declare valid $plugin->component in its version.php.');
332 $this->presentplugins[$type][$plug] = $plugin;
336 if (empty($skipcache)) {
337 $cache->set('present', $this->presentplugins);
342 * Get list of present plugins of given type.
344 * @param string $type
345 * @return array|null list of presnet plugins $name=>$diskversion, null if unknown
347 public function get_present_plugins($type) {
348 $this->load_present_plugins();
349 if (isset($this->presentplugins[$type])) {
350 return $this->presentplugins[$type];
356 * Returns a tree of known plugins and information about them
358 * @return array 2D array. The first keys are plugin type names (e.g. qtype);
359 * the second keys are the plugin local name (e.g. multichoice); and
360 * the values are the corresponding objects extending {@link \core\plugininfo\base}
362 public function get_plugins() {
363 $this->init_pluginsinfo_property();
365 // Make sure all types are initialised.
366 foreach ($this->pluginsinfo as $plugintype => $list) {
367 if ($list === null) {
368 $this->get_plugins_of_type($plugintype);
372 return $this->pluginsinfo;
376 * Returns list of known plugins of the given type.
378 * This method returns the subset of the tree returned by {@link self::get_plugins()}.
379 * If the given type is not known, empty array is returned.
381 * @param string $type plugin type, e.g. 'mod' or 'workshopallocation'
382 * @return \core\plugininfo\base[] (string)plugin name (e.g. 'workshop') => corresponding subclass of {@link \core\plugininfo\base}
384 public function get_plugins_of_type($type) {
387 $this->init_pluginsinfo_property();
389 if (!array_key_exists($type, $this->pluginsinfo)) {
393 if (is_array($this->pluginsinfo[$type])) {
394 return $this->pluginsinfo[$type];
397 $types = core_component::get_plugin_types();
399 if (!isset($types[$type])) {
400 // Orphaned subplugins!
401 $plugintypeclass = static::resolve_plugininfo_class($type);
402 $this->pluginsinfo[$type] = $plugintypeclass::get_plugins($type, null, $plugintypeclass, $this);
403 return $this->pluginsinfo[$type];
406 /** @var \core\plugininfo\base $plugintypeclass */
407 $plugintypeclass = static::resolve_plugininfo_class($type);
408 $plugins = $plugintypeclass::get_plugins($type, $types[$type], $plugintypeclass, $this);
409 $this->pluginsinfo[$type] = $plugins;
411 return $this->pluginsinfo[$type];
415 * Init placeholder array for plugin infos.
417 protected function init_pluginsinfo_property() {
418 if (is_array($this->pluginsinfo)) {
421 $this->pluginsinfo = array();
423 $plugintypes = $this->get_plugin_types();
425 foreach ($plugintypes as $plugintype => $plugintyperootdir) {
426 $this->pluginsinfo[$plugintype] = null;
429 // Add orphaned subplugin types.
430 $this->load_installed_plugins();
431 foreach ($this->installedplugins as $plugintype => $unused) {
432 if (!isset($plugintypes[$plugintype])) {
433 $this->pluginsinfo[$plugintype] = null;
439 * Find the plugin info class for given type.
441 * @param string $type
442 * @return string name of pluginfo class for give plugin type
444 public static function resolve_plugininfo_class($type) {
445 $plugintypes = core_component::get_plugin_types();
446 if (!isset($plugintypes[$type])) {
447 return '\core\plugininfo\orphaned';
450 $parent = core_component::get_subtype_parent($type);
453 $class = '\\'.$parent.'\plugininfo\\' . $type;
454 if (class_exists($class)) {
455 $plugintypeclass = $class;
457 if ($dir = core_component::get_component_directory($parent)) {
458 // BC only - use namespace instead!
459 if (file_exists("$dir/adminlib.php")) {
461 include_once("$dir/adminlib.php");
463 if (class_exists('plugininfo_' . $type)) {
464 $plugintypeclass = 'plugininfo_' . $type;
465 debugging('Class "'.$plugintypeclass.'" is deprecated, migrate to "'.$class.'"', DEBUG_DEVELOPER);
467 debugging('Subplugin type "'.$type.'" should define class "'.$class.'"', DEBUG_DEVELOPER);
468 $plugintypeclass = '\core\plugininfo\general';
471 $plugintypeclass = '\core\plugininfo\general';
475 $class = '\core\plugininfo\\' . $type;
476 if (class_exists($class)) {
477 $plugintypeclass = $class;
479 debugging('All standard types including "'.$type.'" should have plugininfo class!', DEBUG_DEVELOPER);
480 $plugintypeclass = '\core\plugininfo\general';
484 if (!in_array('core\plugininfo\base', class_parents($plugintypeclass))) {
485 throw new coding_exception('Class ' . $plugintypeclass . ' must extend \core\plugininfo\base');
488 return $plugintypeclass;
492 * Returns list of all known subplugins of the given plugin.
494 * For plugins that do not provide subplugins (i.e. there is no support for it),
495 * empty array is returned.
497 * @param string $component full component name, e.g. 'mod_workshop'
498 * @return array (string) component name (e.g. 'workshopallocation_random') => subclass of {@link \core\plugininfo\base}
500 public function get_subplugins_of_plugin($component) {
502 $pluginfo = $this->get_plugin_info($component);
504 if (is_null($pluginfo)) {
508 $subplugins = $this->get_subplugins();
510 if (!isset($subplugins[$pluginfo->component])) {
516 foreach ($subplugins[$pluginfo->component] as $subdata) {
517 foreach ($this->get_plugins_of_type($subdata->type) as $subpluginfo) {
518 $list[$subpluginfo->component] = $subpluginfo;
526 * Returns list of plugins that define their subplugins and the information
527 * about them from the db/subplugins.php file.
529 * @return array with keys like 'mod_quiz', and values the data from the
530 * corresponding db/subplugins.php file.
532 public function get_subplugins() {
534 if (is_array($this->subpluginsinfo)) {
535 return $this->subpluginsinfo;
538 $plugintypes = core_component::get_plugin_types();
540 $this->subpluginsinfo = array();
541 foreach (core_component::get_plugin_types_with_subplugins() as $type => $ignored) {
542 foreach (core_component::get_plugin_list($type) as $plugin => $componentdir) {
543 $component = $type.'_'.$plugin;
544 $subplugins = core_component::get_subplugins($component);
548 $this->subpluginsinfo[$component] = array();
549 foreach ($subplugins as $subplugintype => $ignored) {
550 $subplugin = new stdClass();
551 $subplugin->type = $subplugintype;
552 $subplugin->typerootdir = $plugintypes[$subplugintype];
553 $this->subpluginsinfo[$component][$subplugintype] = $subplugin;
557 return $this->subpluginsinfo;
561 * Returns the name of the plugin that defines the given subplugin type
563 * If the given subplugin type is not actually a subplugin, returns false.
565 * @param string $subplugintype the name of subplugin type, eg. workshopform or quiz
566 * @return false|string the name of the parent plugin, eg. mod_workshop
568 public function get_parent_of_subplugin($subplugintype) {
569 $parent = core_component::get_subtype_parent($subplugintype);
577 * Returns a localized name of a given plugin
579 * @param string $component name of the plugin, eg mod_workshop or auth_ldap
582 public function plugin_name($component) {
584 $pluginfo = $this->get_plugin_info($component);
586 if (is_null($pluginfo)) {
587 throw new moodle_exception('err_unknown_plugin', 'core_plugin', '', array('plugin' => $component));
590 return $pluginfo->displayname;
594 * Returns a localized name of a plugin typed in singular form
596 * Most plugin types define their names in core_plugin lang file. In case of subplugins,
597 * we try to ask the parent plugin for the name. In the worst case, we will return
598 * the value of the passed $type parameter.
600 * @param string $type the type of the plugin, e.g. mod or workshopform
603 public function plugintype_name($type) {
605 if (get_string_manager()->string_exists('type_' . $type, 'core_plugin')) {
606 // For most plugin types, their names are defined in core_plugin lang file.
607 return get_string('type_' . $type, 'core_plugin');
609 } else if ($parent = $this->get_parent_of_subplugin($type)) {
610 // If this is a subplugin, try to ask the parent plugin for the name.
611 if (get_string_manager()->string_exists('subplugintype_' . $type, $parent)) {
612 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type, $parent);
614 return $this->plugin_name($parent) . ' / ' . $type;
623 * Returns a localized name of a plugin type in plural form
625 * Most plugin types define their names in core_plugin lang file. In case of subplugins,
626 * we try to ask the parent plugin for the name. In the worst case, we will return
627 * the value of the passed $type parameter.
629 * @param string $type the type of the plugin, e.g. mod or workshopform
632 public function plugintype_name_plural($type) {
634 if (get_string_manager()->string_exists('type_' . $type . '_plural', 'core_plugin')) {
635 // For most plugin types, their names are defined in core_plugin lang file.
636 return get_string('type_' . $type . '_plural', 'core_plugin');
638 } else if ($parent = $this->get_parent_of_subplugin($type)) {
639 // If this is a subplugin, try to ask the parent plugin for the name.
640 if (get_string_manager()->string_exists('subplugintype_' . $type . '_plural', $parent)) {
641 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type . '_plural', $parent);
643 return $this->plugin_name($parent) . ' / ' . $type;
652 * Returns information about the known plugin, or null
654 * @param string $component frankenstyle component name.
655 * @return \core\plugininfo\base|null the corresponding plugin information.
657 public function get_plugin_info($component) {
658 list($type, $name) = core_component::normalize_component($component);
659 $plugins = $this->get_plugins_of_type($type);
660 if (isset($plugins[$name])) {
661 return $plugins[$name];
668 * Check to see if the current version of the plugin seems to be a checkout of an external repository.
670 * @param string $component frankenstyle component name
671 * @return false|string
673 public function plugin_external_source($component) {
675 $plugininfo = $this->get_plugin_info($component);
677 if (is_null($plugininfo)) {
681 $pluginroot = $plugininfo->rootdir;
683 if (is_dir($pluginroot.'/.git')) {
687 if (is_file($pluginroot.'/.git')) {
688 return 'git-submodule';
691 if (is_dir($pluginroot.'/CVS')) {
695 if (is_dir($pluginroot.'/.svn')) {
699 if (is_dir($pluginroot.'/.hg')) {
707 * Get a list of any other plugins that require this one.
708 * @param string $component frankenstyle component name.
709 * @return array of frankensyle component names that require this one.
711 public function other_plugins_that_require($component) {
713 foreach ($this->get_plugins() as $type => $plugins) {
714 foreach ($plugins as $plugin) {
715 $required = $plugin->get_other_required_plugins();
716 if (isset($required[$component])) {
717 $others[] = $plugin->component;
725 * Check a dependencies list against the list of installed plugins.
726 * @param array $dependencies compenent name to required version or ANY_VERSION.
727 * @return bool true if all the dependencies are satisfied.
729 public function are_dependencies_satisfied($dependencies) {
730 foreach ($dependencies as $component => $requiredversion) {
731 $otherplugin = $this->get_plugin_info($component);
732 if (is_null($otherplugin)) {
736 if ($requiredversion != ANY_VERSION and $otherplugin->versiondisk < $requiredversion) {
745 * Checks all dependencies for all installed plugins
747 * This is used by install and upgrade. The array passed by reference as the second
748 * argument is populated with the list of plugins that have failed dependencies (note that
749 * a single plugin can appear multiple times in the $failedplugins).
751 * @param int $moodleversion the version from version.php.
752 * @param array $failedplugins to return the list of plugins with non-satisfied dependencies
753 * @return bool true if all the dependencies are satisfied for all plugins.
755 public function all_plugins_ok($moodleversion, &$failedplugins = array()) {
758 foreach ($this->get_plugins() as $type => $plugins) {
759 foreach ($plugins as $plugin) {
761 if (!$plugin->is_core_dependency_satisfied($moodleversion)) {
763 $failedplugins[] = $plugin->component;
766 if (!$this->are_dependencies_satisfied($plugin->get_other_required_plugins())) {
768 $failedplugins[] = $plugin->component;
777 * Resolve requirements and dependencies of a plugin.
779 * Returns an array of objects describing the requirement/dependency,
780 * indexed by the frankenstyle name of the component. The returned array
781 * can be empty. The objects in the array have following properties:
786 * ->(string)availability
788 * @param \core\plugininfo\base $plugin the plugin we are checking
789 * @param null|string|int|double $moodleversion explicit moodle core version to check against, defaults to $CFG->version
790 * @param null|string|int $moodlebranch explicit moodle core branch to check against, defaults to $CFG->branch
791 * @return array of objects
793 public function resolve_requirements(\core\plugininfo\base $plugin, $moodleversion=null, $moodlebranch=null) {
796 if ($plugin->versiondisk === null) {
797 // Missing from disk, we have no version.php to read from.
801 if ($moodleversion === null) {
802 $moodleversion = $CFG->version;
805 if ($moodlebranch === null) {
806 $moodlebranch = $CFG->branch;
810 $reqcore = $this->resolve_core_requirements($plugin, $moodleversion);
812 if (!empty($reqcore)) {
813 $reqs['core'] = $reqcore;
816 foreach ($plugin->get_other_required_plugins() as $reqplug => $reqver) {
817 $reqs[$reqplug] = $this->resolve_dependency_requirements($plugin, $reqplug, $reqver, $moodlebranch);
824 * Helper method to resolve plugin's requirements on the moodle core.
826 * @param \core\plugininfo\base $plugin the plugin we are checking
827 * @param string|int|double $moodleversion moodle core branch to check against
830 protected function resolve_core_requirements(\core\plugininfo\base $plugin, $moodleversion) {
832 $reqs = (object)array(
836 'availability' => null,
839 $reqs->hasver = $moodleversion;
841 if (empty($plugin->versionrequires)) {
842 $reqs->reqver = ANY_VERSION;
844 $reqs->reqver = $plugin->versionrequires;
847 if ($plugin->is_core_dependency_satisfied($moodleversion)) {
848 $reqs->status = self::REQUIREMENT_STATUS_OK;
850 $reqs->status = self::REQUIREMENT_STATUS_OUTDATED;
857 * Helper method to resolve plugin's dependecies on other plugins.
859 * @param \core\plugininfo\base $plugin the plugin we are checking
860 * @param string $otherpluginname
861 * @param string|int $requiredversion
862 * @param string|int $moodlebranch explicit moodle core branch to check against, defaults to $CFG->branch
865 protected function resolve_dependency_requirements(\core\plugininfo\base $plugin, $otherpluginname,
866 $requiredversion, $moodlebranch) {
868 $reqs = (object)array(
872 'availability' => null,
875 $otherplugin = $this->get_plugin_info($otherpluginname);
877 if ($otherplugin !== null) {
878 // The required plugin is installed.
879 $reqs->hasver = $otherplugin->versiondisk;
880 $reqs->reqver = $requiredversion;
881 // Check it has sufficient version.
882 if ($requiredversion == ANY_VERSION or $otherplugin->versiondisk >= $requiredversion) {
883 $reqs->status = self::REQUIREMENT_STATUS_OK;
885 $reqs->status = self::REQUIREMENT_STATUS_OUTDATED;
889 // The required plugin is not installed.
890 $reqs->hasver = null;
891 $reqs->reqver = $requiredversion;
892 $reqs->status = self::REQUIREMENT_STATUS_MISSING;
895 if ($reqs->status !== self::REQUIREMENT_STATUS_OK) {
896 if ($this->is_remote_plugin_available($otherpluginname, $requiredversion, false)) {
897 $reqs->availability = self::REQUIREMENT_AVAILABLE;
899 $reqs->availability = self::REQUIREMENT_UNAVAILABLE;
907 * Is the given plugin version available in the plugins directory?
909 * See {@link self::get_remote_plugin_info()} for the full explanation of how the $version
910 * parameter is interpretted.
912 * @param string $component plugin frankenstyle name
913 * @param string|int $version ANY_VERSION or the version number
914 * @param bool $exactmatch false if "given version or higher" is requested
917 public function is_remote_plugin_available($component, $version, $exactmatch) {
919 $info = $this->get_remote_plugin_info($component, $version, $exactmatch);
922 // There is no available plugin of that name.
926 if (empty($info->version)) {
927 // Plugin is known, but no suitable version was found.
935 * Can the given plugin version be installed via the admin UI?
937 * This check should be used whenever attempting to install a plugin from
938 * the plugins directory (new install, available update, missing dependency).
940 * @param string $component
941 * @param int $version version number
942 * @param string $reason returned code of the reason why it is not
945 public function is_remote_plugin_installable($component, $version, &$reason=null) {
948 // Make sure the feature is not disabled.
949 if (!empty($CFG->disableupdateautodeploy)) {
950 $reason = 'disabled';
954 // Make sure the version is available.
955 if (!$this->is_remote_plugin_available($component, $version, true)) {
956 $reason = 'remoteunavailable';
960 // Make sure the plugin type root directory is writable.
961 list($plugintype, $pluginname) = core_component::normalize_component($component);
962 if (!$this->is_plugintype_writable($plugintype)) {
963 $reason = 'notwritableplugintype';
967 $remoteinfo = $this->get_remote_plugin_info($component, $version, true);
968 $localinfo = $this->get_plugin_info($component);
971 // If the plugin is already present, prevent downgrade.
972 if ($localinfo->versiondb > $remoteinfo->version->version) {
973 $reason = 'cannotdowngrade';
977 // Make sure we have write access to all the existing code.
978 if (is_dir($localinfo->rootdir)) {
979 if (!$this->is_plugin_folder_removable($component)) {
980 $reason = 'notwritableplugin';
986 // Looks like it could work.
991 * Given the list of remote plugin infos, return just those installable.
993 * This is typically used on lists returned by
994 * {@link self::available_updates()} or {@link self::missing_dependencies()}
995 * to perform bulk installation of remote plugins.
997 * @param array $remoteinfos list of {@link \core\update\remote_info}
1000 public function filter_installable($remoteinfos) {
1003 if (!empty($CFG->disableupdateautodeploy)) {
1006 if (empty($remoteinfos)) {
1009 $installable = array();
1010 foreach ($remoteinfos as $index => $remoteinfo) {
1011 if ($this->is_remote_plugin_installable($remoteinfo->component, $remoteinfo->version->version)) {
1012 $installable[$index] = $remoteinfo;
1015 return $installable;
1019 * Returns information about a plugin in the plugins directory.
1021 * This is typically used when checking for available dependencies (in
1022 * which case the $version represents minimal version we need), or
1023 * when installing an available update or a new plugin from the plugins
1024 * directory (in which case the $version is exact version we are
1025 * interested in). The interpretation of the $version is controlled
1026 * by the $exactmatch argument.
1028 * If a plugin with the given component name is found, data about the
1029 * plugin are returned as an object. The ->version property of the object
1030 * contains the information about the particular plugin version that
1031 * matches best the given critera. The ->version property is false if no
1032 * suitable version of the plugin was found (yet the plugin itself is
1035 * See {@link \core\update\api::validate_pluginfo_format()} for the
1036 * returned data structure.
1038 * @param string $component plugin frankenstyle name
1039 * @param string|int $version ANY_VERSION or the version number
1040 * @param bool $exactmatch false if "given version or higher" is requested
1041 * @return \core\update\remote_info|bool
1043 public function get_remote_plugin_info($component, $version, $exactmatch) {
1045 if ($exactmatch and $version == ANY_VERSION) {
1046 throw new coding_exception('Invalid request for exactly any version, it does not make sense.');
1049 $client = $this->get_update_api_client();
1052 // Use client's get_plugin_info() method.
1053 if (!isset($this->remotepluginsinfoexact[$component][$version])) {
1054 $this->remotepluginsinfoexact[$component][$version] = $client->get_plugin_info($component, $version);
1056 return $this->remotepluginsinfoexact[$component][$version];
1059 // Use client's find_plugin() method.
1060 if (!isset($this->remotepluginsinfoatleast[$component][$version])) {
1061 $this->remotepluginsinfoatleast[$component][$version] = $client->find_plugin($component, $version);
1063 return $this->remotepluginsinfoatleast[$component][$version];
1068 * Obtain the plugin ZIP file from the given URL
1070 * The caller is supposed to know both downloads URL and the MD5 hash of
1071 * the ZIP contents in advance, typically by using the API requests against
1072 * the plugins directory.
1074 * @param string $url
1075 * @param string $md5
1076 * @return string|bool full path to the file, false on error
1078 public function get_remote_plugin_zip($url, $md5) {
1081 if (!empty($CFG->disableupdateautodeploy)) {
1084 return $this->get_code_manager()->get_remote_plugin_zip($url, $md5);
1088 * Extracts the saved plugin ZIP file.
1090 * Returns the list of files found in the ZIP. The format of that list is
1091 * array of (string)filerelpath => (bool|string) where the array value is
1092 * either true or a string describing the problematic file.
1094 * @see zip_packer::extract_to_pathname()
1095 * @param string $zipfilepath full path to the saved ZIP file
1096 * @param string $targetdir full path to the directory to extract the ZIP file to
1097 * @param string $rootdir explicitly rename the root directory of the ZIP into this non-empty value
1098 * @return array list of extracted files as returned by {@link zip_packer::extract_to_pathname()}
1100 public function unzip_plugin_file($zipfilepath, $targetdir, $rootdir = '') {
1101 return $this->get_code_manager()->unzip_plugin_file($zipfilepath, $targetdir, $rootdir);
1105 * Detects the plugin's name from its ZIP file.
1107 * Plugin ZIP packages are expected to contain a single directory and the
1108 * directory name would become the plugin name once extracted to the Moodle
1111 * @param string $zipfilepath full path to the ZIP files
1112 * @return string|bool false on error
1114 public function get_plugin_zip_root_dir($zipfilepath) {
1115 return $this->get_code_manager()->get_plugin_zip_root_dir($zipfilepath);
1119 * Return a list of missing dependencies.
1121 * This should provide the full list of plugins that should be installed to
1122 * fulfill the requirements of all plugins, if possible.
1124 * @param bool $availableonly return only available missing dependencies
1125 * @return array of \core\update\remote_info|bool indexed by the component name
1127 public function missing_dependencies($availableonly=false) {
1129 $dependencies = array();
1131 foreach ($this->get_plugins() as $plugintype => $pluginfos) {
1132 foreach ($pluginfos as $pluginname => $pluginfo) {
1133 foreach ($this->resolve_requirements($pluginfo) as $reqname => $reqinfo) {
1134 if ($reqname === 'core') {
1137 if ($reqinfo->status != self::REQUIREMENT_STATUS_OK) {
1138 if ($reqinfo->availability == self::REQUIREMENT_AVAILABLE) {
1139 $remoteinfo = $this->get_remote_plugin_info($reqname, $reqinfo->reqver, false);
1141 if (empty($dependencies[$reqname])) {
1142 $dependencies[$reqname] = $remoteinfo;
1144 // If resolving requirements has led to two different versions of the same
1145 // remote plugin, pick the higher version. This can happen in cases like one
1146 // plugin requiring ANY_VERSION and another plugin requiring specific higher
1147 // version with lower maturity of a remote plugin.
1148 if ($remoteinfo->version->version > $dependencies[$reqname]->version->version) {
1149 $dependencies[$reqname] = $remoteinfo;
1154 if (!isset($dependencies[$reqname])) {
1155 // Unable to find a plugin fulfilling the requirements.
1156 $dependencies[$reqname] = false;
1164 if ($availableonly) {
1165 foreach ($dependencies as $component => $info) {
1166 if (empty($info) or empty($info->version)) {
1167 unset($dependencies[$component]);
1172 return $dependencies;
1176 * Is it possible to uninstall the given plugin?
1178 * False is returned if the plugininfo subclass declares the uninstall should
1179 * not be allowed via {@link \core\plugininfo\base::is_uninstall_allowed()} or if the
1180 * core vetoes it (e.g. becase the plugin or some of its subplugins is required
1181 * by some other installed plugin).
1183 * @param string $component full frankenstyle name, e.g. mod_foobar
1186 public function can_uninstall_plugin($component) {
1188 $pluginfo = $this->get_plugin_info($component);
1190 if (is_null($pluginfo)) {
1194 if (!$this->common_uninstall_check($pluginfo)) {
1198 // Verify only if something else requires the subplugins, do not verify their common_uninstall_check()!
1199 $subplugins = $this->get_subplugins_of_plugin($pluginfo->component);
1200 foreach ($subplugins as $subpluginfo) {
1201 // Check if there are some other plugins requiring this subplugin
1202 // (but the parent and siblings).
1203 foreach ($this->other_plugins_that_require($subpluginfo->component) as $requiresme) {
1204 $ismyparent = ($pluginfo->component === $requiresme);
1205 $ismysibling = in_array($requiresme, array_keys($subplugins));
1206 if (!$ismyparent and !$ismysibling) {
1212 // Check if there are some other plugins requiring this plugin
1213 // (but its subplugins).
1214 foreach ($this->other_plugins_that_require($pluginfo->component) as $requiresme) {
1215 $ismysubplugin = in_array($requiresme, array_keys($subplugins));
1216 if (!$ismysubplugin) {
1225 * Perform the installation of plugins.
1227 * If used for installation of remote plugins from the Moodle Plugins
1228 * directory, the $plugins must be list of {@link \core\update\remote_info}
1229 * object that represent installable remote plugins. The caller can use
1230 * {@link self::filter_installable()} to prepare the list.
1232 * If used for installation of plugins from locally available ZIP files,
1233 * the $plugins should be list of objects with properties ->component and
1236 * The method uses {@link mtrace()} to produce direct output and can be
1237 * used in both web and cli interfaces.
1239 * @param array $plugins list of plugins
1240 * @param bool $confirmed should the files be really deployed into the dirroot?
1241 * @param bool $silent perform without output
1242 * @return bool true on success
1244 public function install_plugins(array $plugins, $confirmed, $silent) {
1245 global $CFG, $OUTPUT;
1247 if (!empty($CFG->disableupdateautodeploy)) {
1251 if (empty($plugins)) {
1255 $ok = get_string('ok', 'core');
1257 // Let admins know they can expect more verbose output.
1258 $silent or $this->mtrace(get_string('packagesdebug', 'core_plugin'), PHP_EOL, DEBUG_NORMAL);
1260 // Download all ZIP packages if we do not have them yet.
1262 foreach ($plugins as $plugin) {
1263 if ($plugin instanceof \core\update\remote_info) {
1264 $zips[$plugin->component] = $this->get_remote_plugin_zip($plugin->version->downloadurl,
1265 $plugin->version->downloadmd5);
1266 $silent or $this->mtrace(get_string('packagesdownloading', 'core_plugin', $plugin->component), ' ... ');
1267 $silent or $this->mtrace(PHP_EOL.' <- '.$plugin->version->downloadurl, '', DEBUG_DEVELOPER);
1268 $silent or $this->mtrace(PHP_EOL.' -> '.$zips[$plugin->component], ' ... ', DEBUG_DEVELOPER);
1269 if (!$zips[$plugin->component]) {
1270 $silent or $this->mtrace(get_string('error'));
1273 $silent or $this->mtrace($ok);
1275 if (empty($plugin->zipfilepath)) {
1276 throw new coding_exception('Unexpected data structure provided');
1278 $zips[$plugin->component] = $plugin->zipfilepath;
1279 $silent or $this->mtrace('ZIP '.$plugin->zipfilepath, PHP_EOL, DEBUG_DEVELOPER);
1283 // Validate all downloaded packages.
1284 foreach ($plugins as $plugin) {
1285 $zipfile = $zips[$plugin->component];
1286 $silent or $this->mtrace(get_string('packagesvalidating', 'core_plugin', $plugin->component), ' ... ');
1287 list($plugintype, $pluginname) = core_component::normalize_component($plugin->component);
1288 $tmp = make_request_directory();
1289 $zipcontents = $this->unzip_plugin_file($zipfile, $tmp, $pluginname);
1290 if (empty($zipcontents)) {
1291 $silent or $this->mtrace(get_string('error'));
1292 $silent or $this->mtrace('Unable to unzip '.$zipfile, PHP_EOL, DEBUG_DEVELOPER);
1296 $validator = \core\update\validator::instance($tmp, $zipcontents);
1297 $validator->assert_plugin_type($plugintype);
1298 $validator->assert_moodle_version($CFG->version);
1299 // TODO Check for missing dependencies during validation.
1300 $result = $validator->execute();
1302 $result ? $this->mtrace($ok) : $this->mtrace(get_string('error'));
1303 foreach ($validator->get_messages() as $message) {
1304 if ($message->level === $validator::INFO) {
1305 // Display [OK] validation messages only if debugging mode is DEBUG_NORMAL.
1306 $level = DEBUG_NORMAL;
1307 } else if ($message->level === $validator::DEBUG) {
1308 // Display [Debug] validation messages only if debugging mode is DEBUG_ALL.
1311 // Display [Warning] and [Error] always.
1314 if ($message->level === $validator::WARNING and !CLI_SCRIPT) {
1315 $this->mtrace(' <strong>['.$validator->message_level_name($message->level).']</strong>', ' ', $level);
1317 $this->mtrace(' ['.$validator->message_level_name($message->level).']', ' ', $level);
1319 $this->mtrace($validator->message_code_name($message->msgcode), ' ', $level);
1320 $info = $validator->message_code_info($message->msgcode, $message->addinfo);
1322 $this->mtrace('['.s($info).']', ' ', $level);
1323 } else if (is_string($message->addinfo)) {
1324 $this->mtrace('['.s($message->addinfo, true).']', ' ', $level);
1326 $this->mtrace('['.s(json_encode($message->addinfo, true)).']', ' ', $level);
1328 if ($icon = $validator->message_help_icon($message->msgcode)) {
1330 $this->mtrace(PHP_EOL.' ^^^ '.get_string('help').': '.
1331 get_string($icon->identifier.'_help', $icon->component), '', $level);
1333 $this->mtrace($OUTPUT->render($icon), ' ', $level);
1336 $this->mtrace(PHP_EOL, '', $level);
1340 $silent or $this->mtrace(get_string('packagesvalidatingfailed', 'core_plugin'));
1344 $silent or $this->mtrace(PHP_EOL.get_string('packagesvalidatingok', 'core_plugin'));
1350 // Extract all ZIP packs do the dirroot.
1351 foreach ($plugins as $plugin) {
1352 $silent or $this->mtrace(get_string('packagesextracting', 'core_plugin', $plugin->component), ' ... ');
1353 $zipfile = $zips[$plugin->component];
1354 list($plugintype, $pluginname) = core_component::normalize_component($plugin->component);
1355 $target = $this->get_plugintype_root($plugintype);
1356 if (file_exists($target.'/'.$pluginname)) {
1357 $this->remove_plugin_folder($this->get_plugin_info($plugin->component));
1359 if (!$this->unzip_plugin_file($zipfile, $target, $pluginname)) {
1360 $silent or $this->mtrace(get_string('error'));
1361 $silent or $this->mtrace('Unable to unzip '.$zipfile, PHP_EOL, DEBUG_DEVELOPER);
1362 if (function_exists('opcache_reset')) {
1367 $silent or $this->mtrace($ok);
1369 if (function_exists('opcache_reset')) {
1377 * Outputs the given message via {@link mtrace()}.
1379 * If $debug is provided, then the message is displayed only at the given
1380 * debugging level (e.g. DEBUG_DEVELOPER to display the message only if the
1381 * site has developer debugging level selected).
1383 * @param string $msg message
1384 * @param string $eol end of line
1385 * @param null|int $debug null to display always, int only on given debug level
1387 protected function mtrace($msg, $eol=PHP_EOL, $debug=null) {
1390 if ($debug !== null and !debugging(null, $debug)) {
1398 * Returns uninstall URL if exists.
1400 * @param string $component
1401 * @param string $return either 'overview' or 'manage'
1402 * @return moodle_url uninstall URL, null if uninstall not supported
1404 public function get_uninstall_url($component, $return = 'overview') {
1405 if (!$this->can_uninstall_plugin($component)) {
1409 $pluginfo = $this->get_plugin_info($component);
1411 if (is_null($pluginfo)) {
1415 if (method_exists($pluginfo, 'get_uninstall_url')) {
1416 debugging('plugininfo method get_uninstall_url() is deprecated, all plugins should be uninstalled via standard URL only.');
1417 return $pluginfo->get_uninstall_url($return);
1420 return $pluginfo->get_default_uninstall_url($return);
1424 * Uninstall the given plugin.
1426 * Automatically cleans-up all remaining configuration data, log records, events,
1427 * files from the file pool etc.
1429 * In the future, the functionality of {@link uninstall_plugin()} function may be moved
1430 * into this method and all the code should be refactored to use it. At the moment, we
1431 * mimic this future behaviour by wrapping that function call.
1433 * @param string $component
1434 * @param progress_trace $progress traces the process
1435 * @return bool true on success, false on errors/problems
1437 public function uninstall_plugin($component, progress_trace $progress) {
1439 $pluginfo = $this->get_plugin_info($component);
1441 if (is_null($pluginfo)) {
1445 // Give the pluginfo class a chance to execute some steps.
1446 $result = $pluginfo->uninstall($progress);
1451 // Call the legacy core function to uninstall the plugin.
1453 uninstall_plugin($pluginfo->type, $pluginfo->name);
1454 $progress->output(ob_get_clean());
1460 * Checks if there are some plugins with a known available update
1462 * @return bool true if there is at least one available update
1464 public function some_plugins_updatable() {
1465 foreach ($this->get_plugins() as $type => $plugins) {
1466 foreach ($plugins as $plugin) {
1467 if ($plugin->available_updates()) {
1477 * Returns list of available updates for the given component.
1479 * This method should be considered as internal API and is supposed to be
1480 * called by {@link \core\plugininfo\base::available_updates()} only
1481 * to lazy load the data once they are first requested.
1483 * @param string $component frankenstyle name of the plugin
1484 * @return null|array array of \core\update\info objects or null
1486 public function load_available_updates_for_plugin($component) {
1489 $provider = \core\update\checker::instance();
1491 if (!$provider->enabled() or during_initial_install()) {
1495 if (isset($CFG->updateminmaturity)) {
1496 $minmaturity = $CFG->updateminmaturity;
1498 // This can happen during the very first upgrade to 2.3.
1499 $minmaturity = MATURITY_STABLE;
1502 return $provider->get_update_info($component, array('minmaturity' => $minmaturity));
1506 * Returns a list of all available updates to be installed.
1508 * This is used when "update all plugins" action is performed at the
1509 * administration UI screen.
1511 * Returns array of remote info objects indexed by the plugin
1512 * component. If there are multiple updates available (typically a mix of
1513 * stable and non-stable ones), we pick the most mature most recent one.
1515 * Plugins without explicit maturity are considered more mature than
1516 * release candidates but less mature than explicit stable (this should be
1517 * pretty rare case).
1519 * @return array (string)component => (\core\update\remote_info)remoteinfo
1521 public function available_updates() {
1525 foreach ($this->get_plugins() as $type => $plugins) {
1526 foreach ($plugins as $plugin) {
1527 $availableupdates = $plugin->available_updates();
1528 if (empty($availableupdates)) {
1531 foreach ($availableupdates as $update) {
1532 if (empty($updates[$plugin->component])) {
1533 $updates[$plugin->component] = $update;
1536 $maturitycurrent = $updates[$plugin->component]->maturity;
1537 if (empty($maturitycurrent)) {
1538 $maturitycurrent = MATURITY_STABLE - 25;
1540 $maturityremote = $update->maturity;
1541 if (empty($maturityremote)) {
1542 $maturityremote = MATURITY_STABLE - 25;
1544 if ($maturityremote < $maturitycurrent) {
1547 if ($maturityremote > $maturitycurrent) {
1548 $updates[$plugin->component] = $update;
1551 if ($update->version > $updates[$plugin->component]->version) {
1552 $updates[$plugin->component] = $update;
1559 foreach ($updates as $component => $update) {
1560 $remoteinfo = $this->get_remote_plugin_info($component, $update->version, true);
1561 if (empty($remoteinfo) or empty($remoteinfo->version)) {
1562 unset($updates[$component]);
1564 $updates[$component] = $remoteinfo;
1572 * Check to see if the given plugin folder can be removed by the web server process.
1574 * @param string $component full frankenstyle component
1577 public function is_plugin_folder_removable($component) {
1579 $pluginfo = $this->get_plugin_info($component);
1581 if (is_null($pluginfo)) {
1585 // To be able to remove the plugin folder, its parent must be writable, too.
1586 if (!is_writable(dirname($pluginfo->rootdir))) {
1590 // Check that the folder and all its content is writable (thence removable).
1591 return $this->is_directory_removable($pluginfo->rootdir);
1595 * Is it possible to create a new plugin directory for the given plugin type?
1597 * @throws coding_exception for invalid plugin types or non-existing plugin type locations
1598 * @param string $plugintype
1601 public function is_plugintype_writable($plugintype) {
1603 $plugintypepath = $this->get_plugintype_root($plugintype);
1605 if (is_null($plugintypepath)) {
1606 throw new coding_exception('Unknown plugin type: '.$plugintype);
1609 if ($plugintypepath === false) {
1610 throw new coding_exception('Plugin type location does not exist: '.$plugintype);
1613 return is_writable($plugintypepath);
1617 * Returns the full path of the root of the given plugin type
1619 * Null is returned if the plugin type is not known. False is returned if
1620 * the plugin type root is expected but not found. Otherwise, string is
1623 * @param string $plugintype
1624 * @return string|bool|null
1626 public function get_plugintype_root($plugintype) {
1628 $plugintypepath = null;
1629 foreach (core_component::get_plugin_types() as $type => $fullpath) {
1630 if ($type === $plugintype) {
1631 $plugintypepath = $fullpath;
1635 if (is_null($plugintypepath)) {
1638 if (!is_dir($plugintypepath)) {
1642 return $plugintypepath;
1646 * Defines a list of all plugins that were originally shipped in the standard Moodle distribution,
1647 * but are not anymore and are deleted during upgrades.
1649 * The main purpose of this list is to hide missing plugins during upgrade.
1651 * @param string $type plugin type
1652 * @param string $name plugin name
1655 public static function is_deleted_standard_plugin($type, $name) {
1656 // Do not include plugins that were removed during upgrades to versions that are
1657 // not supported as source versions for upgrade any more. For example, at MOODLE_23_STABLE
1658 // branch, listed should be no plugins that were removed at 1.9.x - 2.1.x versions as
1659 // Moodle 2.3 supports upgrades from 2.2.x only.
1661 'qformat' => array('blackboard', 'learnwise'),
1662 'enrol' => array('authorize'),
1663 'tinymce' => array('dragmath'),
1664 'tool' => array('bloglevelupgrade', 'qeupgradehelper', 'timezoneimport'),
1665 'theme' => array('mymobile', 'afterburner', 'anomaly', 'arialist', 'binarius', 'boxxie', 'brick', 'formal_white',
1666 'formfactor', 'fusion', 'leatherbound', 'magazine', 'nimble', 'nonzero', 'overlay', 'serenity', 'sky_high',
1667 'splash', 'standard', 'standardold'),
1668 'webservice' => array('amf'),
1671 if (!isset($plugins[$type])) {
1674 return in_array($name, $plugins[$type]);
1678 * Defines a white list of all plugins shipped in the standard Moodle distribution
1680 * @param string $type
1681 * @return false|array array of standard plugins or false if the type is unknown
1683 public static function standard_plugins_list($type) {
1685 $standard_plugins = array(
1687 'antivirus' => array(
1692 'accessibilitychecker', 'accessibilityhelper', 'align',
1693 'backcolor', 'bold', 'charmap', 'clear', 'collapse', 'emoticon',
1694 'equation', 'fontcolor', 'html', 'image', 'indent', 'italic',
1695 'link', 'managefiles', 'media', 'noautolink', 'orderedlist',
1696 'rtl', 'strike', 'subscript', 'superscript', 'table', 'title',
1697 'underline', 'undo', 'unorderedlist'
1700 'assignment' => array(
1701 'offline', 'online', 'upload', 'uploadsingle'
1704 'assignsubmission' => array(
1705 'comments', 'file', 'onlinetext'
1708 'assignfeedback' => array(
1709 'comments', 'file', 'offline', 'editpdf'
1713 'cas', 'db', 'email', 'fc', 'imap', 'ldap', 'manual', 'mnet',
1714 'nntp', 'nologin', 'none', 'pam', 'pop3', 'radius',
1715 'shibboleth', 'webservice'
1718 'availability' => array(
1719 'completion', 'date', 'grade', 'group', 'grouping', 'profile'
1723 'activity_modules', 'activity_results', 'admin_bookmarks', 'badges',
1724 'blog_menu', 'blog_recent', 'blog_tags', 'calendar_month',
1725 'calendar_upcoming', 'comments', 'community',
1726 'completionstatus', 'course_list', 'course_overview',
1727 'course_summary', 'feedback', 'globalsearch', 'glossary_random', 'html',
1728 'login', 'mentees', 'messages', 'mnet_hosts', 'myprofile',
1729 'navigation', 'news_items', 'online_users', 'participants',
1730 'private_files', 'quiz_results', 'recent_activity',
1731 'rss_client', 'search_forums', 'section_links',
1732 'selfcompletion', 'settings', 'site_main_menu',
1733 'social_activities', 'tag_flickr', 'tag_youtube', 'tags'
1736 'booktool' => array(
1737 'exportimscp', 'importhtml', 'print'
1740 'cachelock' => array(
1744 'cachestore' => array(
1745 'file', 'memcache', 'memcached', 'mongodb', 'session', 'static'
1748 'calendartype' => array(
1752 'coursereport' => array(
1756 'datafield' => array(
1757 'checkbox', 'date', 'file', 'latlong', 'menu', 'multimenu',
1758 'number', 'picture', 'radiobutton', 'text', 'textarea', 'url'
1761 'datapreset' => array(
1766 'atto', 'textarea', 'tinymce'
1770 'category', 'cohort', 'database', 'flatfile',
1771 'guest', 'imsenterprise', 'ldap', 'manual', 'meta', 'mnet',
1776 'activitynames', 'algebra', 'censor', 'emailprotect',
1777 'emoticon', 'mathjaxloader', 'mediaplugin', 'multilang', 'tex', 'tidy',
1778 'urltolink', 'data', 'glossary'
1782 'singleactivity', 'social', 'topics', 'weeks'
1785 'gradeexport' => array(
1786 'ods', 'txt', 'xls', 'xml'
1789 'gradeimport' => array(
1790 'csv', 'direct', 'xml'
1793 'gradereport' => array(
1794 'grader', 'history', 'outcomes', 'overview', 'user', 'singleview'
1797 'gradingform' => array(
1804 'logstore' => array(
1805 'database', 'legacy', 'standard',
1808 'ltiservice' => array(
1809 'memberships', 'profile', 'toolproxy', 'toolsettings'
1813 'airnotifier', 'email', 'jabber', 'popup'
1816 'mnetservice' => array(
1821 'assign', 'assignment', 'book', 'chat', 'choice', 'data', 'feedback', 'folder',
1822 'forum', 'glossary', 'imscp', 'label', 'lesson', 'lti', 'page',
1823 'quiz', 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop'
1826 'plagiarism' => array(
1829 'portfolio' => array(
1830 'boxnet', 'download', 'flickr', 'googledocs', 'mahara', 'picasa'
1833 'profilefield' => array(
1834 'checkbox', 'datetime', 'menu', 'text', 'textarea'
1837 'qbehaviour' => array(
1838 'adaptive', 'adaptivenopenalty', 'deferredcbm',
1839 'deferredfeedback', 'immediatecbm', 'immediatefeedback',
1840 'informationitem', 'interactive', 'interactivecountback',
1841 'manualgraded', 'missing'
1845 'aiken', 'blackboard_six', 'examview', 'gift',
1846 'missingword', 'multianswer', 'webct',
1851 'calculated', 'calculatedmulti', 'calculatedsimple',
1852 'ddimageortext', 'ddmarker', 'ddwtos', 'description',
1853 'essay', 'gapselect', 'match', 'missingtype', 'multianswer',
1854 'multichoice', 'numerical', 'random', 'randomsamatch',
1855 'shortanswer', 'truefalse'
1859 'grading', 'overview', 'responses', 'statistics'
1862 'quizaccess' => array(
1863 'delaybetweenattempts', 'ipaddress', 'numattempts', 'openclosedate',
1864 'password', 'safebrowser', 'securewindow', 'timelimit'
1868 'backups', 'completion', 'configlog', 'courseoverview', 'eventlist',
1869 'log', 'loglive', 'outline', 'participation', 'progress', 'questioninstances', 'search',
1870 'security', 'stats', 'performance', 'usersessions'
1873 'repository' => array(
1874 'alfresco', 'areafiles', 'boxnet', 'coursefiles', 'dropbox', 'equella', 'filesystem',
1875 'flickr', 'flickr_public', 'googledocs', 'local', 'merlot',
1876 'picasa', 'recent', 'skydrive', 's3', 'upload', 'url', 'user', 'webdav',
1877 'wikimedia', 'youtube'
1884 'scormreport' => array(
1892 'ctrlhelp', 'managefiles', 'moodleemoticon', 'moodleimage',
1893 'moodlemedia', 'moodlenolink', 'pdw', 'spellchecker', 'wrap'
1897 'base', 'bootstrapbase', 'canvas', 'clean', 'more'
1901 'assignmentupgrade', 'availabilityconditions', 'behat', 'capability', 'customlang',
1902 'dbtransfer', 'filetypes', 'generator', 'health', 'innodb', 'installaddon',
1903 'langimport', 'log', 'messageinbound', 'multilangupgrade', 'monitor', 'phpunit', 'profiling',
1904 'replace', 'spamcleaner', 'task', 'templatelibrary',
1905 'unittest', 'uploadcourse', 'uploaduser', 'unsuproles', 'xmldb'
1908 'webservice' => array(
1909 'rest', 'soap', 'xmlrpc'
1912 'workshopallocation' => array(
1913 'manual', 'random', 'scheduled'
1916 'workshopeval' => array(
1920 'workshopform' => array(
1921 'accumulative', 'comments', 'numerrors', 'rubric'
1925 if (isset($standard_plugins[$type])) {
1926 return $standard_plugins[$type];
1933 * Remove the current plugin code from the dirroot.
1935 * If removing the currently installed version (which happens during
1936 * updates), we archive the code so that the upgrade can be cancelled.
1938 * To prevent accidental data-loss, we also archive the existing plugin
1939 * code if cancelling installation of it, so that the developer does not
1940 * loose the only version of their work-in-progress.
1942 * @param \core\plugininfo\base $plugin
1944 public function remove_plugin_folder(\core\plugininfo\base $plugin) {
1946 if (!$this->is_plugin_folder_removable($plugin->component)) {
1947 throw new moodle_exception('err_removing_unremovable_folder', 'core_plugin', '',
1948 array('plugin' => $pluginfo->component, 'rootdir' => $pluginfo->rootdir),
1949 'plugin root folder is not removable as expected');
1952 if ($plugin->get_status() === self::PLUGIN_STATUS_UPTODATE or $plugin->get_status() === self::PLUGIN_STATUS_NEW) {
1953 $this->archive_plugin_version($plugin);
1956 remove_dir($plugin->rootdir);
1958 if (function_exists('opcache_reset')) {
1964 * Can the installation of the new plugin be cancelled?
1966 * Subplugins can be cancelled only via their parent plugin, not separately
1967 * (they are considered as implicit requirements if distributed together
1968 * with the main package).
1970 * @param \core\plugininfo\base $plugin
1973 public function can_cancel_plugin_installation(\core\plugininfo\base $plugin) {
1976 if (!empty($CFG->disableupdateautodeploy)) {
1980 if (empty($plugin) or $plugin->is_standard() or $plugin->is_subplugin()
1981 or !$this->is_plugin_folder_removable($plugin->component)) {
1985 if ($plugin->get_status() === self::PLUGIN_STATUS_NEW) {
1993 * Can the upgrade of the existing plugin be cancelled?
1995 * Subplugins can be cancelled only via their parent plugin, not separately
1996 * (they are considered as implicit requirements if distributed together
1997 * with the main package).
1999 * @param \core\plugininfo\base $plugin
2002 public function can_cancel_plugin_upgrade(\core\plugininfo\base $plugin) {
2005 if (!empty($CFG->disableupdateautodeploy)) {
2006 // Cancelling the plugin upgrade is actually installation of the
2007 // previously archived version.
2011 if (empty($plugin) or $plugin->is_standard() or $plugin->is_subplugin()
2012 or !$this->is_plugin_folder_removable($plugin->component)) {
2016 if ($plugin->get_status() === self::PLUGIN_STATUS_UPGRADE) {
2017 if ($this->get_code_manager()->get_archived_plugin_version($plugin->component, $plugin->versiondb)) {
2026 * Removes the plugin code directory if it is not installed yet.
2028 * This is intended for the plugins check screen to give the admin a chance
2029 * to cancel the installation of just unzipped plugin before the database
2032 * @param string $component
2034 public function cancel_plugin_installation($component) {
2037 if (!empty($CFG->disableupdateautodeploy)) {
2041 $plugin = $this->get_plugin_info($component);
2043 if ($this->can_cancel_plugin_installation($plugin)) {
2044 $this->remove_plugin_folder($plugin);
2051 * Returns plugins, the installation of which can be cancelled.
2053 * @return array [(string)component] => (\core\plugininfo\base)plugin
2055 public function list_cancellable_installations() {
2058 if (!empty($CFG->disableupdateautodeploy)) {
2062 $cancellable = array();
2063 foreach ($this->get_plugins() as $type => $plugins) {
2064 foreach ($plugins as $plugin) {
2065 if ($this->can_cancel_plugin_installation($plugin)) {
2066 $cancellable[$plugin->component] = $plugin;
2071 return $cancellable;
2075 * Archive the current on-disk plugin code.
2077 * @param \core\plugiinfo\base $plugin
2080 public function archive_plugin_version(\core\plugininfo\base $plugin) {
2081 return $this->get_code_manager()->archive_plugin_version($plugin->rootdir, $plugin->component, $plugin->versiondisk);
2085 * Returns list of all archives that can be installed to cancel the plugin upgrade.
2087 * @return array [(string)component] => {(string)->component, (string)->zipfilepath}
2089 public function list_restorable_archives() {
2092 if (!empty($CFG->disableupdateautodeploy)) {
2096 $codeman = $this->get_code_manager();
2097 $restorable = array();
2098 foreach ($this->get_plugins() as $type => $plugins) {
2099 foreach ($plugins as $plugin) {
2100 if ($this->can_cancel_plugin_upgrade($plugin)) {
2101 $restorable[$plugin->component] = (object)array(
2102 'component' => $plugin->component,
2103 'zipfilepath' => $codeman->get_archived_plugin_version($plugin->component, $plugin->versiondb)
2113 * Reorders plugin types into a sequence to be displayed
2115 * For technical reasons, plugin types returned by {@link core_component::get_plugin_types()} are
2116 * in a certain order that does not need to fit the expected order for the display.
2117 * Particularly, activity modules should be displayed first as they represent the
2118 * real heart of Moodle. They should be followed by other plugin types that are
2119 * used to build the courses (as that is what one expects from LMS). After that,
2120 * other supportive plugin types follow.
2122 * @param array $types associative array
2123 * @return array same array with altered order of items
2125 protected function reorder_plugin_types(array $types) {
2126 $fix = array('mod' => $types['mod']);
2127 foreach (core_component::get_plugin_list('mod') as $plugin => $fulldir) {
2128 if (!$subtypes = core_component::get_subplugins('mod_'.$plugin)) {
2131 foreach ($subtypes as $subtype => $ignored) {
2132 $fix[$subtype] = $types[$subtype];
2136 $fix['mod'] = $types['mod'];
2137 $fix['block'] = $types['block'];
2138 $fix['qtype'] = $types['qtype'];
2139 $fix['qbehaviour'] = $types['qbehaviour'];
2140 $fix['qformat'] = $types['qformat'];
2141 $fix['filter'] = $types['filter'];
2143 $fix['editor'] = $types['editor'];
2144 foreach (core_component::get_plugin_list('editor') as $plugin => $fulldir) {
2145 if (!$subtypes = core_component::get_subplugins('editor_'.$plugin)) {
2148 foreach ($subtypes as $subtype => $ignored) {
2149 $fix[$subtype] = $types[$subtype];
2153 $fix['enrol'] = $types['enrol'];
2154 $fix['auth'] = $types['auth'];
2155 $fix['tool'] = $types['tool'];
2156 foreach (core_component::get_plugin_list('tool') as $plugin => $fulldir) {
2157 if (!$subtypes = core_component::get_subplugins('tool_'.$plugin)) {
2160 foreach ($subtypes as $subtype => $ignored) {
2161 $fix[$subtype] = $types[$subtype];
2165 foreach ($types as $type => $path) {
2166 if (!isset($fix[$type])) {
2167 $fix[$type] = $path;
2174 * Check if the given directory can be removed by the web server process.
2176 * This recursively checks that the given directory and all its contents
2179 * @param string $fullpath
2182 public function is_directory_removable($fullpath) {
2184 if (!is_writable($fullpath)) {
2188 if (is_dir($fullpath)) {
2189 $handle = opendir($fullpath);
2196 while ($filename = readdir($handle)) {
2198 if ($filename === '.' or $filename === '..') {
2202 $subfilepath = $fullpath.'/'.$filename;
2204 if (is_dir($subfilepath)) {
2205 $result = $result && $this->is_directory_removable($subfilepath);
2208 $result = $result && is_writable($subfilepath);
2218 * Helper method that implements common uninstall prerequisites
2220 * @param \core\plugininfo\base $pluginfo
2223 protected function common_uninstall_check(\core\plugininfo\base $pluginfo) {
2225 if (!$pluginfo->is_uninstall_allowed()) {
2226 // The plugin's plugininfo class declares it should not be uninstalled.
2230 if ($pluginfo->get_status() === static::PLUGIN_STATUS_NEW) {
2231 // The plugin is not installed. It should be either installed or removed from the disk.
2232 // Relying on this temporary state may be tricky.
2236 if (method_exists($pluginfo, 'get_uninstall_url') and is_null($pluginfo->get_uninstall_url())) {
2237 // Backwards compatibility.
2238 debugging('\core\plugininfo\base subclasses should use is_uninstall_allowed() instead of returning null in get_uninstall_url()',
2247 * Returns a code_manager instance to be used for the plugins code operations.
2249 * @return \core\update\code_manager
2251 protected function get_code_manager() {
2253 if ($this->codemanager === null) {
2254 $this->codemanager = new \core\update\code_manager();
2257 return $this->codemanager;
2261 * Returns a client for https://download.moodle.org/api/
2263 * @return \core\update\api
2265 protected function get_update_api_client() {
2267 if ($this->updateapiclient === null) {
2268 $this->updateapiclient = \core\update\api::client();
2271 return $this->updateapiclient;