MDL-63063 recentlyaccesseditems: fully contained block
[moodle.git] / lib / classes / plugin_manager.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Defines classes used for plugins management
19  *
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.
23  *
24  * @package    core
25  * @copyright  2011 David Mudrak <david@moodle.com>
26  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27  */
29 defined('MOODLE_INTERNAL') || die();
31 /**
32  * Singleton class providing general plugins management functionality.
33  */
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;
91     /**
92      * Direct initiation not allowed, use the factory method {@link self::instance()}
93      */
94     protected function __construct() {
95     }
97     /**
98      * Sorry, this is singleton
99      */
100     protected function __clone() {
101     }
103     /**
104      * Factory method for this class
105      *
106      * @return core_plugin_manager the singleton instance
107      */
108     public static function instance() {
109         if (is_null(static::$singletoninstance)) {
110             static::$singletoninstance = new static();
111         }
112         return static::$singletoninstance;
113     }
115     /**
116      * Reset all caches.
117      * @param bool $phpunitreset
118      */
119     public static function reset_caches($phpunitreset = false) {
120         if ($phpunitreset) {
121             static::$singletoninstance = null;
122         } else {
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;
134             }
135         }
136         $cache = cache::make('core', 'plugin_manager');
137         $cache->purge();
138     }
140     /**
141      * Returns the result of {@link core_component::get_plugin_types()} ordered for humans
142      *
143      * @see self::reorder_plugin_types()
144      * @return array (string)name => (string)location
145      */
146     public function get_plugin_types() {
147         if (func_num_args() > 0) {
148             if (!func_get_arg(0)) {
149                 throw new coding_exception('core_plugin_manager->get_plugin_types() does not support relative paths.');
150             }
151         }
152         if ($this->plugintypes) {
153             return $this->plugintypes;
154         }
156         $this->plugintypes = $this->reorder_plugin_types(core_component::get_plugin_types());
157         return $this->plugintypes;
158     }
160     /**
161      * Load list of installed plugins,
162      * always call before using $this->installedplugins.
163      *
164      * This method is caching results for all plugins.
165      */
166     protected function load_installed_plugins() {
167         global $DB, $CFG;
169         if ($this->installedplugins) {
170             return;
171         }
173         if (empty($CFG->version)) {
174             // Nothing installed yet.
175             $this->installedplugins = array();
176             return;
177         }
179         $cache = cache::make('core', 'plugin_manager');
180         $installed = $cache->get('installed');
182         if (is_array($installed)) {
183             $this->installedplugins = $installed;
184             return;
185         }
187         $this->installedplugins = array();
189         $versions = $DB->get_records('config_plugins', array('name'=>'version'));
190         foreach ($versions as $version) {
191             $parts = explode('_', $version->plugin, 2);
192             if (!isset($parts[1])) {
193                 // Invalid component, there must be at least one "_".
194                 continue;
195             }
196             // Do not verify here if plugin type and name are valid.
197             $this->installedplugins[$parts[0]][$parts[1]] = $version->value;
198         }
200         foreach ($this->installedplugins as $key => $value) {
201             ksort($this->installedplugins[$key]);
202         }
204         $cache->set('installed', $this->installedplugins);
205     }
207     /**
208      * Return list of installed plugins of given type.
209      * @param string $type
210      * @return array $name=>$version
211      */
212     public function get_installed_plugins($type) {
213         $this->load_installed_plugins();
214         if (isset($this->installedplugins[$type])) {
215             return $this->installedplugins[$type];
216         }
217         return array();
218     }
220     /**
221      * Load list of all enabled plugins,
222      * call before using $this->enabledplugins.
223      *
224      * This method is caching results from individual plugin info classes.
225      */
226     protected function load_enabled_plugins() {
227         global $CFG;
229         if ($this->enabledplugins) {
230             return;
231         }
233         if (empty($CFG->version)) {
234             $this->enabledplugins = array();
235             return;
236         }
238         $cache = cache::make('core', 'plugin_manager');
239         $enabled = $cache->get('enabled');
241         if (is_array($enabled)) {
242             $this->enabledplugins = $enabled;
243             return;
244         }
246         $this->enabledplugins = array();
248         require_once($CFG->libdir.'/adminlib.php');
250         $plugintypes = core_component::get_plugin_types();
251         foreach ($plugintypes as $plugintype => $fulldir) {
252             $plugininfoclass = static::resolve_plugininfo_class($plugintype);
253             if (class_exists($plugininfoclass)) {
254                 $enabled = $plugininfoclass::get_enabled_plugins();
255                 if (!is_array($enabled)) {
256                     continue;
257                 }
258                 $this->enabledplugins[$plugintype] = $enabled;
259             }
260         }
262         $cache->set('enabled', $this->enabledplugins);
263     }
265     /**
266      * Get list of enabled plugins of given type,
267      * the result may contain missing plugins.
268      *
269      * @param string $type
270      * @return array|null  list of enabled plugins of this type, null if unknown
271      */
272     public function get_enabled_plugins($type) {
273         $this->load_enabled_plugins();
274         if (isset($this->enabledplugins[$type])) {
275             return $this->enabledplugins[$type];
276         }
277         return null;
278     }
280     /**
281      * Load list of all present plugins - call before using $this->presentplugins.
282      */
283     protected function load_present_plugins() {
284         if ($this->presentplugins) {
285             return;
286         }
288         $cache = cache::make('core', 'plugin_manager');
289         $present = $cache->get('present');
291         if (is_array($present)) {
292             $this->presentplugins = $present;
293             return;
294         }
296         $this->presentplugins = array();
298         $plugintypes = core_component::get_plugin_types();
299         foreach ($plugintypes as $type => $typedir) {
300             $plugs = core_component::get_plugin_list($type);
301             foreach ($plugs as $plug => $fullplug) {
302                 $module = new stdClass();
303                 $plugin = new stdClass();
304                 $plugin->version = null;
305                 include($fullplug.'/version.php');
307                 // Check if the legacy $module syntax is still used.
308                 if (!is_object($module) or (count((array)$module) > 0)) {
309                     debugging('Unsupported $module syntax detected in version.php of the '.$type.'_'.$plug.' plugin.');
310                     $skipcache = true;
311                 }
313                 // Check if the component is properly declared.
314                 if (empty($plugin->component) or ($plugin->component !== $type.'_'.$plug)) {
315                     debugging('Plugin '.$type.'_'.$plug.' does not declare valid $plugin->component in its version.php.');
316                     $skipcache = true;
317                 }
319                 $this->presentplugins[$type][$plug] = $plugin;
320             }
321         }
323         if (empty($skipcache)) {
324             $cache->set('present', $this->presentplugins);
325         }
326     }
328     /**
329      * Get list of present plugins of given type.
330      *
331      * @param string $type
332      * @return array|null  list of presnet plugins $name=>$diskversion, null if unknown
333      */
334     public function get_present_plugins($type) {
335         $this->load_present_plugins();
336         if (isset($this->presentplugins[$type])) {
337             return $this->presentplugins[$type];
338         }
339         return null;
340     }
342     /**
343      * Returns a tree of known plugins and information about them
344      *
345      * @return array 2D array. The first keys are plugin type names (e.g. qtype);
346      *      the second keys are the plugin local name (e.g. multichoice); and
347      *      the values are the corresponding objects extending {@link \core\plugininfo\base}
348      */
349     public function get_plugins() {
350         $this->init_pluginsinfo_property();
352         // Make sure all types are initialised.
353         foreach ($this->pluginsinfo as $plugintype => $list) {
354             if ($list === null) {
355                 $this->get_plugins_of_type($plugintype);
356             }
357         }
359         return $this->pluginsinfo;
360     }
362     /**
363      * Returns list of known plugins of the given type.
364      *
365      * This method returns the subset of the tree returned by {@link self::get_plugins()}.
366      * If the given type is not known, empty array is returned.
367      *
368      * @param string $type plugin type, e.g. 'mod' or 'workshopallocation'
369      * @return \core\plugininfo\base[] (string)plugin name (e.g. 'workshop') => corresponding subclass of {@link \core\plugininfo\base}
370      */
371     public function get_plugins_of_type($type) {
372         global $CFG;
374         $this->init_pluginsinfo_property();
376         if (!array_key_exists($type, $this->pluginsinfo)) {
377             return array();
378         }
380         if (is_array($this->pluginsinfo[$type])) {
381             return $this->pluginsinfo[$type];
382         }
384         $types = core_component::get_plugin_types();
386         if (!isset($types[$type])) {
387             // Orphaned subplugins!
388             $plugintypeclass = static::resolve_plugininfo_class($type);
389             $this->pluginsinfo[$type] = $plugintypeclass::get_plugins($type, null, $plugintypeclass, $this);
390             return $this->pluginsinfo[$type];
391         }
393         /** @var \core\plugininfo\base $plugintypeclass */
394         $plugintypeclass = static::resolve_plugininfo_class($type);
395         $plugins = $plugintypeclass::get_plugins($type, $types[$type], $plugintypeclass, $this);
396         $this->pluginsinfo[$type] = $plugins;
398         return $this->pluginsinfo[$type];
399     }
401     /**
402      * Init placeholder array for plugin infos.
403      */
404     protected function init_pluginsinfo_property() {
405         if (is_array($this->pluginsinfo)) {
406             return;
407         }
408         $this->pluginsinfo = array();
410         $plugintypes = $this->get_plugin_types();
412         foreach ($plugintypes as $plugintype => $plugintyperootdir) {
413             $this->pluginsinfo[$plugintype] = null;
414         }
416         // Add orphaned subplugin types.
417         $this->load_installed_plugins();
418         foreach ($this->installedplugins as $plugintype => $unused) {
419             if (!isset($plugintypes[$plugintype])) {
420                 $this->pluginsinfo[$plugintype] = null;
421             }
422         }
423     }
425     /**
426      * Find the plugin info class for given type.
427      *
428      * @param string $type
429      * @return string name of pluginfo class for give plugin type
430      */
431     public static function resolve_plugininfo_class($type) {
432         $plugintypes = core_component::get_plugin_types();
433         if (!isset($plugintypes[$type])) {
434             return '\core\plugininfo\orphaned';
435         }
437         $parent = core_component::get_subtype_parent($type);
439         if ($parent) {
440             $class = '\\'.$parent.'\plugininfo\\' . $type;
441             if (class_exists($class)) {
442                 $plugintypeclass = $class;
443             } else {
444                 if ($dir = core_component::get_component_directory($parent)) {
445                     // BC only - use namespace instead!
446                     if (file_exists("$dir/adminlib.php")) {
447                         global $CFG;
448                         include_once("$dir/adminlib.php");
449                     }
450                     if (class_exists('plugininfo_' . $type)) {
451                         $plugintypeclass = 'plugininfo_' . $type;
452                         debugging('Class "'.$plugintypeclass.'" is deprecated, migrate to "'.$class.'"', DEBUG_DEVELOPER);
453                     } else {
454                         debugging('Subplugin type "'.$type.'" should define class "'.$class.'"', DEBUG_DEVELOPER);
455                         $plugintypeclass = '\core\plugininfo\general';
456                     }
457                 } else {
458                     $plugintypeclass = '\core\plugininfo\general';
459                 }
460             }
461         } else {
462             $class = '\core\plugininfo\\' . $type;
463             if (class_exists($class)) {
464                 $plugintypeclass = $class;
465             } else {
466                 debugging('All standard types including "'.$type.'" should have plugininfo class!', DEBUG_DEVELOPER);
467                 $plugintypeclass = '\core\plugininfo\general';
468             }
469         }
471         if (!in_array('core\plugininfo\base', class_parents($plugintypeclass))) {
472             throw new coding_exception('Class ' . $plugintypeclass . ' must extend \core\plugininfo\base');
473         }
475         return $plugintypeclass;
476     }
478     /**
479      * Returns list of all known subplugins of the given plugin.
480      *
481      * For plugins that do not provide subplugins (i.e. there is no support for it),
482      * empty array is returned.
483      *
484      * @param string $component full component name, e.g. 'mod_workshop'
485      * @return array (string) component name (e.g. 'workshopallocation_random') => subclass of {@link \core\plugininfo\base}
486      */
487     public function get_subplugins_of_plugin($component) {
489         $pluginfo = $this->get_plugin_info($component);
491         if (is_null($pluginfo)) {
492             return array();
493         }
495         $subplugins = $this->get_subplugins();
497         if (!isset($subplugins[$pluginfo->component])) {
498             return array();
499         }
501         $list = array();
503         foreach ($subplugins[$pluginfo->component] as $subdata) {
504             foreach ($this->get_plugins_of_type($subdata->type) as $subpluginfo) {
505                 $list[$subpluginfo->component] = $subpluginfo;
506             }
507         }
509         return $list;
510     }
512     /**
513      * Returns list of plugins that define their subplugins and the information
514      * about them from the db/subplugins.php file.
515      *
516      * @return array with keys like 'mod_quiz', and values the data from the
517      *      corresponding db/subplugins.php file.
518      */
519     public function get_subplugins() {
521         if (is_array($this->subpluginsinfo)) {
522             return $this->subpluginsinfo;
523         }
525         $plugintypes = core_component::get_plugin_types();
527         $this->subpluginsinfo = array();
528         foreach (core_component::get_plugin_types_with_subplugins() as $type => $ignored) {
529             foreach (core_component::get_plugin_list($type) as $plugin => $componentdir) {
530                 $component = $type.'_'.$plugin;
531                 $subplugins = core_component::get_subplugins($component);
532                 if (!$subplugins) {
533                     continue;
534                 }
535                 $this->subpluginsinfo[$component] = array();
536                 foreach ($subplugins as $subplugintype => $ignored) {
537                     $subplugin = new stdClass();
538                     $subplugin->type = $subplugintype;
539                     $subplugin->typerootdir = $plugintypes[$subplugintype];
540                     $this->subpluginsinfo[$component][$subplugintype] = $subplugin;
541                 }
542             }
543         }
544         return $this->subpluginsinfo;
545     }
547     /**
548      * Returns the name of the plugin that defines the given subplugin type
549      *
550      * If the given subplugin type is not actually a subplugin, returns false.
551      *
552      * @param string $subplugintype the name of subplugin type, eg. workshopform or quiz
553      * @return false|string the name of the parent plugin, eg. mod_workshop
554      */
555     public function get_parent_of_subplugin($subplugintype) {
556         $parent = core_component::get_subtype_parent($subplugintype);
557         if (!$parent) {
558             return false;
559         }
560         return $parent;
561     }
563     /**
564      * Returns a localized name of a given plugin
565      *
566      * @param string $component name of the plugin, eg mod_workshop or auth_ldap
567      * @return string
568      */
569     public function plugin_name($component) {
571         $pluginfo = $this->get_plugin_info($component);
573         if (is_null($pluginfo)) {
574             throw new moodle_exception('err_unknown_plugin', 'core_plugin', '', array('plugin' => $component));
575         }
577         return $pluginfo->displayname;
578     }
580     /**
581      * Returns a localized name of a plugin typed in singular form
582      *
583      * Most plugin types define their names in core_plugin lang file. In case of subplugins,
584      * we try to ask the parent plugin for the name. In the worst case, we will return
585      * the value of the passed $type parameter.
586      *
587      * @param string $type the type of the plugin, e.g. mod or workshopform
588      * @return string
589      */
590     public function plugintype_name($type) {
592         if (get_string_manager()->string_exists('type_' . $type, 'core_plugin')) {
593             // For most plugin types, their names are defined in core_plugin lang file.
594             return get_string('type_' . $type, 'core_plugin');
596         } else if ($parent = $this->get_parent_of_subplugin($type)) {
597             // If this is a subplugin, try to ask the parent plugin for the name.
598             if (get_string_manager()->string_exists('subplugintype_' . $type, $parent)) {
599                 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type, $parent);
600             } else {
601                 return $this->plugin_name($parent) . ' / ' . $type;
602             }
604         } else {
605             return $type;
606         }
607     }
609     /**
610      * Returns a localized name of a plugin type in plural form
611      *
612      * Most plugin types define their names in core_plugin lang file. In case of subplugins,
613      * we try to ask the parent plugin for the name. In the worst case, we will return
614      * the value of the passed $type parameter.
615      *
616      * @param string $type the type of the plugin, e.g. mod or workshopform
617      * @return string
618      */
619     public function plugintype_name_plural($type) {
621         if (get_string_manager()->string_exists('type_' . $type . '_plural', 'core_plugin')) {
622             // For most plugin types, their names are defined in core_plugin lang file.
623             return get_string('type_' . $type . '_plural', 'core_plugin');
625         } else if ($parent = $this->get_parent_of_subplugin($type)) {
626             // If this is a subplugin, try to ask the parent plugin for the name.
627             if (get_string_manager()->string_exists('subplugintype_' . $type . '_plural', $parent)) {
628                 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type . '_plural', $parent);
629             } else {
630                 return $this->plugin_name($parent) . ' / ' . $type;
631             }
633         } else {
634             return $type;
635         }
636     }
638     /**
639      * Returns information about the known plugin, or null
640      *
641      * @param string $component frankenstyle component name.
642      * @return \core\plugininfo\base|null the corresponding plugin information.
643      */
644     public function get_plugin_info($component) {
645         list($type, $name) = core_component::normalize_component($component);
646         $plugins = $this->get_plugins_of_type($type);
647         if (isset($plugins[$name])) {
648             return $plugins[$name];
649         } else {
650             return null;
651         }
652     }
654     /**
655      * Check to see if the current version of the plugin seems to be a checkout of an external repository.
656      *
657      * @param string $component frankenstyle component name
658      * @return false|string
659      */
660     public function plugin_external_source($component) {
662         $plugininfo = $this->get_plugin_info($component);
664         if (is_null($plugininfo)) {
665             return false;
666         }
668         $pluginroot = $plugininfo->rootdir;
670         if (is_dir($pluginroot.'/.git')) {
671             return 'git';
672         }
674         if (is_file($pluginroot.'/.git')) {
675             return 'git-submodule';
676         }
678         if (is_dir($pluginroot.'/CVS')) {
679             return 'cvs';
680         }
682         if (is_dir($pluginroot.'/.svn')) {
683             return 'svn';
684         }
686         if (is_dir($pluginroot.'/.hg')) {
687             return 'mercurial';
688         }
690         return false;
691     }
693     /**
694      * Get a list of any other plugins that require this one.
695      * @param string $component frankenstyle component name.
696      * @return array of frankensyle component names that require this one.
697      */
698     public function other_plugins_that_require($component) {
699         $others = array();
700         foreach ($this->get_plugins() as $type => $plugins) {
701             foreach ($plugins as $plugin) {
702                 $required = $plugin->get_other_required_plugins();
703                 if (isset($required[$component])) {
704                     $others[] = $plugin->component;
705                 }
706             }
707         }
708         return $others;
709     }
711     /**
712      * Check a dependencies list against the list of installed plugins.
713      * @param array $dependencies compenent name to required version or ANY_VERSION.
714      * @return bool true if all the dependencies are satisfied.
715      */
716     public function are_dependencies_satisfied($dependencies) {
717         foreach ($dependencies as $component => $requiredversion) {
718             $otherplugin = $this->get_plugin_info($component);
719             if (is_null($otherplugin)) {
720                 return false;
721             }
723             if ($requiredversion != ANY_VERSION and $otherplugin->versiondisk < $requiredversion) {
724                 return false;
725             }
726         }
728         return true;
729     }
731     /**
732      * Checks all dependencies for all installed plugins
733      *
734      * This is used by install and upgrade. The array passed by reference as the second
735      * argument is populated with the list of plugins that have failed dependencies (note that
736      * a single plugin can appear multiple times in the $failedplugins).
737      *
738      * @param int $moodleversion the version from version.php.
739      * @param array $failedplugins to return the list of plugins with non-satisfied dependencies
740      * @return bool true if all the dependencies are satisfied for all plugins.
741      */
742     public function all_plugins_ok($moodleversion, &$failedplugins = array()) {
744         $return = true;
745         foreach ($this->get_plugins() as $type => $plugins) {
746             foreach ($plugins as $plugin) {
748                 if (!$plugin->is_core_dependency_satisfied($moodleversion)) {
749                     $return = false;
750                     $failedplugins[] = $plugin->component;
751                 }
753                 if (!$this->are_dependencies_satisfied($plugin->get_other_required_plugins())) {
754                     $return = false;
755                     $failedplugins[] = $plugin->component;
756                 }
757             }
758         }
760         return $return;
761     }
763     /**
764      * Resolve requirements and dependencies of a plugin.
765      *
766      * Returns an array of objects describing the requirement/dependency,
767      * indexed by the frankenstyle name of the component. The returned array
768      * can be empty. The objects in the array have following properties:
769      *
770      *  ->(numeric)hasver
771      *  ->(numeric)reqver
772      *  ->(string)status
773      *  ->(string)availability
774      *
775      * @param \core\plugininfo\base $plugin the plugin we are checking
776      * @param null|string|int|double $moodleversion explicit moodle core version to check against, defaults to $CFG->version
777      * @param null|string|int $moodlebranch explicit moodle core branch to check against, defaults to $CFG->branch
778      * @return array of objects
779      */
780     public function resolve_requirements(\core\plugininfo\base $plugin, $moodleversion=null, $moodlebranch=null) {
781         global $CFG;
783         if ($plugin->versiondisk === null) {
784             // Missing from disk, we have no version.php to read from.
785             return array();
786         }
788         if ($moodleversion === null) {
789             $moodleversion = $CFG->version;
790         }
792         if ($moodlebranch === null) {
793             $moodlebranch = $CFG->branch;
794         }
796         $reqs = array();
797         $reqcore = $this->resolve_core_requirements($plugin, $moodleversion);
799         if (!empty($reqcore)) {
800             $reqs['core'] = $reqcore;
801         }
803         foreach ($plugin->get_other_required_plugins() as $reqplug => $reqver) {
804             $reqs[$reqplug] = $this->resolve_dependency_requirements($plugin, $reqplug, $reqver, $moodlebranch);
805         }
807         return $reqs;
808     }
810     /**
811      * Helper method to resolve plugin's requirements on the moodle core.
812      *
813      * @param \core\plugininfo\base $plugin the plugin we are checking
814      * @param string|int|double $moodleversion moodle core branch to check against
815      * @return stdObject
816      */
817     protected function resolve_core_requirements(\core\plugininfo\base $plugin, $moodleversion) {
819         $reqs = (object)array(
820             'hasver' => null,
821             'reqver' => null,
822             'status' => null,
823             'availability' => null,
824         );
826         $reqs->hasver = $moodleversion;
828         if (empty($plugin->versionrequires)) {
829             $reqs->reqver = ANY_VERSION;
830         } else {
831             $reqs->reqver = $plugin->versionrequires;
832         }
834         if ($plugin->is_core_dependency_satisfied($moodleversion)) {
835             $reqs->status = self::REQUIREMENT_STATUS_OK;
836         } else {
837             $reqs->status = self::REQUIREMENT_STATUS_OUTDATED;
838         }
840         return $reqs;
841     }
843     /**
844      * Helper method to resolve plugin's dependecies on other plugins.
845      *
846      * @param \core\plugininfo\base $plugin the plugin we are checking
847      * @param string $otherpluginname
848      * @param string|int $requiredversion
849      * @param string|int $moodlebranch explicit moodle core branch to check against, defaults to $CFG->branch
850      * @return stdClass
851      */
852     protected function resolve_dependency_requirements(\core\plugininfo\base $plugin, $otherpluginname,
853             $requiredversion, $moodlebranch) {
855         $reqs = (object)array(
856             'hasver' => null,
857             'reqver' => null,
858             'status' => null,
859             'availability' => null,
860         );
862         $otherplugin = $this->get_plugin_info($otherpluginname);
864         if ($otherplugin !== null) {
865             // The required plugin is installed.
866             $reqs->hasver = $otherplugin->versiondisk;
867             $reqs->reqver = $requiredversion;
868             // Check it has sufficient version.
869             if ($requiredversion == ANY_VERSION or $otherplugin->versiondisk >= $requiredversion) {
870                 $reqs->status = self::REQUIREMENT_STATUS_OK;
871             } else {
872                 $reqs->status = self::REQUIREMENT_STATUS_OUTDATED;
873             }
875         } else {
876             // The required plugin is not installed.
877             $reqs->hasver = null;
878             $reqs->reqver = $requiredversion;
879             $reqs->status = self::REQUIREMENT_STATUS_MISSING;
880         }
882         if ($reqs->status !== self::REQUIREMENT_STATUS_OK) {
883             if ($this->is_remote_plugin_available($otherpluginname, $requiredversion, false)) {
884                 $reqs->availability = self::REQUIREMENT_AVAILABLE;
885             } else {
886                 $reqs->availability = self::REQUIREMENT_UNAVAILABLE;
887             }
888         }
890         return $reqs;
891     }
893     /**
894      * Is the given plugin version available in the plugins directory?
895      *
896      * See {@link self::get_remote_plugin_info()} for the full explanation of how the $version
897      * parameter is interpretted.
898      *
899      * @param string $component plugin frankenstyle name
900      * @param string|int $version ANY_VERSION or the version number
901      * @param bool $exactmatch false if "given version or higher" is requested
902      * @return boolean
903      */
904     public function is_remote_plugin_available($component, $version, $exactmatch) {
906         $info = $this->get_remote_plugin_info($component, $version, $exactmatch);
908         if (empty($info)) {
909             // There is no available plugin of that name.
910             return false;
911         }
913         if (empty($info->version)) {
914             // Plugin is known, but no suitable version was found.
915             return false;
916         }
918         return true;
919     }
921     /**
922      * Can the given plugin version be installed via the admin UI?
923      *
924      * This check should be used whenever attempting to install a plugin from
925      * the plugins directory (new install, available update, missing dependency).
926      *
927      * @param string $component
928      * @param int $version version number
929      * @param string $reason returned code of the reason why it is not
930      * @return boolean
931      */
932     public function is_remote_plugin_installable($component, $version, &$reason=null) {
933         global $CFG;
935         // Make sure the feature is not disabled.
936         if (!empty($CFG->disableupdateautodeploy)) {
937             $reason = 'disabled';
938             return false;
939         }
941         // Make sure the version is available.
942         if (!$this->is_remote_plugin_available($component, $version, true)) {
943             $reason = 'remoteunavailable';
944             return false;
945         }
947         // Make sure the plugin type root directory is writable.
948         list($plugintype, $pluginname) = core_component::normalize_component($component);
949         if (!$this->is_plugintype_writable($plugintype)) {
950             $reason = 'notwritableplugintype';
951             return false;
952         }
954         $remoteinfo = $this->get_remote_plugin_info($component, $version, true);
955         $localinfo = $this->get_plugin_info($component);
957         if ($localinfo) {
958             // If the plugin is already present, prevent downgrade.
959             if ($localinfo->versiondb > $remoteinfo->version->version) {
960                 $reason = 'cannotdowngrade';
961                 return false;
962             }
964             // Make sure we have write access to all the existing code.
965             if (is_dir($localinfo->rootdir)) {
966                 if (!$this->is_plugin_folder_removable($component)) {
967                     $reason = 'notwritableplugin';
968                     return false;
969                 }
970             }
971         }
973         // Looks like it could work.
974         return true;
975     }
977     /**
978      * Given the list of remote plugin infos, return just those installable.
979      *
980      * This is typically used on lists returned by
981      * {@link self::available_updates()} or {@link self::missing_dependencies()}
982      * to perform bulk installation of remote plugins.
983      *
984      * @param array $remoteinfos list of {@link \core\update\remote_info}
985      * @return array
986      */
987     public function filter_installable($remoteinfos) {
988         global $CFG;
990         if (!empty($CFG->disableupdateautodeploy)) {
991             return array();
992         }
993         if (empty($remoteinfos)) {
994             return array();
995         }
996         $installable = array();
997         foreach ($remoteinfos as $index => $remoteinfo) {
998             if ($this->is_remote_plugin_installable($remoteinfo->component, $remoteinfo->version->version)) {
999                 $installable[$index] = $remoteinfo;
1000             }
1001         }
1002         return $installable;
1003     }
1005     /**
1006      * Returns information about a plugin in the plugins directory.
1007      *
1008      * This is typically used when checking for available dependencies (in
1009      * which case the $version represents minimal version we need), or
1010      * when installing an available update or a new plugin from the plugins
1011      * directory (in which case the $version is exact version we are
1012      * interested in). The interpretation of the $version is controlled
1013      * by the $exactmatch argument.
1014      *
1015      * If a plugin with the given component name is found, data about the
1016      * plugin are returned as an object. The ->version property of the object
1017      * contains the information about the particular plugin version that
1018      * matches best the given critera. The ->version property is false if no
1019      * suitable version of the plugin was found (yet the plugin itself is
1020      * known).
1021      *
1022      * See {@link \core\update\api::validate_pluginfo_format()} for the
1023      * returned data structure.
1024      *
1025      * @param string $component plugin frankenstyle name
1026      * @param string|int $version ANY_VERSION or the version number
1027      * @param bool $exactmatch false if "given version or higher" is requested
1028      * @return \core\update\remote_info|bool
1029      */
1030     public function get_remote_plugin_info($component, $version, $exactmatch) {
1032         if ($exactmatch and $version == ANY_VERSION) {
1033             throw new coding_exception('Invalid request for exactly any version, it does not make sense.');
1034         }
1036         $client = $this->get_update_api_client();
1038         if ($exactmatch) {
1039             // Use client's get_plugin_info() method.
1040             if (!isset($this->remotepluginsinfoexact[$component][$version])) {
1041                 $this->remotepluginsinfoexact[$component][$version] = $client->get_plugin_info($component, $version);
1042             }
1043             return $this->remotepluginsinfoexact[$component][$version];
1045         } else {
1046             // Use client's find_plugin() method.
1047             if (!isset($this->remotepluginsinfoatleast[$component][$version])) {
1048                 $this->remotepluginsinfoatleast[$component][$version] = $client->find_plugin($component, $version);
1049             }
1050             return $this->remotepluginsinfoatleast[$component][$version];
1051         }
1052     }
1054     /**
1055      * Obtain the plugin ZIP file from the given URL
1056      *
1057      * The caller is supposed to know both downloads URL and the MD5 hash of
1058      * the ZIP contents in advance, typically by using the API requests against
1059      * the plugins directory.
1060      *
1061      * @param string $url
1062      * @param string $md5
1063      * @return string|bool full path to the file, false on error
1064      */
1065     public function get_remote_plugin_zip($url, $md5) {
1066         global $CFG;
1068         if (!empty($CFG->disableupdateautodeploy)) {
1069             return false;
1070         }
1071         return $this->get_code_manager()->get_remote_plugin_zip($url, $md5);
1072     }
1074     /**
1075      * Extracts the saved plugin ZIP file.
1076      *
1077      * Returns the list of files found in the ZIP. The format of that list is
1078      * array of (string)filerelpath => (bool|string) where the array value is
1079      * either true or a string describing the problematic file.
1080      *
1081      * @see zip_packer::extract_to_pathname()
1082      * @param string $zipfilepath full path to the saved ZIP file
1083      * @param string $targetdir full path to the directory to extract the ZIP file to
1084      * @param string $rootdir explicitly rename the root directory of the ZIP into this non-empty value
1085      * @return array list of extracted files as returned by {@link zip_packer::extract_to_pathname()}
1086      */
1087     public function unzip_plugin_file($zipfilepath, $targetdir, $rootdir = '') {
1088         return $this->get_code_manager()->unzip_plugin_file($zipfilepath, $targetdir, $rootdir);
1089     }
1091     /**
1092      * Detects the plugin's name from its ZIP file.
1093      *
1094      * Plugin ZIP packages are expected to contain a single directory and the
1095      * directory name would become the plugin name once extracted to the Moodle
1096      * dirroot.
1097      *
1098      * @param string $zipfilepath full path to the ZIP files
1099      * @return string|bool false on error
1100      */
1101     public function get_plugin_zip_root_dir($zipfilepath) {
1102         return $this->get_code_manager()->get_plugin_zip_root_dir($zipfilepath);
1103     }
1105     /**
1106      * Return a list of missing dependencies.
1107      *
1108      * This should provide the full list of plugins that should be installed to
1109      * fulfill the requirements of all plugins, if possible.
1110      *
1111      * @param bool $availableonly return only available missing dependencies
1112      * @return array of \core\update\remote_info|bool indexed by the component name
1113      */
1114     public function missing_dependencies($availableonly=false) {
1116         $dependencies = array();
1118         foreach ($this->get_plugins() as $plugintype => $pluginfos) {
1119             foreach ($pluginfos as $pluginname => $pluginfo) {
1120                 foreach ($this->resolve_requirements($pluginfo) as $reqname => $reqinfo) {
1121                     if ($reqname === 'core') {
1122                         continue;
1123                     }
1124                     if ($reqinfo->status != self::REQUIREMENT_STATUS_OK) {
1125                         if ($reqinfo->availability == self::REQUIREMENT_AVAILABLE) {
1126                             $remoteinfo = $this->get_remote_plugin_info($reqname, $reqinfo->reqver, false);
1128                             if (empty($dependencies[$reqname])) {
1129                                 $dependencies[$reqname] = $remoteinfo;
1130                             } else {
1131                                 // If resolving requirements has led to two different versions of the same
1132                                 // remote plugin, pick the higher version. This can happen in cases like one
1133                                 // plugin requiring ANY_VERSION and another plugin requiring specific higher
1134                                 // version with lower maturity of a remote plugin.
1135                                 if ($remoteinfo->version->version > $dependencies[$reqname]->version->version) {
1136                                     $dependencies[$reqname] = $remoteinfo;
1137                                 }
1138                             }
1140                         } else {
1141                             if (!isset($dependencies[$reqname])) {
1142                                 // Unable to find a plugin fulfilling the requirements.
1143                                 $dependencies[$reqname] = false;
1144                             }
1145                         }
1146                     }
1147                 }
1148             }
1149         }
1151         if ($availableonly) {
1152             foreach ($dependencies as $component => $info) {
1153                 if (empty($info) or empty($info->version)) {
1154                     unset($dependencies[$component]);
1155                 }
1156             }
1157         }
1159         return $dependencies;
1160     }
1162     /**
1163      * Is it possible to uninstall the given plugin?
1164      *
1165      * False is returned if the plugininfo subclass declares the uninstall should
1166      * not be allowed via {@link \core\plugininfo\base::is_uninstall_allowed()} or if the
1167      * core vetoes it (e.g. becase the plugin or some of its subplugins is required
1168      * by some other installed plugin).
1169      *
1170      * @param string $component full frankenstyle name, e.g. mod_foobar
1171      * @return bool
1172      */
1173     public function can_uninstall_plugin($component) {
1175         $pluginfo = $this->get_plugin_info($component);
1177         if (is_null($pluginfo)) {
1178             return false;
1179         }
1181         if (!$this->common_uninstall_check($pluginfo)) {
1182             return false;
1183         }
1185         // Verify only if something else requires the subplugins, do not verify their common_uninstall_check()!
1186         $subplugins = $this->get_subplugins_of_plugin($pluginfo->component);
1187         foreach ($subplugins as $subpluginfo) {
1188             // Check if there are some other plugins requiring this subplugin
1189             // (but the parent and siblings).
1190             foreach ($this->other_plugins_that_require($subpluginfo->component) as $requiresme) {
1191                 $ismyparent = ($pluginfo->component === $requiresme);
1192                 $ismysibling = in_array($requiresme, array_keys($subplugins));
1193                 if (!$ismyparent and !$ismysibling) {
1194                     return false;
1195                 }
1196             }
1197         }
1199         // Check if there are some other plugins requiring this plugin
1200         // (but its subplugins).
1201         foreach ($this->other_plugins_that_require($pluginfo->component) as $requiresme) {
1202             $ismysubplugin = in_array($requiresme, array_keys($subplugins));
1203             if (!$ismysubplugin) {
1204                 return false;
1205             }
1206         }
1208         return true;
1209     }
1211     /**
1212      * Perform the installation of plugins.
1213      *
1214      * If used for installation of remote plugins from the Moodle Plugins
1215      * directory, the $plugins must be list of {@link \core\update\remote_info}
1216      * object that represent installable remote plugins. The caller can use
1217      * {@link self::filter_installable()} to prepare the list.
1218      *
1219      * If used for installation of plugins from locally available ZIP files,
1220      * the $plugins should be list of objects with properties ->component and
1221      * ->zipfilepath.
1222      *
1223      * The method uses {@link mtrace()} to produce direct output and can be
1224      * used in both web and cli interfaces.
1225      *
1226      * @param array $plugins list of plugins
1227      * @param bool $confirmed should the files be really deployed into the dirroot?
1228      * @param bool $silent perform without output
1229      * @return bool true on success
1230      */
1231     public function install_plugins(array $plugins, $confirmed, $silent) {
1232         global $CFG, $OUTPUT;
1234         if (!empty($CFG->disableupdateautodeploy)) {
1235             return false;
1236         }
1238         if (empty($plugins)) {
1239             return false;
1240         }
1242         $ok = get_string('ok', 'core');
1244         // Let admins know they can expect more verbose output.
1245         $silent or $this->mtrace(get_string('packagesdebug', 'core_plugin'), PHP_EOL, DEBUG_NORMAL);
1247         // Download all ZIP packages if we do not have them yet.
1248         $zips = array();
1249         foreach ($plugins as $plugin) {
1250             if ($plugin instanceof \core\update\remote_info) {
1251                 $zips[$plugin->component] = $this->get_remote_plugin_zip($plugin->version->downloadurl,
1252                     $plugin->version->downloadmd5);
1253                 $silent or $this->mtrace(get_string('packagesdownloading', 'core_plugin', $plugin->component), ' ... ');
1254                 $silent or $this->mtrace(PHP_EOL.' <- '.$plugin->version->downloadurl, '', DEBUG_DEVELOPER);
1255                 $silent or $this->mtrace(PHP_EOL.' -> '.$zips[$plugin->component], ' ... ', DEBUG_DEVELOPER);
1256                 if (!$zips[$plugin->component]) {
1257                     $silent or $this->mtrace(get_string('error'));
1258                     return false;
1259                 }
1260                 $silent or $this->mtrace($ok);
1261             } else {
1262                 if (empty($plugin->zipfilepath)) {
1263                     throw new coding_exception('Unexpected data structure provided');
1264                 }
1265                 $zips[$plugin->component] = $plugin->zipfilepath;
1266                 $silent or $this->mtrace('ZIP '.$plugin->zipfilepath, PHP_EOL, DEBUG_DEVELOPER);
1267             }
1268         }
1270         // Validate all downloaded packages.
1271         foreach ($plugins as $plugin) {
1272             $zipfile = $zips[$plugin->component];
1273             $silent or $this->mtrace(get_string('packagesvalidating', 'core_plugin', $plugin->component), ' ... ');
1274             list($plugintype, $pluginname) = core_component::normalize_component($plugin->component);
1275             $tmp = make_request_directory();
1276             $zipcontents = $this->unzip_plugin_file($zipfile, $tmp, $pluginname);
1277             if (empty($zipcontents)) {
1278                 $silent or $this->mtrace(get_string('error'));
1279                 $silent or $this->mtrace('Unable to unzip '.$zipfile, PHP_EOL, DEBUG_DEVELOPER);
1280                 return false;
1281             }
1283             $validator = \core\update\validator::instance($tmp, $zipcontents);
1284             $validator->assert_plugin_type($plugintype);
1285             $validator->assert_moodle_version($CFG->version);
1286             // TODO Check for missing dependencies during validation.
1287             $result = $validator->execute();
1288             if (!$silent) {
1289                 $result ? $this->mtrace($ok) : $this->mtrace(get_string('error'));
1290                 foreach ($validator->get_messages() as $message) {
1291                     if ($message->level === $validator::INFO) {
1292                         // Display [OK] validation messages only if debugging mode is DEBUG_NORMAL.
1293                         $level = DEBUG_NORMAL;
1294                     } else if ($message->level === $validator::DEBUG) {
1295                         // Display [Debug] validation messages only if debugging mode is DEBUG_ALL.
1296                         $level = DEBUG_ALL;
1297                     } else {
1298                         // Display [Warning] and [Error] always.
1299                         $level = null;
1300                     }
1301                     if ($message->level === $validator::WARNING and !CLI_SCRIPT) {
1302                         $this->mtrace('  <strong>['.$validator->message_level_name($message->level).']</strong>', ' ', $level);
1303                     } else {
1304                         $this->mtrace('  ['.$validator->message_level_name($message->level).']', ' ', $level);
1305                     }
1306                     $this->mtrace($validator->message_code_name($message->msgcode), ' ', $level);
1307                     $info = $validator->message_code_info($message->msgcode, $message->addinfo);
1308                     if ($info) {
1309                         $this->mtrace('['.s($info).']', ' ', $level);
1310                     } else if (is_string($message->addinfo)) {
1311                         $this->mtrace('['.s($message->addinfo, true).']', ' ', $level);
1312                     } else {
1313                         $this->mtrace('['.s(json_encode($message->addinfo, true)).']', ' ', $level);
1314                     }
1315                     if ($icon = $validator->message_help_icon($message->msgcode)) {
1316                         if (CLI_SCRIPT) {
1317                             $this->mtrace(PHP_EOL.'  ^^^ '.get_string('help').': '.
1318                                 get_string($icon->identifier.'_help', $icon->component), '', $level);
1319                         } else {
1320                             $this->mtrace($OUTPUT->render($icon), ' ', $level);
1321                         }
1322                     }
1323                     $this->mtrace(PHP_EOL, '', $level);
1324                 }
1325             }
1326             if (!$result) {
1327                 $silent or $this->mtrace(get_string('packagesvalidatingfailed', 'core_plugin'));
1328                 return false;
1329             }
1330         }
1331         $silent or $this->mtrace(PHP_EOL.get_string('packagesvalidatingok', 'core_plugin'));
1333         if (!$confirmed) {
1334             return true;
1335         }
1337         // Extract all ZIP packs do the dirroot.
1338         foreach ($plugins as $plugin) {
1339             $silent or $this->mtrace(get_string('packagesextracting', 'core_plugin', $plugin->component), ' ... ');
1340             $zipfile = $zips[$plugin->component];
1341             list($plugintype, $pluginname) = core_component::normalize_component($plugin->component);
1342             $target = $this->get_plugintype_root($plugintype);
1343             if (file_exists($target.'/'.$pluginname)) {
1344                 $this->remove_plugin_folder($this->get_plugin_info($plugin->component));
1345             }
1346             if (!$this->unzip_plugin_file($zipfile, $target, $pluginname)) {
1347                 $silent or $this->mtrace(get_string('error'));
1348                 $silent or $this->mtrace('Unable to unzip '.$zipfile, PHP_EOL, DEBUG_DEVELOPER);
1349                 if (function_exists('opcache_reset')) {
1350                     opcache_reset();
1351                 }
1352                 return false;
1353             }
1354             $silent or $this->mtrace($ok);
1355         }
1356         if (function_exists('opcache_reset')) {
1357             opcache_reset();
1358         }
1360         return true;
1361     }
1363     /**
1364      * Outputs the given message via {@link mtrace()}.
1365      *
1366      * If $debug is provided, then the message is displayed only at the given
1367      * debugging level (e.g. DEBUG_DEVELOPER to display the message only if the
1368      * site has developer debugging level selected).
1369      *
1370      * @param string $msg message
1371      * @param string $eol end of line
1372      * @param null|int $debug null to display always, int only on given debug level
1373      */
1374     protected function mtrace($msg, $eol=PHP_EOL, $debug=null) {
1375         global $CFG;
1377         if ($debug !== null and !debugging(null, $debug)) {
1378             return;
1379         }
1381         mtrace($msg, $eol);
1382     }
1384     /**
1385      * Returns uninstall URL if exists.
1386      *
1387      * @param string $component
1388      * @param string $return either 'overview' or 'manage'
1389      * @return moodle_url uninstall URL, null if uninstall not supported
1390      */
1391     public function get_uninstall_url($component, $return = 'overview') {
1392         if (!$this->can_uninstall_plugin($component)) {
1393             return null;
1394         }
1396         $pluginfo = $this->get_plugin_info($component);
1398         if (is_null($pluginfo)) {
1399             return null;
1400         }
1402         if (method_exists($pluginfo, 'get_uninstall_url')) {
1403             debugging('plugininfo method get_uninstall_url() is deprecated, all plugins should be uninstalled via standard URL only.');
1404             return $pluginfo->get_uninstall_url($return);
1405         }
1407         return $pluginfo->get_default_uninstall_url($return);
1408     }
1410     /**
1411      * Uninstall the given plugin.
1412      *
1413      * Automatically cleans-up all remaining configuration data, log records, events,
1414      * files from the file pool etc.
1415      *
1416      * In the future, the functionality of {@link uninstall_plugin()} function may be moved
1417      * into this method and all the code should be refactored to use it. At the moment, we
1418      * mimic this future behaviour by wrapping that function call.
1419      *
1420      * @param string $component
1421      * @param progress_trace $progress traces the process
1422      * @return bool true on success, false on errors/problems
1423      */
1424     public function uninstall_plugin($component, progress_trace $progress) {
1426         $pluginfo = $this->get_plugin_info($component);
1428         if (is_null($pluginfo)) {
1429             return false;
1430         }
1432         // Give the pluginfo class a chance to execute some steps.
1433         $result = $pluginfo->uninstall($progress);
1434         if (!$result) {
1435             return false;
1436         }
1438         // Call the legacy core function to uninstall the plugin.
1439         ob_start();
1440         uninstall_plugin($pluginfo->type, $pluginfo->name);
1441         $progress->output(ob_get_clean());
1443         return true;
1444     }
1446     /**
1447      * Checks if there are some plugins with a known available update
1448      *
1449      * @return bool true if there is at least one available update
1450      */
1451     public function some_plugins_updatable() {
1452         foreach ($this->get_plugins() as $type => $plugins) {
1453             foreach ($plugins as $plugin) {
1454                 if ($plugin->available_updates()) {
1455                     return true;
1456                 }
1457             }
1458         }
1460         return false;
1461     }
1463     /**
1464      * Returns list of available updates for the given component.
1465      *
1466      * This method should be considered as internal API and is supposed to be
1467      * called by {@link \core\plugininfo\base::available_updates()} only
1468      * to lazy load the data once they are first requested.
1469      *
1470      * @param string $component frankenstyle name of the plugin
1471      * @return null|array array of \core\update\info objects or null
1472      */
1473     public function load_available_updates_for_plugin($component) {
1474         global $CFG;
1476         $provider = \core\update\checker::instance();
1478         if (!$provider->enabled() or during_initial_install()) {
1479             return null;
1480         }
1482         if (isset($CFG->updateminmaturity)) {
1483             $minmaturity = $CFG->updateminmaturity;
1484         } else {
1485             // This can happen during the very first upgrade to 2.3.
1486             $minmaturity = MATURITY_STABLE;
1487         }
1489         return $provider->get_update_info($component, array('minmaturity' => $minmaturity));
1490     }
1492     /**
1493      * Returns a list of all available updates to be installed.
1494      *
1495      * This is used when "update all plugins" action is performed at the
1496      * administration UI screen.
1497      *
1498      * Returns array of remote info objects indexed by the plugin
1499      * component. If there are multiple updates available (typically a mix of
1500      * stable and non-stable ones), we pick the most mature most recent one.
1501      *
1502      * Plugins without explicit maturity are considered more mature than
1503      * release candidates but less mature than explicit stable (this should be
1504      * pretty rare case).
1505      *
1506      * @return array (string)component => (\core\update\remote_info)remoteinfo
1507      */
1508     public function available_updates() {
1510         $updates = array();
1512         foreach ($this->get_plugins() as $type => $plugins) {
1513             foreach ($plugins as $plugin) {
1514                 $availableupdates = $plugin->available_updates();
1515                 if (empty($availableupdates)) {
1516                     continue;
1517                 }
1518                 foreach ($availableupdates as $update) {
1519                     if (empty($updates[$plugin->component])) {
1520                         $updates[$plugin->component] = $update;
1521                         continue;
1522                     }
1523                     $maturitycurrent = $updates[$plugin->component]->maturity;
1524                     if (empty($maturitycurrent)) {
1525                         $maturitycurrent = MATURITY_STABLE - 25;
1526                     }
1527                     $maturityremote = $update->maturity;
1528                     if (empty($maturityremote)) {
1529                         $maturityremote = MATURITY_STABLE - 25;
1530                     }
1531                     if ($maturityremote < $maturitycurrent) {
1532                         continue;
1533                     }
1534                     if ($maturityremote > $maturitycurrent) {
1535                         $updates[$plugin->component] = $update;
1536                         continue;
1537                     }
1538                     if ($update->version > $updates[$plugin->component]->version) {
1539                         $updates[$plugin->component] = $update;
1540                         continue;
1541                     }
1542                 }
1543             }
1544         }
1546         foreach ($updates as $component => $update) {
1547             $remoteinfo = $this->get_remote_plugin_info($component, $update->version, true);
1548             if (empty($remoteinfo) or empty($remoteinfo->version)) {
1549                 unset($updates[$component]);
1550             } else {
1551                 $updates[$component] = $remoteinfo;
1552             }
1553         }
1555         return $updates;
1556     }
1558     /**
1559      * Check to see if the given plugin folder can be removed by the web server process.
1560      *
1561      * @param string $component full frankenstyle component
1562      * @return bool
1563      */
1564     public function is_plugin_folder_removable($component) {
1566         $pluginfo = $this->get_plugin_info($component);
1568         if (is_null($pluginfo)) {
1569             return false;
1570         }
1572         // To be able to remove the plugin folder, its parent must be writable, too.
1573         if (!is_writable(dirname($pluginfo->rootdir))) {
1574             return false;
1575         }
1577         // Check that the folder and all its content is writable (thence removable).
1578         return $this->is_directory_removable($pluginfo->rootdir);
1579     }
1581     /**
1582      * Is it possible to create a new plugin directory for the given plugin type?
1583      *
1584      * @throws coding_exception for invalid plugin types or non-existing plugin type locations
1585      * @param string $plugintype
1586      * @return boolean
1587      */
1588     public function is_plugintype_writable($plugintype) {
1590         $plugintypepath = $this->get_plugintype_root($plugintype);
1592         if (is_null($plugintypepath)) {
1593             throw new coding_exception('Unknown plugin type: '.$plugintype);
1594         }
1596         if ($plugintypepath === false) {
1597             throw new coding_exception('Plugin type location does not exist: '.$plugintype);
1598         }
1600         return is_writable($plugintypepath);
1601     }
1603     /**
1604      * Returns the full path of the root of the given plugin type
1605      *
1606      * Null is returned if the plugin type is not known. False is returned if
1607      * the plugin type root is expected but not found. Otherwise, string is
1608      * returned.
1609      *
1610      * @param string $plugintype
1611      * @return string|bool|null
1612      */
1613     public function get_plugintype_root($plugintype) {
1615         $plugintypepath = null;
1616         foreach (core_component::get_plugin_types() as $type => $fullpath) {
1617             if ($type === $plugintype) {
1618                 $plugintypepath = $fullpath;
1619                 break;
1620             }
1621         }
1622         if (is_null($plugintypepath)) {
1623             return null;
1624         }
1625         if (!is_dir($plugintypepath)) {
1626             return false;
1627         }
1629         return $plugintypepath;
1630     }
1632     /**
1633      * Defines a list of all plugins that were originally shipped in the standard Moodle distribution,
1634      * but are not anymore and are deleted during upgrades.
1635      *
1636      * The main purpose of this list is to hide missing plugins during upgrade.
1637      *
1638      * @param string $type plugin type
1639      * @param string $name plugin name
1640      * @return bool
1641      */
1642     public static function is_deleted_standard_plugin($type, $name) {
1643         // Do not include plugins that were removed during upgrades to versions that are
1644         // not supported as source versions for upgrade any more. For example, at MOODLE_23_STABLE
1645         // branch, listed should be no plugins that were removed at 1.9.x - 2.1.x versions as
1646         // Moodle 2.3 supports upgrades from 2.2.x only.
1647         $plugins = array(
1648             'qformat' => array('blackboard', 'learnwise'),
1649             'auth' => array('radius', 'fc', 'nntp', 'pam', 'pop3', 'imap'),
1650             'block' => array('course_overview', 'messages'),
1651             'cachestore' => array('memcache'),
1652             'enrol' => array('authorize'),
1653             'report' => array('search'),
1654             'repository' => array('alfresco'),
1655             'tinymce' => array('dragmath'),
1656             'tool' => array('bloglevelupgrade', 'qeupgradehelper', 'timezoneimport', 'assignmentupgrade'),
1657             'theme' => array('afterburner', 'anomaly', 'arialist', 'base', 'binarius', 'boxxie', 'brick', 'canvas',
1658                 'formal_white', 'formfactor', 'fusion', 'leatherbound', 'magazine', 'mymobile', 'nimble', 'nonzero',
1659                 'overlay', 'serenity', 'sky_high', 'splash', 'standard', 'standardold'),
1660             'webservice' => array('amf'),
1661         );
1663         if (!isset($plugins[$type])) {
1664             return false;
1665         }
1666         return in_array($name, $plugins[$type]);
1667     }
1669     /**
1670      * Defines a white list of all plugins shipped in the standard Moodle distribution
1671      *
1672      * @param string $type
1673      * @return false|array array of standard plugins or false if the type is unknown
1674      */
1675     public static function standard_plugins_list($type) {
1677         $standard_plugins = array(
1679             'antivirus' => array(
1680                 'clamav'
1681             ),
1683             'atto' => array(
1684                 'accessibilitychecker', 'accessibilityhelper', 'align',
1685                 'backcolor', 'bold', 'charmap', 'clear', 'collapse', 'emoticon',
1686                 'equation', 'fontcolor', 'html', 'image', 'indent', 'italic',
1687                 'link', 'managefiles', 'media', 'noautolink', 'orderedlist',
1688                 'recordrtc', 'rtl', 'strike', 'subscript', 'superscript', 'table',
1689                 'title', 'underline', 'undo', 'unorderedlist'
1690             ),
1692             'assignment' => array(
1693                 'offline', 'online', 'upload', 'uploadsingle'
1694             ),
1696             'assignsubmission' => array(
1697                 'comments', 'file', 'onlinetext'
1698             ),
1700             'assignfeedback' => array(
1701                 'comments', 'file', 'offline', 'editpdf'
1702             ),
1704             'auth' => array(
1705                 'cas', 'db', 'email', 'ldap', 'lti', 'manual', 'mnet',
1706                 'nologin', 'none', 'oauth2', 'shibboleth', 'webservice'
1707             ),
1709             'availability' => array(
1710                 'completion', 'date', 'grade', 'group', 'grouping', 'profile'
1711             ),
1713             'block' => array(
1714                 'activity_modules', 'activity_results', 'admin_bookmarks', 'badges',
1715                 'blog_menu', 'blog_recent', 'blog_tags', 'calendar_month',
1716                 'calendar_upcoming', 'comments', 'community',
1717                 'completionstatus', 'course_list', 'course_summary',
1718                 'feedback', 'globalsearch', 'glossary_random', 'html',
1719                 'login', 'lp', 'mentees', 'mnet_hosts', 'myoverview', 'myprofile',
1720                 'navigation', 'news_items', 'online_users', 'participants',
1721                 'private_files', 'quiz_results', 'recent_activity', 'recentlyaccesseditems',
1722                 'rss_client', 'search_forums', 'section_links',
1723                 'selfcompletion', 'settings', 'site_main_menu',
1724                 'social_activities', 'tag_flickr', 'tag_youtube', 'tags', 'timeline'
1725             ),
1727             'booktool' => array(
1728                 'exportimscp', 'importhtml', 'print'
1729             ),
1731             'cachelock' => array(
1732                 'file'
1733             ),
1735             'cachestore' => array(
1736                 'file', 'memcached', 'mongodb', 'session', 'static', 'apcu', 'redis'
1737             ),
1739             'calendartype' => array(
1740                 'gregorian'
1741             ),
1743             'coursereport' => array(
1744                 // Deprecated!
1745             ),
1747             'datafield' => array(
1748                 'checkbox', 'date', 'file', 'latlong', 'menu', 'multimenu',
1749                 'number', 'picture', 'radiobutton', 'text', 'textarea', 'url'
1750             ),
1752             'dataformat' => array(
1753                 'html', 'csv', 'json', 'excel', 'ods',
1754             ),
1756             'datapreset' => array(
1757                 'imagegallery'
1758             ),
1760             'fileconverter' => array(
1761                 'unoconv', 'googledrive'
1762             ),
1764             'editor' => array(
1765                 'atto', 'textarea', 'tinymce'
1766             ),
1768             'enrol' => array(
1769                 'category', 'cohort', 'database', 'flatfile',
1770                 'guest', 'imsenterprise', 'ldap', 'lti', 'manual', 'meta', 'mnet',
1771                 'paypal', 'self'
1772             ),
1774             'filter' => array(
1775                 'activitynames', 'algebra', 'censor', 'emailprotect',
1776                 'emoticon', 'mathjaxloader', 'mediaplugin', 'multilang', 'tex', 'tidy',
1777                 'urltolink', 'data', 'glossary'
1778             ),
1780             'format' => array(
1781                 'singleactivity', 'social', 'topics', 'weeks'
1782             ),
1784             'gradeexport' => array(
1785                 'ods', 'txt', 'xls', 'xml'
1786             ),
1788             'gradeimport' => array(
1789                 'csv', 'direct', 'xml'
1790             ),
1792             'gradereport' => array(
1793                 'grader', 'history', 'outcomes', 'overview', 'user', 'singleview'
1794             ),
1796             'gradingform' => array(
1797                 'rubric', 'guide'
1798             ),
1800             'local' => array(
1801             ),
1803             'logstore' => array(
1804                 'database', 'legacy', 'standard',
1805             ),
1807             'ltiservice' => array(
1808                 'gradebookservices', 'memberships', 'profile', 'toolproxy', 'toolsettings'
1809             ),
1811             'mlbackend' => array(
1812                 'php', 'python'
1813             ),
1815             'media' => array(
1816                 'html5audio', 'html5video', 'swf', 'videojs', 'vimeo', 'youtube'
1817             ),
1819             'message' => array(
1820                 'airnotifier', 'email', 'jabber', 'popup'
1821             ),
1823             'mnetservice' => array(
1824                 'enrol'
1825             ),
1827             'mod' => array(
1828                 'assign', 'assignment', 'book', 'chat', 'choice', 'data', 'feedback', 'folder',
1829                 'forum', 'glossary', 'imscp', 'label', 'lesson', 'lti', 'page',
1830                 'quiz', 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop'
1831             ),
1833             'plagiarism' => array(
1834             ),
1836             'portfolio' => array(
1837                 'boxnet', 'download', 'flickr', 'googledocs', 'mahara', 'picasa'
1838             ),
1840             'profilefield' => array(
1841                 'checkbox', 'datetime', 'menu', 'text', 'textarea'
1842             ),
1844             'qbehaviour' => array(
1845                 'adaptive', 'adaptivenopenalty', 'deferredcbm',
1846                 'deferredfeedback', 'immediatecbm', 'immediatefeedback',
1847                 'informationitem', 'interactive', 'interactivecountback',
1848                 'manualgraded', 'missing'
1849             ),
1851             'qformat' => array(
1852                 'aiken', 'blackboard_six', 'examview', 'gift',
1853                 'missingword', 'multianswer', 'webct',
1854                 'xhtml', 'xml'
1855             ),
1857             'qtype' => array(
1858                 'calculated', 'calculatedmulti', 'calculatedsimple',
1859                 'ddimageortext', 'ddmarker', 'ddwtos', 'description',
1860                 'essay', 'gapselect', 'match', 'missingtype', 'multianswer',
1861                 'multichoice', 'numerical', 'random', 'randomsamatch',
1862                 'shortanswer', 'truefalse'
1863             ),
1865             'quiz' => array(
1866                 'grading', 'overview', 'responses', 'statistics'
1867             ),
1869             'quizaccess' => array(
1870                 'delaybetweenattempts', 'ipaddress', 'numattempts', 'offlineattempts', 'openclosedate',
1871                 'password', 'safebrowser', 'securewindow', 'timelimit'
1872             ),
1874             'report' => array(
1875                 'backups', 'competency', 'completion', 'configlog', 'courseoverview', 'eventlist',
1876                 'insights', 'log', 'loglive', 'outline', 'participation', 'progress', 'questioninstances',
1877                 'security', 'stats', 'performance', 'usersessions'
1878             ),
1880             'repository' => array(
1881                 'areafiles', 'boxnet', 'coursefiles', 'dropbox', 'equella', 'filesystem',
1882                 'flickr', 'flickr_public', 'googledocs', 'local', 'merlot',
1883                 'onedrive', 'picasa', 'recent', 'skydrive', 's3', 'upload', 'url', 'user', 'webdav',
1884                 'wikimedia', 'youtube'
1885             ),
1887             'search' => array(
1888                 'simpledb', 'solr'
1889             ),
1891             'scormreport' => array(
1892                 'basic',
1893                 'interactions',
1894                 'graphs',
1895                 'objectives'
1896             ),
1898             'tinymce' => array(
1899                 'ctrlhelp', 'managefiles', 'moodleemoticon', 'moodleimage',
1900                 'moodlemedia', 'moodlenolink', 'pdw', 'spellchecker', 'wrap'
1901             ),
1903             'theme' => array(
1904                 'boost', 'bootstrapbase', 'clean', 'more'
1905             ),
1907             'tool' => array(
1908                 'analytics', 'availabilityconditions', 'behat', 'capability', 'cohortroles', 'customlang',
1909                 'dataprivacy', 'dbtransfer', 'filetypes', 'generator', 'health', 'httpsreplace', 'innodb', 'installaddon',
1910                 'langimport', 'log', 'lp', 'lpimportcsv', 'lpmigrate', 'messageinbound', 'mobile', 'multilangupgrade',
1911                 'monitor', 'oauth2', 'phpunit', 'policy', 'profiling', 'recyclebin', 'replace', 'spamcleaner', 'task',
1912                 'templatelibrary', 'uploadcourse', 'uploaduser', 'unsuproles', 'usertours', 'xmldb'
1913             ),
1915             'webservice' => array(
1916                 'rest', 'soap', 'xmlrpc'
1917             ),
1919             'workshopallocation' => array(
1920                 'manual', 'random', 'scheduled'
1921             ),
1923             'workshopeval' => array(
1924                 'best'
1925             ),
1927             'workshopform' => array(
1928                 'accumulative', 'comments', 'numerrors', 'rubric'
1929             )
1930         );
1932         if (isset($standard_plugins[$type])) {
1933             return $standard_plugins[$type];
1934         } else {
1935             return false;
1936         }
1937     }
1939     /**
1940      * Remove the current plugin code from the dirroot.
1941      *
1942      * If removing the currently installed version (which happens during
1943      * updates), we archive the code so that the upgrade can be cancelled.
1944      *
1945      * To prevent accidental data-loss, we also archive the existing plugin
1946      * code if cancelling installation of it, so that the developer does not
1947      * loose the only version of their work-in-progress.
1948      *
1949      * @param \core\plugininfo\base $plugin
1950      */
1951     public function remove_plugin_folder(\core\plugininfo\base $plugin) {
1953         if (!$this->is_plugin_folder_removable($plugin->component)) {
1954             throw new moodle_exception('err_removing_unremovable_folder', 'core_plugin', '',
1955                 array('plugin' => $plugin->component, 'rootdir' => $plugin->rootdir),
1956                 'plugin root folder is not removable as expected');
1957         }
1959         if ($plugin->get_status() === self::PLUGIN_STATUS_UPTODATE or $plugin->get_status() === self::PLUGIN_STATUS_NEW) {
1960             $this->archive_plugin_version($plugin);
1961         }
1963         remove_dir($plugin->rootdir);
1964         clearstatcache();
1965         if (function_exists('opcache_reset')) {
1966             opcache_reset();
1967         }
1968     }
1970     /**
1971      * Can the installation of the new plugin be cancelled?
1972      *
1973      * Subplugins can be cancelled only via their parent plugin, not separately
1974      * (they are considered as implicit requirements if distributed together
1975      * with the main package).
1976      *
1977      * @param \core\plugininfo\base $plugin
1978      * @return bool
1979      */
1980     public function can_cancel_plugin_installation(\core\plugininfo\base $plugin) {
1981         global $CFG;
1983         if (!empty($CFG->disableupdateautodeploy)) {
1984             return false;
1985         }
1987         if (empty($plugin) or $plugin->is_standard() or $plugin->is_subplugin()
1988                 or !$this->is_plugin_folder_removable($plugin->component)) {
1989             return false;
1990         }
1992         if ($plugin->get_status() === self::PLUGIN_STATUS_NEW) {
1993             return true;
1994         }
1996         return false;
1997     }
1999     /**
2000      * Can the upgrade of the existing plugin be cancelled?
2001      *
2002      * Subplugins can be cancelled only via their parent plugin, not separately
2003      * (they are considered as implicit requirements if distributed together
2004      * with the main package).
2005      *
2006      * @param \core\plugininfo\base $plugin
2007      * @return bool
2008      */
2009     public function can_cancel_plugin_upgrade(\core\plugininfo\base $plugin) {
2010         global $CFG;
2012         if (!empty($CFG->disableupdateautodeploy)) {
2013             // Cancelling the plugin upgrade is actually installation of the
2014             // previously archived version.
2015             return false;
2016         }
2018         if (empty($plugin) or $plugin->is_standard() or $plugin->is_subplugin()
2019                 or !$this->is_plugin_folder_removable($plugin->component)) {
2020             return false;
2021         }
2023         if ($plugin->get_status() === self::PLUGIN_STATUS_UPGRADE) {
2024             if ($this->get_code_manager()->get_archived_plugin_version($plugin->component, $plugin->versiondb)) {
2025                 return true;
2026             }
2027         }
2029         return false;
2030     }
2032     /**
2033      * Removes the plugin code directory if it is not installed yet.
2034      *
2035      * This is intended for the plugins check screen to give the admin a chance
2036      * to cancel the installation of just unzipped plugin before the database
2037      * upgrade happens.
2038      *
2039      * @param string $component
2040      */
2041     public function cancel_plugin_installation($component) {
2042         global $CFG;
2044         if (!empty($CFG->disableupdateautodeploy)) {
2045             return false;
2046         }
2048         $plugin = $this->get_plugin_info($component);
2050         if ($this->can_cancel_plugin_installation($plugin)) {
2051             $this->remove_plugin_folder($plugin);
2052         }
2054         return false;
2055     }
2057     /**
2058      * Returns plugins, the installation of which can be cancelled.
2059      *
2060      * @return array [(string)component] => (\core\plugininfo\base)plugin
2061      */
2062     public function list_cancellable_installations() {
2063         global $CFG;
2065         if (!empty($CFG->disableupdateautodeploy)) {
2066             return array();
2067         }
2069         $cancellable = array();
2070         foreach ($this->get_plugins() as $type => $plugins) {
2071             foreach ($plugins as $plugin) {
2072                 if ($this->can_cancel_plugin_installation($plugin)) {
2073                     $cancellable[$plugin->component] = $plugin;
2074                 }
2075             }
2076         }
2078         return $cancellable;
2079     }
2081     /**
2082      * Archive the current on-disk plugin code.
2083      *
2084      * @param \core\plugiinfo\base $plugin
2085      * @return bool
2086      */
2087     public function archive_plugin_version(\core\plugininfo\base $plugin) {
2088         return $this->get_code_manager()->archive_plugin_version($plugin->rootdir, $plugin->component, $plugin->versiondisk);
2089     }
2091     /**
2092      * Returns list of all archives that can be installed to cancel the plugin upgrade.
2093      *
2094      * @return array [(string)component] => {(string)->component, (string)->zipfilepath}
2095      */
2096     public function list_restorable_archives() {
2097         global $CFG;
2099         if (!empty($CFG->disableupdateautodeploy)) {
2100             return false;
2101         }
2103         $codeman = $this->get_code_manager();
2104         $restorable = array();
2105         foreach ($this->get_plugins() as $type => $plugins) {
2106             foreach ($plugins as $plugin) {
2107                 if ($this->can_cancel_plugin_upgrade($plugin)) {
2108                     $restorable[$plugin->component] = (object)array(
2109                         'component' => $plugin->component,
2110                         'zipfilepath' => $codeman->get_archived_plugin_version($plugin->component, $plugin->versiondb)
2111                     );
2112                 }
2113             }
2114         }
2116         return $restorable;
2117     }
2119     /**
2120      * Reorders plugin types into a sequence to be displayed
2121      *
2122      * For technical reasons, plugin types returned by {@link core_component::get_plugin_types()} are
2123      * in a certain order that does not need to fit the expected order for the display.
2124      * Particularly, activity modules should be displayed first as they represent the
2125      * real heart of Moodle. They should be followed by other plugin types that are
2126      * used to build the courses (as that is what one expects from LMS). After that,
2127      * other supportive plugin types follow.
2128      *
2129      * @param array $types associative array
2130      * @return array same array with altered order of items
2131      */
2132     protected function reorder_plugin_types(array $types) {
2133         $fix = array('mod' => $types['mod']);
2134         foreach (core_component::get_plugin_list('mod') as $plugin => $fulldir) {
2135             if (!$subtypes = core_component::get_subplugins('mod_'.$plugin)) {
2136                 continue;
2137             }
2138             foreach ($subtypes as $subtype => $ignored) {
2139                 $fix[$subtype] = $types[$subtype];
2140             }
2141         }
2143         $fix['mod']        = $types['mod'];
2144         $fix['block']      = $types['block'];
2145         $fix['qtype']      = $types['qtype'];
2146         $fix['qbehaviour'] = $types['qbehaviour'];
2147         $fix['qformat']    = $types['qformat'];
2148         $fix['filter']     = $types['filter'];
2150         $fix['editor']     = $types['editor'];
2151         foreach (core_component::get_plugin_list('editor') as $plugin => $fulldir) {
2152             if (!$subtypes = core_component::get_subplugins('editor_'.$plugin)) {
2153                 continue;
2154             }
2155             foreach ($subtypes as $subtype => $ignored) {
2156                 $fix[$subtype] = $types[$subtype];
2157             }
2158         }
2160         $fix['enrol'] = $types['enrol'];
2161         $fix['auth']  = $types['auth'];
2162         $fix['tool']  = $types['tool'];
2163         foreach (core_component::get_plugin_list('tool') as $plugin => $fulldir) {
2164             if (!$subtypes = core_component::get_subplugins('tool_'.$plugin)) {
2165                 continue;
2166             }
2167             foreach ($subtypes as $subtype => $ignored) {
2168                 $fix[$subtype] = $types[$subtype];
2169             }
2170         }
2172         foreach ($types as $type => $path) {
2173             if (!isset($fix[$type])) {
2174                 $fix[$type] = $path;
2175             }
2176         }
2177         return $fix;
2178     }
2180     /**
2181      * Check if the given directory can be removed by the web server process.
2182      *
2183      * This recursively checks that the given directory and all its contents
2184      * it writable.
2185      *
2186      * @param string $fullpath
2187      * @return boolean
2188      */
2189     public function is_directory_removable($fullpath) {
2191         if (!is_writable($fullpath)) {
2192             return false;
2193         }
2195         if (is_dir($fullpath)) {
2196             $handle = opendir($fullpath);
2197         } else {
2198             return false;
2199         }
2201         $result = true;
2203         while ($filename = readdir($handle)) {
2205             if ($filename === '.' or $filename === '..') {
2206                 continue;
2207             }
2209             $subfilepath = $fullpath.'/'.$filename;
2211             if (is_dir($subfilepath)) {
2212                 $result = $result && $this->is_directory_removable($subfilepath);
2214             } else {
2215                 $result = $result && is_writable($subfilepath);
2216             }
2217         }
2219         closedir($handle);
2221         return $result;
2222     }
2224     /**
2225      * Helper method that implements common uninstall prerequisites
2226      *
2227      * @param \core\plugininfo\base $pluginfo
2228      * @return bool
2229      */
2230     protected function common_uninstall_check(\core\plugininfo\base $pluginfo) {
2232         if (!$pluginfo->is_uninstall_allowed()) {
2233             // The plugin's plugininfo class declares it should not be uninstalled.
2234             return false;
2235         }
2237         if ($pluginfo->get_status() === static::PLUGIN_STATUS_NEW) {
2238             // The plugin is not installed. It should be either installed or removed from the disk.
2239             // Relying on this temporary state may be tricky.
2240             return false;
2241         }
2243         if (method_exists($pluginfo, 'get_uninstall_url') and is_null($pluginfo->get_uninstall_url())) {
2244             // Backwards compatibility.
2245             debugging('\core\plugininfo\base subclasses should use is_uninstall_allowed() instead of returning null in get_uninstall_url()',
2246                 DEBUG_DEVELOPER);
2247             return false;
2248         }
2250         return true;
2251     }
2253     /**
2254      * Returns a code_manager instance to be used for the plugins code operations.
2255      *
2256      * @return \core\update\code_manager
2257      */
2258     protected function get_code_manager() {
2260         if ($this->codemanager === null) {
2261             $this->codemanager = new \core\update\code_manager();
2262         }
2264         return $this->codemanager;
2265     }
2267     /**
2268      * Returns a client for https://download.moodle.org/api/
2269      *
2270      * @return \core\update\api
2271      */
2272     protected function get_update_api_client() {
2274         if ($this->updateapiclient === null) {
2275             $this->updateapiclient = \core\update\api::client();
2276         }
2278         return $this->updateapiclient;
2279     }