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';
62 /** the current Moodle version is too high for plugin. */
63 const REQUIREMENT_STATUS_NEWER
= 'newer';
65 /** the required dependency is available in the plugins directory */
66 const REQUIREMENT_AVAILABLE
= 'available';
67 /** the required dependency is available in the plugins directory */
68 const REQUIREMENT_UNAVAILABLE
= 'unavailable';
70 /** the moodle version is explicitly supported */
71 const VERSION_SUPPORTED
= 'supported';
72 /** the moodle version is not explicitly supported */
73 const VERSION_NOT_SUPPORTED
= 'notsupported';
74 /** the plugin does not specify supports */
75 const VERSION_NO_SUPPORTS
= 'nosupports';
77 /** @var core_plugin_manager holds the singleton instance */
78 protected static $singletoninstance;
79 /** @var array of raw plugins information */
80 protected $pluginsinfo = null;
81 /** @var array of raw subplugins information */
82 protected $subpluginsinfo = null;
83 /** @var array cache information about availability in the plugins directory if requesting "at least" version */
84 protected $remotepluginsinfoatleast = null;
85 /** @var array cache information about availability in the plugins directory if requesting exact version */
86 protected $remotepluginsinfoexact = null;
87 /** @var array list of installed plugins $name=>$version */
88 protected $installedplugins = null;
89 /** @var array list of all enabled plugins $name=>$name */
90 protected $enabledplugins = null;
91 /** @var array list of all enabled plugins $name=>$diskversion */
92 protected $presentplugins = null;
93 /** @var array reordered list of plugin types */
94 protected $plugintypes = null;
95 /** @var \core\update\code_manager code manager to use for plugins code operations */
96 protected $codemanager = null;
97 /** @var \core\update\api client instance to use for accessing download.moodle.org/api/ */
98 protected $updateapiclient = null;
101 * Direct initiation not allowed, use the factory method {@link self::instance()}
103 protected function __construct() {
107 * Sorry, this is singleton
109 protected function __clone() {
113 * Factory method for this class
115 * @return core_plugin_manager the singleton instance
117 public static function instance() {
118 if (is_null(static::$singletoninstance)) {
119 static::$singletoninstance = new static();
121 return static::$singletoninstance;
126 * @param bool $phpunitreset
128 public static function reset_caches($phpunitreset = false) {
130 static::$singletoninstance = null;
132 if (static::$singletoninstance) {
133 static::$singletoninstance->pluginsinfo
= null;
134 static::$singletoninstance->subpluginsinfo
= null;
135 static::$singletoninstance->remotepluginsinfoatleast
= null;
136 static::$singletoninstance->remotepluginsinfoexact
= null;
137 static::$singletoninstance->installedplugins
= null;
138 static::$singletoninstance->enabledplugins
= null;
139 static::$singletoninstance->presentplugins
= null;
140 static::$singletoninstance->plugintypes
= null;
141 static::$singletoninstance->codemanager
= null;
142 static::$singletoninstance->updateapiclient
= null;
145 $cache = cache
::make('core', 'plugin_manager');
150 * Returns the result of {@link core_component::get_plugin_types()} ordered for humans
152 * @see self::reorder_plugin_types()
153 * @return array (string)name => (string)location
155 public function get_plugin_types() {
156 if (func_num_args() > 0) {
157 if (!func_get_arg(0)) {
158 throw new coding_exception('core_plugin_manager->get_plugin_types() does not support relative paths.');
161 if ($this->plugintypes
) {
162 return $this->plugintypes
;
165 $this->plugintypes
= $this->reorder_plugin_types(core_component
::get_plugin_types());
166 return $this->plugintypes
;
170 * Load list of installed plugins,
171 * always call before using $this->installedplugins.
173 * This method is caching results for all plugins.
175 protected function load_installed_plugins() {
178 if ($this->installedplugins
) {
182 if (empty($CFG->version
)) {
183 // Nothing installed yet.
184 $this->installedplugins
= array();
188 $cache = cache
::make('core', 'plugin_manager');
189 $installed = $cache->get('installed');
191 if (is_array($installed)) {
192 $this->installedplugins
= $installed;
196 $this->installedplugins
= array();
198 $versions = $DB->get_records('config_plugins', array('name'=>'version'));
199 foreach ($versions as $version) {
200 $parts = explode('_', $version->plugin
, 2);
201 if (!isset($parts[1])) {
202 // Invalid component, there must be at least one "_".
205 // Do not verify here if plugin type and name are valid.
206 $this->installedplugins
[$parts[0]][$parts[1]] = $version->value
;
209 foreach ($this->installedplugins
as $key => $value) {
210 ksort($this->installedplugins
[$key]);
213 $cache->set('installed', $this->installedplugins
);
217 * Return list of installed plugins of given type.
218 * @param string $type
219 * @return array $name=>$version
221 public function get_installed_plugins($type) {
222 $this->load_installed_plugins();
223 if (isset($this->installedplugins
[$type])) {
224 return $this->installedplugins
[$type];
230 * Load list of all enabled plugins,
231 * call before using $this->enabledplugins.
233 * This method is caching results from individual plugin info classes.
235 protected function load_enabled_plugins() {
238 if ($this->enabledplugins
) {
242 if (empty($CFG->version
)) {
243 $this->enabledplugins
= array();
247 $cache = cache
::make('core', 'plugin_manager');
248 $enabled = $cache->get('enabled');
250 if (is_array($enabled)) {
251 $this->enabledplugins
= $enabled;
255 $this->enabledplugins
= array();
257 require_once($CFG->libdir
.'/adminlib.php');
259 $plugintypes = core_component
::get_plugin_types();
260 foreach ($plugintypes as $plugintype => $fulldir) {
261 $plugininfoclass = static::resolve_plugininfo_class($plugintype);
262 if (class_exists($plugininfoclass)) {
263 $enabled = $plugininfoclass::get_enabled_plugins();
264 if (!is_array($enabled)) {
267 $this->enabledplugins
[$plugintype] = $enabled;
271 $cache->set('enabled', $this->enabledplugins
);
275 * Get list of enabled plugins of given type,
276 * the result may contain missing plugins.
278 * @param string $type
279 * @return array|null list of enabled plugins of this type, null if unknown
281 public function get_enabled_plugins($type) {
282 $this->load_enabled_plugins();
283 if (isset($this->enabledplugins
[$type])) {
284 return $this->enabledplugins
[$type];
290 * Load list of all present plugins - call before using $this->presentplugins.
292 protected function load_present_plugins() {
293 if ($this->presentplugins
) {
297 $cache = cache
::make('core', 'plugin_manager');
298 $present = $cache->get('present');
300 if (is_array($present)) {
301 $this->presentplugins
= $present;
305 $this->presentplugins
= array();
307 $plugintypes = core_component
::get_plugin_types();
308 foreach ($plugintypes as $type => $typedir) {
309 $plugs = core_component
::get_plugin_list($type);
310 foreach ($plugs as $plug => $fullplug) {
311 $module = new stdClass();
312 $plugin = new stdClass();
313 $plugin->version
= null;
314 include($fullplug.'/version.php');
316 // Check if the legacy $module syntax is still used.
317 if (!is_object($module) or (count((array)$module) > 0)) {
318 debugging('Unsupported $module syntax detected in version.php of the '.$type.'_'.$plug.' plugin.');
322 // Check if the component is properly declared.
323 if (empty($plugin->component
) or ($plugin->component
!== $type.'_'.$plug)) {
324 debugging('Plugin '.$type.'_'.$plug.' does not declare valid $plugin->component in its version.php.');
328 $this->presentplugins
[$type][$plug] = $plugin;
332 if (empty($skipcache)) {
333 $cache->set('present', $this->presentplugins
);
338 * Get list of present plugins of given type.
340 * @param string $type
341 * @return array|null list of presnet plugins $name=>$diskversion, null if unknown
343 public function get_present_plugins($type) {
344 $this->load_present_plugins();
345 if (isset($this->presentplugins
[$type])) {
346 return $this->presentplugins
[$type];
352 * Returns a tree of known plugins and information about them
354 * @return array 2D array. The first keys are plugin type names (e.g. qtype);
355 * the second keys are the plugin local name (e.g. multichoice); and
356 * the values are the corresponding objects extending {@link \core\plugininfo\base}
358 public function get_plugins() {
359 $this->init_pluginsinfo_property();
361 // Make sure all types are initialised.
362 foreach ($this->pluginsinfo
as $plugintype => $list) {
363 if ($list === null) {
364 $this->get_plugins_of_type($plugintype);
368 return $this->pluginsinfo
;
372 * Returns list of known plugins of the given type.
374 * This method returns the subset of the tree returned by {@link self::get_plugins()}.
375 * If the given type is not known, empty array is returned.
377 * @param string $type plugin type, e.g. 'mod' or 'workshopallocation'
378 * @return \core\plugininfo\base[] (string)plugin name (e.g. 'workshop') => corresponding subclass of {@link \core\plugininfo\base}
380 public function get_plugins_of_type($type) {
383 $this->init_pluginsinfo_property();
385 if (!array_key_exists($type, $this->pluginsinfo
)) {
389 if (is_array($this->pluginsinfo
[$type])) {
390 return $this->pluginsinfo
[$type];
393 $types = core_component
::get_plugin_types();
395 if (!isset($types[$type])) {
396 // Orphaned subplugins!
397 $plugintypeclass = static::resolve_plugininfo_class($type);
398 $this->pluginsinfo
[$type] = $plugintypeclass::get_plugins($type, null, $plugintypeclass, $this);
399 return $this->pluginsinfo
[$type];
402 /** @var \core\plugininfo\base $plugintypeclass */
403 $plugintypeclass = static::resolve_plugininfo_class($type);
404 $plugins = $plugintypeclass::get_plugins($type, $types[$type], $plugintypeclass, $this);
405 $this->pluginsinfo
[$type] = $plugins;
407 return $this->pluginsinfo
[$type];
411 * Init placeholder array for plugin infos.
413 protected function init_pluginsinfo_property() {
414 if (is_array($this->pluginsinfo
)) {
417 $this->pluginsinfo
= array();
419 $plugintypes = $this->get_plugin_types();
421 foreach ($plugintypes as $plugintype => $plugintyperootdir) {
422 $this->pluginsinfo
[$plugintype] = null;
425 // Add orphaned subplugin types.
426 $this->load_installed_plugins();
427 foreach ($this->installedplugins
as $plugintype => $unused) {
428 if (!isset($plugintypes[$plugintype])) {
429 $this->pluginsinfo
[$plugintype] = null;
435 * Find the plugin info class for given type.
437 * @param string $type
438 * @return string name of pluginfo class for give plugin type
440 public static function resolve_plugininfo_class($type) {
441 $plugintypes = core_component
::get_plugin_types();
442 if (!isset($plugintypes[$type])) {
443 return '\core\plugininfo\orphaned';
446 $parent = core_component
::get_subtype_parent($type);
449 $class = '\\'.$parent.'\plugininfo\\' . $type;
450 if (class_exists($class)) {
451 $plugintypeclass = $class;
453 if ($dir = core_component
::get_component_directory($parent)) {
454 // BC only - use namespace instead!
455 if (file_exists("$dir/adminlib.php")) {
457 include_once("$dir/adminlib.php");
459 if (class_exists('plugininfo_' . $type)) {
460 $plugintypeclass = 'plugininfo_' . $type;
461 debugging('Class "'.$plugintypeclass.'" is deprecated, migrate to "'.$class.'"', DEBUG_DEVELOPER
);
463 debugging('Subplugin type "'.$type.'" should define class "'.$class.'"', DEBUG_DEVELOPER
);
464 $plugintypeclass = '\core\plugininfo\general';
467 $plugintypeclass = '\core\plugininfo\general';
471 $class = '\core\plugininfo\\' . $type;
472 if (class_exists($class)) {
473 $plugintypeclass = $class;
475 debugging('All standard types including "'.$type.'" should have plugininfo class!', DEBUG_DEVELOPER
);
476 $plugintypeclass = '\core\plugininfo\general';
480 if (!in_array('core\plugininfo\base', class_parents($plugintypeclass))) {
481 throw new coding_exception('Class ' . $plugintypeclass . ' must extend \core\plugininfo\base');
484 return $plugintypeclass;
488 * Returns list of all known subplugins of the given plugin.
490 * For plugins that do not provide subplugins (i.e. there is no support for it),
491 * empty array is returned.
493 * @param string $component full component name, e.g. 'mod_workshop'
494 * @return array (string) component name (e.g. 'workshopallocation_random') => subclass of {@link \core\plugininfo\base}
496 public function get_subplugins_of_plugin($component) {
498 $pluginfo = $this->get_plugin_info($component);
500 if (is_null($pluginfo)) {
504 $subplugins = $this->get_subplugins();
506 if (!isset($subplugins[$pluginfo->component
])) {
512 foreach ($subplugins[$pluginfo->component
] as $subdata) {
513 foreach ($this->get_plugins_of_type($subdata->type
) as $subpluginfo) {
514 $list[$subpluginfo->component
] = $subpluginfo;
522 * Returns list of plugins that define their subplugins and the information
523 * about them from the db/subplugins.json file.
525 * @return array with keys like 'mod_quiz', and values the data from the
526 * corresponding db/subplugins.json file.
528 public function get_subplugins() {
530 if (is_array($this->subpluginsinfo
)) {
531 return $this->subpluginsinfo
;
534 $plugintypes = core_component
::get_plugin_types();
536 $this->subpluginsinfo
= array();
537 foreach (core_component
::get_plugin_types_with_subplugins() as $type => $ignored) {
538 foreach (core_component
::get_plugin_list($type) as $plugin => $componentdir) {
539 $component = $type.'_'.$plugin;
540 $subplugins = core_component
::get_subplugins($component);
544 $this->subpluginsinfo
[$component] = array();
545 foreach ($subplugins as $subplugintype => $ignored) {
546 $subplugin = new stdClass();
547 $subplugin->type
= $subplugintype;
548 $subplugin->typerootdir
= $plugintypes[$subplugintype];
549 $this->subpluginsinfo
[$component][$subplugintype] = $subplugin;
553 return $this->subpluginsinfo
;
557 * Returns the name of the plugin that defines the given subplugin type
559 * If the given subplugin type is not actually a subplugin, returns false.
561 * @param string $subplugintype the name of subplugin type, eg. workshopform or quiz
562 * @return false|string the name of the parent plugin, eg. mod_workshop
564 public function get_parent_of_subplugin($subplugintype) {
565 $parent = core_component
::get_subtype_parent($subplugintype);
573 * Returns a localized name of a given plugin
575 * @param string $component name of the plugin, eg mod_workshop or auth_ldap
578 public function plugin_name($component) {
580 $pluginfo = $this->get_plugin_info($component);
582 if (is_null($pluginfo)) {
583 throw new moodle_exception('err_unknown_plugin', 'core_plugin', '', array('plugin' => $component));
586 return $pluginfo->displayname
;
590 * Returns a localized name of a plugin typed in singular form
592 * Most plugin types define their names in core_plugin lang file. In case of subplugins,
593 * we try to ask the parent plugin for the name. In the worst case, we will return
594 * the value of the passed $type parameter.
596 * @param string $type the type of the plugin, e.g. mod or workshopform
599 public function plugintype_name($type) {
601 if (get_string_manager()->string_exists('type_' . $type, 'core_plugin')) {
602 // For most plugin types, their names are defined in core_plugin lang file.
603 return get_string('type_' . $type, 'core_plugin');
605 } else if ($parent = $this->get_parent_of_subplugin($type)) {
606 // If this is a subplugin, try to ask the parent plugin for the name.
607 if (get_string_manager()->string_exists('subplugintype_' . $type, $parent)) {
608 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type, $parent);
610 return $this->plugin_name($parent) . ' / ' . $type;
619 * Returns a localized name of a plugin type in plural form
621 * Most plugin types define their names in core_plugin lang file. In case of subplugins,
622 * we try to ask the parent plugin for the name. In the worst case, we will return
623 * the value of the passed $type parameter.
625 * @param string $type the type of the plugin, e.g. mod or workshopform
628 public function plugintype_name_plural($type) {
630 if (get_string_manager()->string_exists('type_' . $type . '_plural', 'core_plugin')) {
631 // For most plugin types, their names are defined in core_plugin lang file.
632 return get_string('type_' . $type . '_plural', 'core_plugin');
634 } else if ($parent = $this->get_parent_of_subplugin($type)) {
635 // If this is a subplugin, try to ask the parent plugin for the name.
636 if (get_string_manager()->string_exists('subplugintype_' . $type . '_plural', $parent)) {
637 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type . '_plural', $parent);
639 return $this->plugin_name($parent) . ' / ' . $type;
648 * Returns information about the known plugin, or null
650 * @param string $component frankenstyle component name.
651 * @return \core\plugininfo\base|null the corresponding plugin information.
653 public function get_plugin_info($component) {
654 list($type, $name) = core_component
::normalize_component($component);
655 $plugins = $this->get_plugins_of_type($type);
656 if (isset($plugins[$name])) {
657 return $plugins[$name];
664 * Check to see if the current version of the plugin seems to be a checkout of an external repository.
666 * @param string $component frankenstyle component name
667 * @return false|string
669 public function plugin_external_source($component) {
671 $plugininfo = $this->get_plugin_info($component);
673 if (is_null($plugininfo)) {
677 $pluginroot = $plugininfo->rootdir
;
679 if (is_dir($pluginroot.'/.git')) {
683 if (is_file($pluginroot.'/.git')) {
684 return 'git-submodule';
687 if (is_dir($pluginroot.'/CVS')) {
691 if (is_dir($pluginroot.'/.svn')) {
695 if (is_dir($pluginroot.'/.hg')) {
703 * Get a list of any other plugins that require this one.
704 * @param string $component frankenstyle component name.
705 * @return array of frankensyle component names that require this one.
707 public function other_plugins_that_require($component) {
709 foreach ($this->get_plugins() as $type => $plugins) {
710 foreach ($plugins as $plugin) {
711 $required = $plugin->get_other_required_plugins();
712 if (isset($required[$component])) {
713 $others[] = $plugin->component
;
721 * Check a dependencies list against the list of installed plugins.
722 * @param array $dependencies compenent name to required version or ANY_VERSION.
723 * @return bool true if all the dependencies are satisfied.
725 public function are_dependencies_satisfied($dependencies) {
726 foreach ($dependencies as $component => $requiredversion) {
727 $otherplugin = $this->get_plugin_info($component);
728 if (is_null($otherplugin)) {
732 if ($requiredversion != ANY_VERSION
and $otherplugin->versiondisk
< $requiredversion) {
741 * Checks all dependencies for all installed plugins
743 * This is used by install and upgrade. The array passed by reference as the second
744 * argument is populated with the list of plugins that have failed dependencies (note that
745 * a single plugin can appear multiple times in the $failedplugins).
747 * @param int $moodleversion the version from version.php.
748 * @param array $failedplugins to return the list of plugins with non-satisfied dependencies
749 * @param int $branch the current moodle branch, null if not provided
750 * @return bool true if all the dependencies are satisfied for all plugins.
752 public function all_plugins_ok($moodleversion, &$failedplugins = array(), $branch = null) {
754 if (empty($branch)) {
755 $branch = $CFG->branch ??
'';
756 if (empty($branch)) {
757 // During initial install there is no branch set.
758 require($CFG->dirroot
. '/version.php');
759 $branch = (int)$branch;
760 // Force CFG->branch to int value during install.
761 $CFG->branch
= $branch;
765 foreach ($this->get_plugins() as $type => $plugins) {
766 foreach ($plugins as $plugin) {
768 if (!$plugin->is_core_dependency_satisfied($moodleversion)) {
770 $failedplugins[] = $plugin->component
;
773 if (!$this->are_dependencies_satisfied($plugin->get_other_required_plugins())) {
775 $failedplugins[] = $plugin->component
;
778 if (!$plugin->is_core_compatible_satisfied($branch)) {
780 $failedplugins[] = $plugin->component
;
789 * Resolve requirements and dependencies of a plugin.
791 * Returns an array of objects describing the requirement/dependency,
792 * indexed by the frankenstyle name of the component. The returned array
793 * can be empty. The objects in the array have following properties:
798 * ->(string)availability
800 * @param \core\plugininfo\base $plugin the plugin we are checking
801 * @param null|string|int|double $moodleversion explicit moodle core version to check against, defaults to $CFG->version
802 * @param null|string|int $moodlebranch explicit moodle core branch to check against, defaults to $CFG->branch
803 * @return array of objects
805 public function resolve_requirements(\core\plugininfo\base
$plugin, $moodleversion=null, $moodlebranch=null) {
808 if ($plugin->versiondisk
=== null) {
809 // Missing from disk, we have no version.php to read from.
813 if ($moodleversion === null) {
814 $moodleversion = $CFG->version
;
817 if ($moodlebranch === null) {
818 $moodlebranch = $CFG->branch
;
822 $reqcore = $this->resolve_core_requirements($plugin, $moodleversion, $moodlebranch);
824 if (!empty($reqcore)) {
825 $reqs['core'] = $reqcore;
828 foreach ($plugin->get_other_required_plugins() as $reqplug => $reqver) {
829 $reqs[$reqplug] = $this->resolve_dependency_requirements($plugin, $reqplug, $reqver, $moodlebranch);
836 * Helper method to resolve plugin's requirements on the moodle core.
838 * @param \core\plugininfo\base $plugin the plugin we are checking
839 * @param string|int|double $moodleversion moodle core branch to check against
842 protected function resolve_core_requirements(\core\plugininfo\base
$plugin, $moodleversion, $moodlebranch) {
844 $reqs = (object)array(
848 'availability' => null,
850 $reqs->hasver
= $moodleversion;
852 if (empty($plugin->versionrequires
)) {
853 $reqs->reqver
= ANY_VERSION
;
855 $reqs->reqver
= $plugin->versionrequires
;
858 if ($plugin->is_core_dependency_satisfied($moodleversion)) {
859 $reqs->status
= self
::REQUIREMENT_STATUS_OK
;
861 $reqs->status
= self
::REQUIREMENT_STATUS_OUTDATED
;
864 // Now check if there is an explicit incompatible, supersedes requires.
865 if (isset($plugin->pluginincompatible
) && $plugin->pluginincompatible
!= null) {
866 if (!$plugin->is_core_compatible_satisfied($moodlebranch)) {
868 $reqs->status
= self
::REQUIREMENT_STATUS_NEWER
;
876 * Helper method to resolve plugin's dependecies on other plugins.
878 * @param \core\plugininfo\base $plugin the plugin we are checking
879 * @param string $otherpluginname
880 * @param string|int $requiredversion
881 * @param string|int $moodlebranch explicit moodle core branch to check against, defaults to $CFG->branch
884 protected function resolve_dependency_requirements(\core\plugininfo\base
$plugin, $otherpluginname,
885 $requiredversion, $moodlebranch) {
887 $reqs = (object)array(
891 'availability' => null,
894 $otherplugin = $this->get_plugin_info($otherpluginname);
896 if ($otherplugin !== null) {
897 // The required plugin is installed.
898 $reqs->hasver
= $otherplugin->versiondisk
;
899 $reqs->reqver
= $requiredversion;
900 // Check it has sufficient version.
901 if ($requiredversion == ANY_VERSION
or $otherplugin->versiondisk
>= $requiredversion) {
902 $reqs->status
= self
::REQUIREMENT_STATUS_OK
;
904 $reqs->status
= self
::REQUIREMENT_STATUS_OUTDATED
;
908 // The required plugin is not installed.
909 $reqs->hasver
= null;
910 $reqs->reqver
= $requiredversion;
911 $reqs->status
= self
::REQUIREMENT_STATUS_MISSING
;
914 if ($reqs->status
!== self
::REQUIREMENT_STATUS_OK
) {
915 if ($this->is_remote_plugin_available($otherpluginname, $requiredversion, false)) {
916 $reqs->availability
= self
::REQUIREMENT_AVAILABLE
;
918 $reqs->availability
= self
::REQUIREMENT_UNAVAILABLE
;
926 * Helper method to determine whether a moodle version is explicitly supported.
928 * @param \core\plugininfo\base $plugin the plugin we are checking
929 * @param int $branch the moodle branch to check support for
932 public function check_explicitly_supported($plugin, $branch) : string {
933 // Check for correctly formed supported.
934 if (isset($plugin->pluginsupported
)) {
935 // Broken apart for readability.
937 if (!is_array($plugin->pluginsupported
)) {
940 if (!is_int($plugin->pluginsupported
[0]) ||
!is_int($plugin->pluginsupported
[1])) {
943 if (count($plugin->pluginsupported
) != 2) {
947 throw new coding_exception(get_string('err_supported_syntax', 'core_plugin'));
951 if (isset($plugin->pluginsupported
) && $plugin->pluginsupported
!= null) {
952 if ($plugin->pluginsupported
[0] <= $branch && $branch <= $plugin->pluginsupported
[1]) {
953 return self
::VERSION_SUPPORTED
;
955 return self
::VERSION_NOT_SUPPORTED
;
958 // If supports aren't specified, but incompatible is, return not supported if not incompatible.
959 if (!isset($plugin->pluginsupported
) && isset($plugin->pluginincompatible
) && !empty($plugin->pluginincompatible
)) {
960 if (!$plugin->is_core_compatible_satisfied($branch)) {
961 return self
::VERSION_NOT_SUPPORTED
;
964 return self
::VERSION_NO_SUPPORTS
;
969 * Is the given plugin version available in the plugins directory?
971 * See {@link self::get_remote_plugin_info()} for the full explanation of how the $version
972 * parameter is interpretted.
974 * @param string $component plugin frankenstyle name
975 * @param string|int $version ANY_VERSION or the version number
976 * @param bool $exactmatch false if "given version or higher" is requested
979 public function is_remote_plugin_available($component, $version, $exactmatch) {
981 $info = $this->get_remote_plugin_info($component, $version, $exactmatch);
984 // There is no available plugin of that name.
988 if (empty($info->version
)) {
989 // Plugin is known, but no suitable version was found.
997 * Can the given plugin version be installed via the admin UI?
999 * This check should be used whenever attempting to install a plugin from
1000 * the plugins directory (new install, available update, missing dependency).
1002 * @param string $component
1003 * @param int $version version number
1004 * @param string $reason returned code of the reason why it is not
1005 * @param bool $checkremote check this version availability on moodle server
1008 public function is_remote_plugin_installable($component, $version, &$reason = null, $checkremote = true) {
1011 // Make sure the feature is not disabled.
1012 if (!empty($CFG->disableupdateautodeploy
)) {
1013 $reason = 'disabled';
1017 // Make sure the version is available.
1018 if ($checkremote && !$this->is_remote_plugin_available($component, $version, true)) {
1019 $reason = 'remoteunavailable';
1023 // Make sure the plugin type root directory is writable.
1024 list($plugintype, $pluginname) = core_component
::normalize_component($component);
1025 if (!$this->is_plugintype_writable($plugintype)) {
1026 $reason = 'notwritableplugintype';
1030 if (!$checkremote) {
1031 $remoteversion = $version;
1033 $remoteinfo = $this->get_remote_plugin_info($component, $version, true);
1034 $remoteversion = $remoteinfo->version
->version
;
1036 $localinfo = $this->get_plugin_info($component);
1039 // If the plugin is already present, prevent downgrade.
1040 if ($localinfo->versiondb
> $remoteversion) {
1041 $reason = 'cannotdowngrade';
1045 // Make sure we have write access to all the existing code.
1046 if (is_dir($localinfo->rootdir
)) {
1047 if (!$this->is_plugin_folder_removable($component)) {
1048 $reason = 'notwritableplugin';
1054 // Looks like it could work.
1059 * Given the list of remote plugin infos, return just those installable.
1061 * This is typically used on lists returned by
1062 * {@link self::available_updates()} or {@link self::missing_dependencies()}
1063 * to perform bulk installation of remote plugins.
1065 * @param array $remoteinfos list of {@link \core\update\remote_info}
1068 public function filter_installable($remoteinfos) {
1071 if (!empty($CFG->disableupdateautodeploy
)) {
1074 if (empty($remoteinfos)) {
1077 $installable = array();
1078 foreach ($remoteinfos as $index => $remoteinfo) {
1079 if ($this->is_remote_plugin_installable($remoteinfo->component
, $remoteinfo->version
->version
)) {
1080 $installable[$index] = $remoteinfo;
1083 return $installable;
1087 * Returns information about a plugin in the plugins directory.
1089 * This is typically used when checking for available dependencies (in
1090 * which case the $version represents minimal version we need), or
1091 * when installing an available update or a new plugin from the plugins
1092 * directory (in which case the $version is exact version we are
1093 * interested in). The interpretation of the $version is controlled
1094 * by the $exactmatch argument.
1096 * If a plugin with the given component name is found, data about the
1097 * plugin are returned as an object. The ->version property of the object
1098 * contains the information about the particular plugin version that
1099 * matches best the given critera. The ->version property is false if no
1100 * suitable version of the plugin was found (yet the plugin itself is
1103 * See {@link \core\update\api::validate_pluginfo_format()} for the
1104 * returned data structure.
1106 * @param string $component plugin frankenstyle name
1107 * @param string|int $version ANY_VERSION or the version number
1108 * @param bool $exactmatch false if "given version or higher" is requested
1109 * @return \core\update\remote_info|bool
1111 public function get_remote_plugin_info($component, $version, $exactmatch) {
1113 if ($exactmatch and $version == ANY_VERSION
) {
1114 throw new coding_exception('Invalid request for exactly any version, it does not make sense.');
1117 $client = $this->get_update_api_client();
1120 // Use client's get_plugin_info() method.
1121 if (!isset($this->remotepluginsinfoexact
[$component][$version])) {
1122 $this->remotepluginsinfoexact
[$component][$version] = $client->get_plugin_info($component, $version);
1124 return $this->remotepluginsinfoexact
[$component][$version];
1127 // Use client's find_plugin() method.
1128 if (!isset($this->remotepluginsinfoatleast
[$component][$version])) {
1129 $this->remotepluginsinfoatleast
[$component][$version] = $client->find_plugin($component, $version);
1131 return $this->remotepluginsinfoatleast
[$component][$version];
1136 * Obtain the plugin ZIP file from the given URL
1138 * The caller is supposed to know both downloads URL and the MD5 hash of
1139 * the ZIP contents in advance, typically by using the API requests against
1140 * the plugins directory.
1142 * @param string $url
1143 * @param string $md5
1144 * @return string|bool full path to the file, false on error
1146 public function get_remote_plugin_zip($url, $md5) {
1149 if (!empty($CFG->disableupdateautodeploy
)) {
1152 return $this->get_code_manager()->get_remote_plugin_zip($url, $md5);
1156 * Extracts the saved plugin ZIP file.
1158 * Returns the list of files found in the ZIP. The format of that list is
1159 * array of (string)filerelpath => (bool|string) where the array value is
1160 * either true or a string describing the problematic file.
1162 * @see zip_packer::extract_to_pathname()
1163 * @param string $zipfilepath full path to the saved ZIP file
1164 * @param string $targetdir full path to the directory to extract the ZIP file to
1165 * @param string $rootdir explicitly rename the root directory of the ZIP into this non-empty value
1166 * @return array list of extracted files as returned by {@link zip_packer::extract_to_pathname()}
1168 public function unzip_plugin_file($zipfilepath, $targetdir, $rootdir = '') {
1169 return $this->get_code_manager()->unzip_plugin_file($zipfilepath, $targetdir, $rootdir);
1173 * Detects the plugin's name from its ZIP file.
1175 * Plugin ZIP packages are expected to contain a single directory and the
1176 * directory name would become the plugin name once extracted to the Moodle
1179 * @param string $zipfilepath full path to the ZIP files
1180 * @return string|bool false on error
1182 public function get_plugin_zip_root_dir($zipfilepath) {
1183 return $this->get_code_manager()->get_plugin_zip_root_dir($zipfilepath);
1187 * Return a list of missing dependencies.
1189 * This should provide the full list of plugins that should be installed to
1190 * fulfill the requirements of all plugins, if possible.
1192 * @param bool $availableonly return only available missing dependencies
1193 * @return array of \core\update\remote_info|bool indexed by the component name
1195 public function missing_dependencies($availableonly=false) {
1197 $dependencies = array();
1199 foreach ($this->get_plugins() as $plugintype => $pluginfos) {
1200 foreach ($pluginfos as $pluginname => $pluginfo) {
1201 foreach ($this->resolve_requirements($pluginfo) as $reqname => $reqinfo) {
1202 if ($reqname === 'core') {
1205 if ($reqinfo->status
!= self
::REQUIREMENT_STATUS_OK
) {
1206 if ($reqinfo->availability
== self
::REQUIREMENT_AVAILABLE
) {
1207 $remoteinfo = $this->get_remote_plugin_info($reqname, $reqinfo->reqver
, false);
1209 if (empty($dependencies[$reqname])) {
1210 $dependencies[$reqname] = $remoteinfo;
1212 // If resolving requirements has led to two different versions of the same
1213 // remote plugin, pick the higher version. This can happen in cases like one
1214 // plugin requiring ANY_VERSION and another plugin requiring specific higher
1215 // version with lower maturity of a remote plugin.
1216 if ($remoteinfo->version
->version
> $dependencies[$reqname]->version
->version
) {
1217 $dependencies[$reqname] = $remoteinfo;
1222 if (!isset($dependencies[$reqname])) {
1223 // Unable to find a plugin fulfilling the requirements.
1224 $dependencies[$reqname] = false;
1232 if ($availableonly) {
1233 foreach ($dependencies as $component => $info) {
1234 if (empty($info) or empty($info->version
)) {
1235 unset($dependencies[$component]);
1240 return $dependencies;
1244 * Is it possible to uninstall the given plugin?
1246 * False is returned if the plugininfo subclass declares the uninstall should
1247 * not be allowed via {@link \core\plugininfo\base::is_uninstall_allowed()} or if the
1248 * core vetoes it (e.g. becase the plugin or some of its subplugins is required
1249 * by some other installed plugin).
1251 * @param string $component full frankenstyle name, e.g. mod_foobar
1254 public function can_uninstall_plugin($component) {
1256 $pluginfo = $this->get_plugin_info($component);
1258 if (is_null($pluginfo)) {
1262 if (!$this->common_uninstall_check($pluginfo)) {
1266 // Verify only if something else requires the subplugins, do not verify their common_uninstall_check()!
1267 $subplugins = $this->get_subplugins_of_plugin($pluginfo->component
);
1268 foreach ($subplugins as $subpluginfo) {
1269 // Check if there are some other plugins requiring this subplugin
1270 // (but the parent and siblings).
1271 foreach ($this->other_plugins_that_require($subpluginfo->component
) as $requiresme) {
1272 $ismyparent = ($pluginfo->component
=== $requiresme);
1273 $ismysibling = in_array($requiresme, array_keys($subplugins));
1274 if (!$ismyparent and !$ismysibling) {
1280 // Check if there are some other plugins requiring this plugin
1281 // (but its subplugins).
1282 foreach ($this->other_plugins_that_require($pluginfo->component
) as $requiresme) {
1283 $ismysubplugin = in_array($requiresme, array_keys($subplugins));
1284 if (!$ismysubplugin) {
1293 * Perform the installation of plugins.
1295 * If used for installation of remote plugins from the Moodle Plugins
1296 * directory, the $plugins must be list of {@link \core\update\remote_info}
1297 * object that represent installable remote plugins. The caller can use
1298 * {@link self::filter_installable()} to prepare the list.
1300 * If used for installation of plugins from locally available ZIP files,
1301 * the $plugins should be list of objects with properties ->component and
1304 * The method uses {@link mtrace()} to produce direct output and can be
1305 * used in both web and cli interfaces.
1307 * @param array $plugins list of plugins
1308 * @param bool $confirmed should the files be really deployed into the dirroot?
1309 * @param bool $silent perform without output
1310 * @return bool true on success
1312 public function install_plugins(array $plugins, $confirmed, $silent) {
1313 global $CFG, $OUTPUT;
1315 if (!empty($CFG->disableupdateautodeploy
)) {
1319 if (empty($plugins)) {
1323 $ok = get_string('statusok', 'core');
1325 // Let admins know they can expect more verbose output.
1326 $silent or $this->mtrace(get_string('packagesdebug', 'core_plugin'), PHP_EOL
, DEBUG_NORMAL
);
1328 // Download all ZIP packages if we do not have them yet.
1330 foreach ($plugins as $plugin) {
1331 if ($plugin instanceof \core\update\remote_info
) {
1332 $zips[$plugin->component
] = $this->get_remote_plugin_zip($plugin->version
->downloadurl
,
1333 $plugin->version
->downloadmd5
);
1334 $silent or $this->mtrace(get_string('packagesdownloading', 'core_plugin', $plugin->component
), ' ... ');
1335 $silent or $this->mtrace(PHP_EOL
.' <- '.$plugin->version
->downloadurl
, '', DEBUG_DEVELOPER
);
1336 $silent or $this->mtrace(PHP_EOL
.' -> '.$zips[$plugin->component
], ' ... ', DEBUG_DEVELOPER
);
1337 if (!$zips[$plugin->component
]) {
1338 $silent or $this->mtrace(get_string('error'));
1341 $silent or $this->mtrace($ok);
1343 if (empty($plugin->zipfilepath
)) {
1344 throw new coding_exception('Unexpected data structure provided');
1346 $zips[$plugin->component
] = $plugin->zipfilepath
;
1347 $silent or $this->mtrace('ZIP '.$plugin->zipfilepath
, PHP_EOL
, DEBUG_DEVELOPER
);
1351 // Validate all downloaded packages.
1352 foreach ($plugins as $plugin) {
1353 $zipfile = $zips[$plugin->component
];
1354 $silent or $this->mtrace(get_string('packagesvalidating', 'core_plugin', $plugin->component
), ' ... ');
1355 list($plugintype, $pluginname) = core_component
::normalize_component($plugin->component
);
1356 $tmp = make_request_directory();
1357 $zipcontents = $this->unzip_plugin_file($zipfile, $tmp, $pluginname);
1358 if (empty($zipcontents)) {
1359 $silent or $this->mtrace(get_string('error'));
1360 $silent or $this->mtrace('Unable to unzip '.$zipfile, PHP_EOL
, DEBUG_DEVELOPER
);
1364 $validator = \core\update\validator
::instance($tmp, $zipcontents);
1365 $validator->assert_plugin_type($plugintype);
1366 $validator->assert_moodle_version($CFG->version
);
1367 // TODO Check for missing dependencies during validation.
1368 $result = $validator->execute();
1370 $result ?
$this->mtrace($ok) : $this->mtrace(get_string('error'));
1371 foreach ($validator->get_messages() as $message) {
1372 if ($message->level
=== $validator::INFO
) {
1373 // Display [OK] validation messages only if debugging mode is DEBUG_NORMAL.
1374 $level = DEBUG_NORMAL
;
1375 } else if ($message->level
=== $validator::DEBUG
) {
1376 // Display [Debug] validation messages only if debugging mode is DEBUG_ALL.
1379 // Display [Warning] and [Error] always.
1382 if ($message->level
=== $validator::WARNING
and !CLI_SCRIPT
) {
1383 $this->mtrace(' <strong>['.$validator->message_level_name($message->level
).']</strong>', ' ', $level);
1385 $this->mtrace(' ['.$validator->message_level_name($message->level
).']', ' ', $level);
1387 $this->mtrace($validator->message_code_name($message->msgcode
), ' ', $level);
1388 $info = $validator->message_code_info($message->msgcode
, $message->addinfo
);
1390 $this->mtrace('['.s($info).']', ' ', $level);
1391 } else if (is_string($message->addinfo
)) {
1392 $this->mtrace('['.s($message->addinfo
, true).']', ' ', $level);
1394 $this->mtrace('['.s(json_encode($message->addinfo
, true)).']', ' ', $level);
1396 if ($icon = $validator->message_help_icon($message->msgcode
)) {
1398 $this->mtrace(PHP_EOL
.' ^^^ '.get_string('help').': '.
1399 get_string($icon->identifier
.'_help', $icon->component
), '', $level);
1401 $this->mtrace($OUTPUT->render($icon), ' ', $level);
1404 $this->mtrace(PHP_EOL
, '', $level);
1408 $silent or $this->mtrace(get_string('packagesvalidatingfailed', 'core_plugin'));
1412 $silent or $this->mtrace(PHP_EOL
.get_string('packagesvalidatingok', 'core_plugin'));
1418 // Extract all ZIP packs do the dirroot.
1419 foreach ($plugins as $plugin) {
1420 $silent or $this->mtrace(get_string('packagesextracting', 'core_plugin', $plugin->component
), ' ... ');
1421 $zipfile = $zips[$plugin->component
];
1422 list($plugintype, $pluginname) = core_component
::normalize_component($plugin->component
);
1423 $target = $this->get_plugintype_root($plugintype);
1424 if (file_exists($target.'/'.$pluginname)) {
1425 $this->remove_plugin_folder($this->get_plugin_info($plugin->component
));
1427 if (!$this->unzip_plugin_file($zipfile, $target, $pluginname)) {
1428 $silent or $this->mtrace(get_string('error'));
1429 $silent or $this->mtrace('Unable to unzip '.$zipfile, PHP_EOL
, DEBUG_DEVELOPER
);
1430 if (function_exists('opcache_reset')) {
1435 $silent or $this->mtrace($ok);
1437 if (function_exists('opcache_reset')) {
1445 * Outputs the given message via {@link mtrace()}.
1447 * If $debug is provided, then the message is displayed only at the given
1448 * debugging level (e.g. DEBUG_DEVELOPER to display the message only if the
1449 * site has developer debugging level selected).
1451 * @param string $msg message
1452 * @param string $eol end of line
1453 * @param null|int $debug null to display always, int only on given debug level
1455 protected function mtrace($msg, $eol=PHP_EOL
, $debug=null) {
1458 if ($debug !== null and !debugging(null, $debug)) {
1466 * Returns uninstall URL if exists.
1468 * @param string $component
1469 * @param string $return either 'overview' or 'manage'
1470 * @return moodle_url uninstall URL, null if uninstall not supported
1472 public function get_uninstall_url($component, $return = 'overview') {
1473 if (!$this->can_uninstall_plugin($component)) {
1477 $pluginfo = $this->get_plugin_info($component);
1479 if (is_null($pluginfo)) {
1483 if (method_exists($pluginfo, 'get_uninstall_url')) {
1484 debugging('plugininfo method get_uninstall_url() is deprecated, all plugins should be uninstalled via standard URL only.');
1485 return $pluginfo->get_uninstall_url($return);
1488 return $pluginfo->get_default_uninstall_url($return);
1492 * Uninstall the given plugin.
1494 * Automatically cleans-up all remaining configuration data, log records, events,
1495 * files from the file pool etc.
1497 * In the future, the functionality of {@link uninstall_plugin()} function may be moved
1498 * into this method and all the code should be refactored to use it. At the moment, we
1499 * mimic this future behaviour by wrapping that function call.
1501 * @param string $component
1502 * @param progress_trace $progress traces the process
1503 * @return bool true on success, false on errors/problems
1505 public function uninstall_plugin($component, progress_trace
$progress) {
1507 $pluginfo = $this->get_plugin_info($component);
1509 if (is_null($pluginfo)) {
1513 // Give the pluginfo class a chance to execute some steps.
1514 $result = $pluginfo->uninstall($progress);
1519 // Call the legacy core function to uninstall the plugin.
1521 uninstall_plugin($pluginfo->type
, $pluginfo->name
);
1522 $progress->output(ob_get_clean());
1528 * Checks if there are some plugins with a known available update
1530 * @return bool true if there is at least one available update
1532 public function some_plugins_updatable() {
1533 foreach ($this->get_plugins() as $type => $plugins) {
1534 foreach ($plugins as $plugin) {
1535 if ($plugin->available_updates()) {
1545 * Returns list of available updates for the given component.
1547 * This method should be considered as internal API and is supposed to be
1548 * called by {@link \core\plugininfo\base::available_updates()} only
1549 * to lazy load the data once they are first requested.
1551 * @param string $component frankenstyle name of the plugin
1552 * @return null|array array of \core\update\info objects or null
1554 public function load_available_updates_for_plugin($component) {
1557 $provider = \core\update\checker
::instance();
1559 if (!$provider->enabled() or during_initial_install()) {
1563 if (isset($CFG->updateminmaturity
)) {
1564 $minmaturity = $CFG->updateminmaturity
;
1566 // This can happen during the very first upgrade to 2.3.
1567 $minmaturity = MATURITY_STABLE
;
1570 return $provider->get_update_info($component, array('minmaturity' => $minmaturity));
1574 * Returns a list of all available updates to be installed.
1576 * This is used when "update all plugins" action is performed at the
1577 * administration UI screen.
1579 * Returns array of remote info objects indexed by the plugin
1580 * component. If there are multiple updates available (typically a mix of
1581 * stable and non-stable ones), we pick the most mature most recent one.
1583 * Plugins without explicit maturity are considered more mature than
1584 * release candidates but less mature than explicit stable (this should be
1585 * pretty rare case).
1587 * @return array (string)component => (\core\update\remote_info)remoteinfo
1589 public function available_updates() {
1593 foreach ($this->get_plugins() as $type => $plugins) {
1594 foreach ($plugins as $plugin) {
1595 $availableupdates = $plugin->available_updates();
1596 if (empty($availableupdates)) {
1599 foreach ($availableupdates as $update) {
1600 if (empty($updates[$plugin->component
])) {
1601 $updates[$plugin->component
] = $update;
1604 $maturitycurrent = $updates[$plugin->component
]->maturity
;
1605 if (empty($maturitycurrent)) {
1606 $maturitycurrent = MATURITY_STABLE
- 25;
1608 $maturityremote = $update->maturity
;
1609 if (empty($maturityremote)) {
1610 $maturityremote = MATURITY_STABLE
- 25;
1612 if ($maturityremote < $maturitycurrent) {
1615 if ($maturityremote > $maturitycurrent) {
1616 $updates[$plugin->component
] = $update;
1619 if ($update->version
> $updates[$plugin->component
]->version
) {
1620 $updates[$plugin->component
] = $update;
1627 foreach ($updates as $component => $update) {
1628 $remoteinfo = $this->get_remote_plugin_info($component, $update->version
, true);
1629 if (empty($remoteinfo) or empty($remoteinfo->version
)) {
1630 unset($updates[$component]);
1632 $updates[$component] = $remoteinfo;
1640 * Check to see if the given plugin folder can be removed by the web server process.
1642 * @param string $component full frankenstyle component
1645 public function is_plugin_folder_removable($component) {
1647 $pluginfo = $this->get_plugin_info($component);
1649 if (is_null($pluginfo)) {
1653 // To be able to remove the plugin folder, its parent must be writable, too.
1654 if (!is_writable(dirname($pluginfo->rootdir
))) {
1658 // Check that the folder and all its content is writable (thence removable).
1659 return $this->is_directory_removable($pluginfo->rootdir
);
1663 * Is it possible to create a new plugin directory for the given plugin type?
1665 * @throws coding_exception for invalid plugin types or non-existing plugin type locations
1666 * @param string $plugintype
1669 public function is_plugintype_writable($plugintype) {
1671 $plugintypepath = $this->get_plugintype_root($plugintype);
1673 if (is_null($plugintypepath)) {
1674 throw new coding_exception('Unknown plugin type: '.$plugintype);
1677 if ($plugintypepath === false) {
1678 throw new coding_exception('Plugin type location does not exist: '.$plugintype);
1681 return is_writable($plugintypepath);
1685 * Returns the full path of the root of the given plugin type
1687 * Null is returned if the plugin type is not known. False is returned if
1688 * the plugin type root is expected but not found. Otherwise, string is
1691 * @param string $plugintype
1692 * @return string|bool|null
1694 public function get_plugintype_root($plugintype) {
1696 $plugintypepath = null;
1697 foreach (core_component
::get_plugin_types() as $type => $fullpath) {
1698 if ($type === $plugintype) {
1699 $plugintypepath = $fullpath;
1703 if (is_null($plugintypepath)) {
1706 if (!is_dir($plugintypepath)) {
1710 return $plugintypepath;
1714 * Defines a list of all plugins that were originally shipped in the standard Moodle distribution,
1715 * but are not anymore and are deleted during upgrades.
1717 * The main purpose of this list is to hide missing plugins during upgrade.
1719 * @param string $type plugin type
1720 * @param string $name plugin name
1723 public static function is_deleted_standard_plugin($type, $name) {
1724 // Do not include plugins that were removed during upgrades to versions that are
1725 // not supported as source versions for upgrade any more. For example, at MOODLE_23_STABLE
1726 // branch, listed should be no plugins that were removed at 1.9.x - 2.1.x versions as
1727 // Moodle 2.3 supports upgrades from 2.2.x only.
1729 'qformat' => array('blackboard', 'learnwise', 'examview'),
1730 'auth' => array('radius', 'fc', 'nntp', 'pam', 'pop3', 'imap'),
1731 'block' => array('course_overview', 'messages', 'community', 'participants', 'quiz_results'),
1732 'cachestore' => array('memcache'),
1733 'enrol' => array('authorize'),
1734 'filter' => array('censor'),
1735 'media' => array('swf'),
1736 'portfolio' => array('picasa', 'boxnet'),
1737 'qformat' => array('webct'),
1738 'message' => array('jabber'),
1739 'quizaccess' => array('safebrowser'),
1740 'report' => array('search'),
1741 'repository' => array('alfresco', 'picasa', 'skydrive', 'boxnet'),
1742 'tinymce' => array('dragmath'),
1743 'tool' => array('bloglevelupgrade', 'qeupgradehelper', 'timezoneimport', 'assignmentupgrade', 'health'),
1744 'theme' => array('bootstrapbase', 'clean', 'more', 'afterburner', 'anomaly', 'arialist', 'base',
1745 'binarius', 'boxxie', 'brick', 'canvas', 'formal_white', 'formfactor', 'fusion', 'leatherbound',
1746 'magazine', 'mymobile', 'nimble', 'nonzero', 'overlay', 'serenity', 'sky_high', 'splash',
1747 'standard', 'standardold'),
1748 'webservice' => array('amf'),
1751 if (!isset($plugins[$type])) {
1754 return in_array($name, $plugins[$type]);
1758 * Defines a white list of all plugins shipped in the standard Moodle distribution
1760 * @param string $type
1761 * @return false|array array of standard plugins or false if the type is unknown
1763 public static function standard_plugins_list($type) {
1765 $standard_plugins = array(
1767 'antivirus' => array(
1772 'accessibilitychecker', 'accessibilityhelper', 'align',
1773 'backcolor', 'bold', 'charmap', 'clear', 'collapse', 'emoticon',
1774 'equation', 'fontcolor', 'html', 'image', 'indent', 'italic',
1775 'link', 'managefiles', 'media', 'noautolink', 'orderedlist',
1776 'recordrtc', 'rtl', 'strike', 'subscript', 'superscript', 'table',
1777 'title', 'underline', 'undo', 'unorderedlist', 'h5p', 'emojipicker',
1780 'assignment' => array(
1781 'offline', 'online', 'upload', 'uploadsingle'
1784 'assignsubmission' => array(
1785 'comments', 'file', 'onlinetext'
1788 'assignfeedback' => array(
1789 'comments', 'file', 'offline', 'editpdf'
1793 'cas', 'db', 'email', 'ldap', 'lti', 'manual', 'mnet',
1794 'nologin', 'none', 'oauth2', 'shibboleth', 'webservice'
1797 'availability' => array(
1798 'completion', 'date', 'grade', 'group', 'grouping', 'profile'
1802 'accessreview', 'activity_modules', 'activity_results', 'admin_bookmarks', 'badges',
1803 'blog_menu', 'blog_recent', 'blog_tags', 'calendar_month',
1804 'calendar_upcoming', 'comments',
1805 'completionstatus', 'course_list', 'course_summary',
1806 'feedback', 'globalsearch', 'glossary_random', 'html',
1807 'login', 'lp', 'mentees', 'mnet_hosts', 'myoverview', 'myprofile',
1808 'navigation', 'news_items', 'online_users',
1809 'private_files', 'recent_activity', 'recentlyaccesseditems',
1810 'recentlyaccessedcourses', 'rss_client', 'search_forums', 'section_links',
1811 'selfcompletion', 'settings', 'site_main_menu',
1812 'social_activities', 'starredcourses', 'tag_flickr', 'tag_youtube', 'tags', 'timeline'
1815 'booktool' => array(
1816 'exportimscp', 'importhtml', 'print'
1819 'cachelock' => array(
1823 'cachestore' => array(
1824 'file', 'memcached', 'mongodb', 'session', 'static', 'apcu', 'redis'
1827 'calendartype' => array(
1831 'contenttype' => array(
1835 'customfield' => array(
1836 'checkbox', 'date', 'select', 'text', 'textarea'
1839 'coursereport' => array(
1843 'datafield' => array(
1844 'checkbox', 'date', 'file', 'latlong', 'menu', 'multimenu',
1845 'number', 'picture', 'radiobutton', 'text', 'textarea', 'url'
1848 'dataformat' => array(
1849 'html', 'csv', 'json', 'excel', 'ods', 'pdf',
1852 'datapreset' => array(
1856 'fileconverter' => array(
1857 'unoconv', 'googledrive'
1861 'atto', 'textarea', 'tinymce'
1865 'category', 'cohort', 'database', 'flatfile',
1866 'guest', 'imsenterprise', 'ldap', 'lti', 'manual', 'meta', 'mnet',
1867 'paypal', 'self', 'fee',
1871 'activitynames', 'algebra', 'emailprotect',
1872 'emoticon', 'displayh5p', 'mathjaxloader', 'mediaplugin', 'multilang', 'tex', 'tidy',
1873 'urltolink', 'data', 'glossary'
1877 'singleactivity', 'social', 'topics', 'weeks'
1880 'forumreport' => array(
1884 'gradeexport' => array(
1885 'ods', 'txt', 'xls', 'xml'
1888 'gradeimport' => array(
1889 'csv', 'direct', 'xml'
1892 'gradereport' => array(
1893 'grader', 'history', 'outcomes', 'overview', 'user', 'singleview'
1896 'gradingform' => array(
1907 'logstore' => array(
1908 'database', 'legacy', 'standard',
1911 'ltiservice' => array(
1912 'gradebookservices', 'memberships', 'profile', 'toolproxy', 'toolsettings', 'basicoutcomes'
1915 'mlbackend' => array(
1920 'html5audio', 'html5video', 'videojs', 'vimeo', 'youtube'
1924 'airnotifier', 'email', 'popup'
1927 'mnetservice' => array(
1932 'assign', 'assignment', 'bigbluebuttonbn', 'book', 'chat', 'choice', 'data', 'feedback', 'folder',
1933 'forum', 'glossary', 'h5pactivity', 'imscp', 'label', 'lesson', 'lti', 'page',
1934 'quiz', 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop'
1941 'plagiarism' => array(
1944 'portfolio' => array(
1945 'download', 'flickr', 'googledocs', 'mahara'
1948 'profilefield' => array(
1949 'checkbox', 'datetime', 'menu', 'social', 'text', 'textarea'
1974 'qbehaviour' => array(
1975 'adaptive', 'adaptivenopenalty', 'deferredcbm',
1976 'deferredfeedback', 'immediatecbm', 'immediatefeedback',
1977 'informationitem', 'interactive', 'interactivecountback',
1978 'manualgraded', 'missing'
1982 'aiken', 'blackboard_six', 'gift',
1983 'missingword', 'multianswer',
1988 'calculated', 'calculatedmulti', 'calculatedsimple',
1989 'ddimageortext', 'ddmarker', 'ddwtos', 'description',
1990 'essay', 'gapselect', 'match', 'missingtype', 'multianswer',
1991 'multichoice', 'numerical', 'random', 'randomsamatch',
1992 'shortanswer', 'truefalse'
1996 'grading', 'overview', 'responses', 'statistics'
1999 'quizaccess' => array(
2000 'delaybetweenattempts', 'ipaddress', 'numattempts', 'offlineattempts', 'openclosedate',
2001 'password', 'seb', 'securewindow', 'timelimit'
2005 'backups', 'competency', 'completion', 'configlog', 'courseoverview', 'eventlist',
2006 'infectedfiles', 'insights', 'log', 'loglive', 'outline', 'participation', 'progress',
2007 'questioninstances', 'security', 'stats', 'status', 'performance', 'usersessions'
2010 'repository' => array(
2011 'areafiles', 'contentbank', 'coursefiles', 'dropbox', 'equella', 'filesystem',
2012 'flickr', 'flickr_public', 'googledocs', 'local', 'merlot', 'nextcloud',
2013 'onedrive', 'recent', 's3', 'upload', 'url', 'user', 'webdav',
2014 'wikimedia', 'youtube'
2021 'scormreport' => array(
2029 'ctrlhelp', 'managefiles', 'moodleemoticon', 'moodleimage',
2030 'moodlemedia', 'moodlenolink', 'pdw', 'spellchecker', 'wrap'
2038 'admin_presets', 'analytics', 'availabilityconditions', 'behat', 'brickfield', 'capability', 'cohortroles',
2039 'componentlibrary', 'customlang', 'dataprivacy', 'dbtransfer', 'filetypes', 'generator', 'httpsreplace', 'innodb',
2040 'installaddon', 'langimport', 'licensemanager', 'log', 'lp', 'lpimportcsv', 'lpmigrate', 'messageinbound',
2041 'mobile', 'moodlenet', 'multilangupgrade', 'monitor', 'oauth2', 'phpunit', 'policy', 'profiling', 'recyclebin',
2042 'replace', 'spamcleaner', 'task', 'templatelibrary', 'uploadcourse', 'uploaduser', 'unsuproles',
2043 'usertours', 'xmldb'
2046 'webservice' => array(
2047 'rest', 'soap', 'xmlrpc'
2050 'workshopallocation' => array(
2051 'manual', 'random', 'scheduled'
2054 'workshopeval' => array(
2058 'workshopform' => array(
2059 'accumulative', 'comments', 'numerrors', 'rubric'
2063 if (isset($standard_plugins[$type])) {
2064 return $standard_plugins[$type];
2071 * Remove the current plugin code from the dirroot.
2073 * If removing the currently installed version (which happens during
2074 * updates), we archive the code so that the upgrade can be cancelled.
2076 * To prevent accidental data-loss, we also archive the existing plugin
2077 * code if cancelling installation of it, so that the developer does not
2078 * loose the only version of their work-in-progress.
2080 * @param \core\plugininfo\base $plugin
2082 public function remove_plugin_folder(\core\plugininfo\base
$plugin) {
2084 if (!$this->is_plugin_folder_removable($plugin->component
)) {
2085 throw new moodle_exception('err_removing_unremovable_folder', 'core_plugin', '',
2086 array('plugin' => $plugin->component
, 'rootdir' => $plugin->rootdir
),
2087 'plugin root folder is not removable as expected');
2090 if ($plugin->get_status() === self
::PLUGIN_STATUS_UPTODATE
or $plugin->get_status() === self
::PLUGIN_STATUS_NEW
) {
2091 $this->archive_plugin_version($plugin);
2094 remove_dir($plugin->rootdir
);
2096 if (function_exists('opcache_reset')) {
2102 * Can the installation of the new plugin be cancelled?
2104 * Subplugins can be cancelled only via their parent plugin, not separately
2105 * (they are considered as implicit requirements if distributed together
2106 * with the main package).
2108 * @param \core\plugininfo\base $plugin
2111 public function can_cancel_plugin_installation(\core\plugininfo\base
$plugin) {
2114 if (!empty($CFG->disableupdateautodeploy
)) {
2118 if (empty($plugin) or $plugin->is_standard() or $plugin->is_subplugin()
2119 or !$this->is_plugin_folder_removable($plugin->component
)) {
2123 if ($plugin->get_status() === self
::PLUGIN_STATUS_NEW
) {
2131 * Can the upgrade of the existing plugin be cancelled?
2133 * Subplugins can be cancelled only via their parent plugin, not separately
2134 * (they are considered as implicit requirements if distributed together
2135 * with the main package).
2137 * @param \core\plugininfo\base $plugin
2140 public function can_cancel_plugin_upgrade(\core\plugininfo\base
$plugin) {
2143 if (!empty($CFG->disableupdateautodeploy
)) {
2144 // Cancelling the plugin upgrade is actually installation of the
2145 // previously archived version.
2149 if (empty($plugin) or $plugin->is_standard() or $plugin->is_subplugin()
2150 or !$this->is_plugin_folder_removable($plugin->component
)) {
2154 if ($plugin->get_status() === self
::PLUGIN_STATUS_UPGRADE
) {
2155 if ($this->get_code_manager()->get_archived_plugin_version($plugin->component
, $plugin->versiondb
)) {
2164 * Removes the plugin code directory if it is not installed yet.
2166 * This is intended for the plugins check screen to give the admin a chance
2167 * to cancel the installation of just unzipped plugin before the database
2170 * @param string $component
2172 public function cancel_plugin_installation($component) {
2175 if (!empty($CFG->disableupdateautodeploy
)) {
2179 $plugin = $this->get_plugin_info($component);
2181 if ($this->can_cancel_plugin_installation($plugin)) {
2182 $this->remove_plugin_folder($plugin);
2189 * Returns plugins, the installation of which can be cancelled.
2191 * @return array [(string)component] => (\core\plugininfo\base)plugin
2193 public function list_cancellable_installations() {
2196 if (!empty($CFG->disableupdateautodeploy
)) {
2200 $cancellable = array();
2201 foreach ($this->get_plugins() as $type => $plugins) {
2202 foreach ($plugins as $plugin) {
2203 if ($this->can_cancel_plugin_installation($plugin)) {
2204 $cancellable[$plugin->component
] = $plugin;
2209 return $cancellable;
2213 * Archive the current on-disk plugin code.
2215 * @param \core\plugiinfo\base $plugin
2218 public function archive_plugin_version(\core\plugininfo\base
$plugin) {
2219 return $this->get_code_manager()->archive_plugin_version($plugin->rootdir
, $plugin->component
, $plugin->versiondisk
);
2223 * Returns list of all archives that can be installed to cancel the plugin upgrade.
2225 * @return array [(string)component] => {(string)->component, (string)->zipfilepath}
2227 public function list_restorable_archives() {
2230 if (!empty($CFG->disableupdateautodeploy
)) {
2234 $codeman = $this->get_code_manager();
2235 $restorable = array();
2236 foreach ($this->get_plugins() as $type => $plugins) {
2237 foreach ($plugins as $plugin) {
2238 if ($this->can_cancel_plugin_upgrade($plugin)) {
2239 $restorable[$plugin->component
] = (object)array(
2240 'component' => $plugin->component
,
2241 'zipfilepath' => $codeman->get_archived_plugin_version($plugin->component
, $plugin->versiondb
)
2251 * Reorders plugin types into a sequence to be displayed
2253 * For technical reasons, plugin types returned by {@link core_component::get_plugin_types()} are
2254 * in a certain order that does not need to fit the expected order for the display.
2255 * Particularly, activity modules should be displayed first as they represent the
2256 * real heart of Moodle. They should be followed by other plugin types that are
2257 * used to build the courses (as that is what one expects from LMS). After that,
2258 * other supportive plugin types follow.
2260 * @param array $types associative array
2261 * @return array same array with altered order of items
2263 protected function reorder_plugin_types(array $types) {
2264 $fix = array('mod' => $types['mod']);
2265 foreach (core_component
::get_plugin_list('mod') as $plugin => $fulldir) {
2266 if (!$subtypes = core_component
::get_subplugins('mod_'.$plugin)) {
2269 foreach ($subtypes as $subtype => $ignored) {
2270 $fix[$subtype] = $types[$subtype];
2274 $fix['mod'] = $types['mod'];
2275 $fix['block'] = $types['block'];
2276 $fix['qtype'] = $types['qtype'];
2277 $fix['qbank'] = $types['qbank'];
2278 $fix['qbehaviour'] = $types['qbehaviour'];
2279 $fix['qformat'] = $types['qformat'];
2280 $fix['filter'] = $types['filter'];
2282 $fix['editor'] = $types['editor'];
2283 foreach (core_component
::get_plugin_list('editor') as $plugin => $fulldir) {
2284 if (!$subtypes = core_component
::get_subplugins('editor_'.$plugin)) {
2287 foreach ($subtypes as $subtype => $ignored) {
2288 $fix[$subtype] = $types[$subtype];
2292 $fix['enrol'] = $types['enrol'];
2293 $fix['auth'] = $types['auth'];
2294 $fix['tool'] = $types['tool'];
2295 foreach (core_component
::get_plugin_list('tool') as $plugin => $fulldir) {
2296 if (!$subtypes = core_component
::get_subplugins('tool_'.$plugin)) {
2299 foreach ($subtypes as $subtype => $ignored) {
2300 $fix[$subtype] = $types[$subtype];
2304 foreach ($types as $type => $path) {
2305 if (!isset($fix[$type])) {
2306 $fix[$type] = $path;
2313 * Check if the given directory can be removed by the web server process.
2315 * This recursively checks that the given directory and all its contents
2318 * @param string $fullpath
2321 public function is_directory_removable($fullpath) {
2323 if (!is_writable($fullpath)) {
2327 if (is_dir($fullpath)) {
2328 $handle = opendir($fullpath);
2335 while ($filename = readdir($handle)) {
2337 if ($filename === '.' or $filename === '..') {
2341 $subfilepath = $fullpath.'/'.$filename;
2343 if (is_dir($subfilepath)) {
2344 $result = $result && $this->is_directory_removable($subfilepath);
2347 $result = $result && is_writable($subfilepath);
2357 * Helper method that implements common uninstall prerequisites
2359 * @param \core\plugininfo\base $pluginfo
2362 protected function common_uninstall_check(\core\plugininfo\base
$pluginfo) {
2364 // Check if uninstall is allowed from the GUI.
2365 if (!empty($CFG->uninstallclionly
) && (!CLI_SCRIPT
)) {
2369 if (!$pluginfo->is_uninstall_allowed()) {
2370 // The plugin's plugininfo class declares it should not be uninstalled.
2374 if ($pluginfo->get_status() === static::PLUGIN_STATUS_NEW
) {
2375 // The plugin is not installed. It should be either installed or removed from the disk.
2376 // Relying on this temporary state may be tricky.
2380 if (method_exists($pluginfo, 'get_uninstall_url') and is_null($pluginfo->get_uninstall_url())) {
2381 // Backwards compatibility.
2382 debugging('\core\plugininfo\base subclasses should use is_uninstall_allowed() instead of returning null in get_uninstall_url()',
2391 * Returns a code_manager instance to be used for the plugins code operations.
2393 * @return \core\update\code_manager
2395 protected function get_code_manager() {
2397 if ($this->codemanager
=== null) {
2398 $this->codemanager
= new \core\update\
code_manager();
2401 return $this->codemanager
;
2405 * Returns a client for https://download.moodle.org/api/
2407 * @return \core\update\api
2409 protected function get_update_api_client() {
2411 if ($this->updateapiclient
=== null) {
2412 $this->updateapiclient
= \core\update\api
::client();
2415 return $this->updateapiclient
;