Merge branch 'MDL-67638_master' of https://github.com/nguyenphuctien/moodle
[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';
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;
100     /**
101      * Direct initiation not allowed, use the factory method {@link self::instance()}
102      */
103     protected function __construct() {
104     }
106     /**
107      * Sorry, this is singleton
108      */
109     protected function __clone() {
110     }
112     /**
113      * Factory method for this class
114      *
115      * @return core_plugin_manager the singleton instance
116      */
117     public static function instance() {
118         if (is_null(static::$singletoninstance)) {
119             static::$singletoninstance = new static();
120         }
121         return static::$singletoninstance;
122     }
124     /**
125      * Reset all caches.
126      * @param bool $phpunitreset
127      */
128     public static function reset_caches($phpunitreset = false) {
129         if ($phpunitreset) {
130             static::$singletoninstance = null;
131         } else {
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;
143             }
144         }
145         $cache = cache::make('core', 'plugin_manager');
146         $cache->purge();
147     }
149     /**
150      * Returns the result of {@link core_component::get_plugin_types()} ordered for humans
151      *
152      * @see self::reorder_plugin_types()
153      * @return array (string)name => (string)location
154      */
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.');
159             }
160         }
161         if ($this->plugintypes) {
162             return $this->plugintypes;
163         }
165         $this->plugintypes = $this->reorder_plugin_types(core_component::get_plugin_types());
166         return $this->plugintypes;
167     }
169     /**
170      * Load list of installed plugins,
171      * always call before using $this->installedplugins.
172      *
173      * This method is caching results for all plugins.
174      */
175     protected function load_installed_plugins() {
176         global $DB, $CFG;
178         if ($this->installedplugins) {
179             return;
180         }
182         if (empty($CFG->version)) {
183             // Nothing installed yet.
184             $this->installedplugins = array();
185             return;
186         }
188         $cache = cache::make('core', 'plugin_manager');
189         $installed = $cache->get('installed');
191         if (is_array($installed)) {
192             $this->installedplugins = $installed;
193             return;
194         }
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 "_".
203                 continue;
204             }
205             // Do not verify here if plugin type and name are valid.
206             $this->installedplugins[$parts[0]][$parts[1]] = $version->value;
207         }
209         foreach ($this->installedplugins as $key => $value) {
210             ksort($this->installedplugins[$key]);
211         }
213         $cache->set('installed', $this->installedplugins);
214     }
216     /**
217      * Return list of installed plugins of given type.
218      * @param string $type
219      * @return array $name=>$version
220      */
221     public function get_installed_plugins($type) {
222         $this->load_installed_plugins();
223         if (isset($this->installedplugins[$type])) {
224             return $this->installedplugins[$type];
225         }
226         return array();
227     }
229     /**
230      * Load list of all enabled plugins,
231      * call before using $this->enabledplugins.
232      *
233      * This method is caching results from individual plugin info classes.
234      */
235     protected function load_enabled_plugins() {
236         global $CFG;
238         if ($this->enabledplugins) {
239             return;
240         }
242         if (empty($CFG->version)) {
243             $this->enabledplugins = array();
244             return;
245         }
247         $cache = cache::make('core', 'plugin_manager');
248         $enabled = $cache->get('enabled');
250         if (is_array($enabled)) {
251             $this->enabledplugins = $enabled;
252             return;
253         }
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)) {
265                     continue;
266                 }
267                 $this->enabledplugins[$plugintype] = $enabled;
268             }
269         }
271         $cache->set('enabled', $this->enabledplugins);
272     }
274     /**
275      * Get list of enabled plugins of given type,
276      * the result may contain missing plugins.
277      *
278      * @param string $type
279      * @return array|null  list of enabled plugins of this type, null if unknown
280      */
281     public function get_enabled_plugins($type) {
282         $this->load_enabled_plugins();
283         if (isset($this->enabledplugins[$type])) {
284             return $this->enabledplugins[$type];
285         }
286         return null;
287     }
289     /**
290      * Load list of all present plugins - call before using $this->presentplugins.
291      */
292     protected function load_present_plugins() {
293         if ($this->presentplugins) {
294             return;
295         }
297         $cache = cache::make('core', 'plugin_manager');
298         $present = $cache->get('present');
300         if (is_array($present)) {
301             $this->presentplugins = $present;
302             return;
303         }
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.');
319                     $skipcache = true;
320                 }
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.');
325                     $skipcache = true;
326                 }
328                 $this->presentplugins[$type][$plug] = $plugin;
329             }
330         }
332         if (empty($skipcache)) {
333             $cache->set('present', $this->presentplugins);
334         }
335     }
337     /**
338      * Get list of present plugins of given type.
339      *
340      * @param string $type
341      * @return array|null  list of presnet plugins $name=>$diskversion, null if unknown
342      */
343     public function get_present_plugins($type) {
344         $this->load_present_plugins();
345         if (isset($this->presentplugins[$type])) {
346             return $this->presentplugins[$type];
347         }
348         return null;
349     }
351     /**
352      * Returns a tree of known plugins and information about them
353      *
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}
357      */
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);
365             }
366         }
368         return $this->pluginsinfo;
369     }
371     /**
372      * Returns list of known plugins of the given type.
373      *
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.
376      *
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}
379      */
380     public function get_plugins_of_type($type) {
381         global $CFG;
383         $this->init_pluginsinfo_property();
385         if (!array_key_exists($type, $this->pluginsinfo)) {
386             return array();
387         }
389         if (is_array($this->pluginsinfo[$type])) {
390             return $this->pluginsinfo[$type];
391         }
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];
400         }
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];
408     }
410     /**
411      * Init placeholder array for plugin infos.
412      */
413     protected function init_pluginsinfo_property() {
414         if (is_array($this->pluginsinfo)) {
415             return;
416         }
417         $this->pluginsinfo = array();
419         $plugintypes = $this->get_plugin_types();
421         foreach ($plugintypes as $plugintype => $plugintyperootdir) {
422             $this->pluginsinfo[$plugintype] = null;
423         }
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;
430             }
431         }
432     }
434     /**
435      * Find the plugin info class for given type.
436      *
437      * @param string $type
438      * @return string name of pluginfo class for give plugin type
439      */
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';
444         }
446         $parent = core_component::get_subtype_parent($type);
448         if ($parent) {
449             $class = '\\'.$parent.'\plugininfo\\' . $type;
450             if (class_exists($class)) {
451                 $plugintypeclass = $class;
452             } else {
453                 if ($dir = core_component::get_component_directory($parent)) {
454                     // BC only - use namespace instead!
455                     if (file_exists("$dir/adminlib.php")) {
456                         global $CFG;
457                         include_once("$dir/adminlib.php");
458                     }
459                     if (class_exists('plugininfo_' . $type)) {
460                         $plugintypeclass = 'plugininfo_' . $type;
461                         debugging('Class "'.$plugintypeclass.'" is deprecated, migrate to "'.$class.'"', DEBUG_DEVELOPER);
462                     } else {
463                         debugging('Subplugin type "'.$type.'" should define class "'.$class.'"', DEBUG_DEVELOPER);
464                         $plugintypeclass = '\core\plugininfo\general';
465                     }
466                 } else {
467                     $plugintypeclass = '\core\plugininfo\general';
468                 }
469             }
470         } else {
471             $class = '\core\plugininfo\\' . $type;
472             if (class_exists($class)) {
473                 $plugintypeclass = $class;
474             } else {
475                 debugging('All standard types including "'.$type.'" should have plugininfo class!', DEBUG_DEVELOPER);
476                 $plugintypeclass = '\core\plugininfo\general';
477             }
478         }
480         if (!in_array('core\plugininfo\base', class_parents($plugintypeclass))) {
481             throw new coding_exception('Class ' . $plugintypeclass . ' must extend \core\plugininfo\base');
482         }
484         return $plugintypeclass;
485     }
487     /**
488      * Returns list of all known subplugins of the given plugin.
489      *
490      * For plugins that do not provide subplugins (i.e. there is no support for it),
491      * empty array is returned.
492      *
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}
495      */
496     public function get_subplugins_of_plugin($component) {
498         $pluginfo = $this->get_plugin_info($component);
500         if (is_null($pluginfo)) {
501             return array();
502         }
504         $subplugins = $this->get_subplugins();
506         if (!isset($subplugins[$pluginfo->component])) {
507             return array();
508         }
510         $list = array();
512         foreach ($subplugins[$pluginfo->component] as $subdata) {
513             foreach ($this->get_plugins_of_type($subdata->type) as $subpluginfo) {
514                 $list[$subpluginfo->component] = $subpluginfo;
515             }
516         }
518         return $list;
519     }
521     /**
522      * Returns list of plugins that define their subplugins and the information
523      * about them from the db/subplugins.json file.
524      *
525      * @return array with keys like 'mod_quiz', and values the data from the
526      *      corresponding db/subplugins.json file.
527      */
528     public function get_subplugins() {
530         if (is_array($this->subpluginsinfo)) {
531             return $this->subpluginsinfo;
532         }
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);
541                 if (!$subplugins) {
542                     continue;
543                 }
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;
550                 }
551             }
552         }
553         return $this->subpluginsinfo;
554     }
556     /**
557      * Returns the name of the plugin that defines the given subplugin type
558      *
559      * If the given subplugin type is not actually a subplugin, returns false.
560      *
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
563      */
564     public function get_parent_of_subplugin($subplugintype) {
565         $parent = core_component::get_subtype_parent($subplugintype);
566         if (!$parent) {
567             return false;
568         }
569         return $parent;
570     }
572     /**
573      * Returns a localized name of a given plugin
574      *
575      * @param string $component name of the plugin, eg mod_workshop or auth_ldap
576      * @return string
577      */
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));
584         }
586         return $pluginfo->displayname;
587     }
589     /**
590      * Returns a localized name of a plugin typed in singular form
591      *
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.
595      *
596      * @param string $type the type of the plugin, e.g. mod or workshopform
597      * @return string
598      */
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);
609             } else {
610                 return $this->plugin_name($parent) . ' / ' . $type;
611             }
613         } else {
614             return $type;
615         }
616     }
618     /**
619      * Returns a localized name of a plugin type in plural form
620      *
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.
624      *
625      * @param string $type the type of the plugin, e.g. mod or workshopform
626      * @return string
627      */
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);
638             } else {
639                 return $this->plugin_name($parent) . ' / ' . $type;
640             }
642         } else {
643             return $type;
644         }
645     }
647     /**
648      * Returns information about the known plugin, or null
649      *
650      * @param string $component frankenstyle component name.
651      * @return \core\plugininfo\base|null the corresponding plugin information.
652      */
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];
658         } else {
659             return null;
660         }
661     }
663     /**
664      * Check to see if the current version of the plugin seems to be a checkout of an external repository.
665      *
666      * @param string $component frankenstyle component name
667      * @return false|string
668      */
669     public function plugin_external_source($component) {
671         $plugininfo = $this->get_plugin_info($component);
673         if (is_null($plugininfo)) {
674             return false;
675         }
677         $pluginroot = $plugininfo->rootdir;
679         if (is_dir($pluginroot.'/.git')) {
680             return 'git';
681         }
683         if (is_file($pluginroot.'/.git')) {
684             return 'git-submodule';
685         }
687         if (is_dir($pluginroot.'/CVS')) {
688             return 'cvs';
689         }
691         if (is_dir($pluginroot.'/.svn')) {
692             return 'svn';
693         }
695         if (is_dir($pluginroot.'/.hg')) {
696             return 'mercurial';
697         }
699         return false;
700     }
702     /**
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.
706      */
707     public function other_plugins_that_require($component) {
708         $others = array();
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;
714                 }
715             }
716         }
717         return $others;
718     }
720     /**
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.
724      */
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)) {
729                 return false;
730             }
732             if ($requiredversion != ANY_VERSION and $otherplugin->versiondisk < $requiredversion) {
733                 return false;
734             }
735         }
737         return true;
738     }
740     /**
741      * Checks all dependencies for all installed plugins
742      *
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).
746      *
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.
751      */
752     public function all_plugins_ok($moodleversion, &$failedplugins = array(), $branch = null) {
753         global $CFG;
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;
762             }
763         }
764         $return = true;
765         foreach ($this->get_plugins() as $type => $plugins) {
766             foreach ($plugins as $plugin) {
768                 if (!$plugin->is_core_dependency_satisfied($moodleversion)) {
769                     $return = false;
770                     $failedplugins[] = $plugin->component;
771                 }
773                 if (!$this->are_dependencies_satisfied($plugin->get_other_required_plugins())) {
774                     $return = false;
775                     $failedplugins[] = $plugin->component;
776                 }
778                 if (!$plugin->is_core_compatible_satisfied($branch)) {
779                     $return = false;
780                     $failedplugins[] = $plugin->component;
781                 }
782             }
783         }
785         return $return;
786     }
788     /**
789      * Resolve requirements and dependencies of a plugin.
790      *
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:
794      *
795      *  ->(numeric)hasver
796      *  ->(numeric)reqver
797      *  ->(string)status
798      *  ->(string)availability
799      *
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
804      */
805     public function resolve_requirements(\core\plugininfo\base $plugin, $moodleversion=null, $moodlebranch=null) {
806         global $CFG;
808         if ($plugin->versiondisk === null) {
809             // Missing from disk, we have no version.php to read from.
810             return array();
811         }
813         if ($moodleversion === null) {
814             $moodleversion = $CFG->version;
815         }
817         if ($moodlebranch === null) {
818             $moodlebranch = $CFG->branch;
819         }
821         $reqs = array();
822         $reqcore = $this->resolve_core_requirements($plugin, $moodleversion, $moodlebranch);
824         if (!empty($reqcore)) {
825             $reqs['core'] = $reqcore;
826         }
828         foreach ($plugin->get_other_required_plugins() as $reqplug => $reqver) {
829             $reqs[$reqplug] = $this->resolve_dependency_requirements($plugin, $reqplug, $reqver, $moodlebranch);
830         }
832         return $reqs;
833     }
835     /**
836      * Helper method to resolve plugin's requirements on the moodle core.
837      *
838      * @param \core\plugininfo\base $plugin the plugin we are checking
839      * @param string|int|double $moodleversion moodle core branch to check against
840      * @return stdObject
841      */
842     protected function resolve_core_requirements(\core\plugininfo\base $plugin, $moodleversion, $moodlebranch) {
844         $reqs = (object)array(
845             'hasver' => null,
846             'reqver' => null,
847             'status' => null,
848             'availability' => null,
849         );
850         $reqs->hasver = $moodleversion;
852         if (empty($plugin->versionrequires)) {
853             $reqs->reqver = ANY_VERSION;
854         } else {
855             $reqs->reqver = $plugin->versionrequires;
856         }
858         if ($plugin->is_core_dependency_satisfied($moodleversion)) {
859             $reqs->status = self::REQUIREMENT_STATUS_OK;
860         } else {
861             $reqs->status = self::REQUIREMENT_STATUS_OUTDATED;
862         }
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;
869             }
870         }
872         return $reqs;
873     }
875     /**
876      * Helper method to resolve plugin's dependecies on other plugins.
877      *
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
882      * @return stdClass
883      */
884     protected function resolve_dependency_requirements(\core\plugininfo\base $plugin, $otherpluginname,
885             $requiredversion, $moodlebranch) {
887         $reqs = (object)array(
888             'hasver' => null,
889             'reqver' => null,
890             'status' => null,
891             'availability' => null,
892         );
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;
903             } else {
904                 $reqs->status = self::REQUIREMENT_STATUS_OUTDATED;
905             }
907         } else {
908             // The required plugin is not installed.
909             $reqs->hasver = null;
910             $reqs->reqver = $requiredversion;
911             $reqs->status = self::REQUIREMENT_STATUS_MISSING;
912         }
914         if ($reqs->status !== self::REQUIREMENT_STATUS_OK) {
915             if ($this->is_remote_plugin_available($otherpluginname, $requiredversion, false)) {
916                 $reqs->availability = self::REQUIREMENT_AVAILABLE;
917             } else {
918                 $reqs->availability = self::REQUIREMENT_UNAVAILABLE;
919             }
920         }
922         return $reqs;
923     }
925     /**
926      * Helper method to determine whether a moodle version is explicitly supported.
927      *
928      * @param \core\plugininfo\base $plugin the plugin we are checking
929      * @param int $branch the moodle branch to check support for
930      * @return bool
931      */
932     public function check_explicitly_supported($plugin, $branch) : bool {
933         // Check for correctly formed supported.
934         if (isset($plugin->pluginsupported)) {
935             // Broken apart for readability.
936             $error = false;
937             if (!is_array($plugin->pluginsupported)) {
938                 $error = true;
939             }
940             if (!is_int($plugin->pluginsupported[0]) || !is_int($plugin->pluginsupported[1])) {
941                 $error = true;
942             }
943             if (count($plugin->pluginsupported) != 2) {
944                 $error = true;
945             }
946             if ($error) {
947                 throw new coding_exception(get_string('err_supported_syntax', 'core_plugin'));
948             }
949         }
951         if (isset($plugin->pluginsupported) && $plugin->pluginsupported != null) {
952             if ($plugin->pluginsupported[0] <= $branch && $branch <= $plugin->pluginsupported[1]) {
953                 return self::VERSION_SUPPORTED;
954             } else {
955                 return self::VERSION_NOT_SUPPORTED;
956             }
957         } else {
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;
962                 }
963             }
964             return self::VERSION_NO_SUPPORTS;
965         }
966     }
968     /**
969      * Is the given plugin version available in the plugins directory?
970      *
971      * See {@link self::get_remote_plugin_info()} for the full explanation of how the $version
972      * parameter is interpretted.
973      *
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
977      * @return boolean
978      */
979     public function is_remote_plugin_available($component, $version, $exactmatch) {
981         $info = $this->get_remote_plugin_info($component, $version, $exactmatch);
983         if (empty($info)) {
984             // There is no available plugin of that name.
985             return false;
986         }
988         if (empty($info->version)) {
989             // Plugin is known, but no suitable version was found.
990             return false;
991         }
993         return true;
994     }
996     /**
997      * Can the given plugin version be installed via the admin UI?
998      *
999      * This check should be used whenever attempting to install a plugin from
1000      * the plugins directory (new install, available update, missing dependency).
1001      *
1002      * @param string $component
1003      * @param int $version version number
1004      * @param string $reason returned code of the reason why it is not
1005      * @return boolean
1006      */
1007     public function is_remote_plugin_installable($component, $version, &$reason=null) {
1008         global $CFG;
1010         // Make sure the feature is not disabled.
1011         if (!empty($CFG->disableupdateautodeploy)) {
1012             $reason = 'disabled';
1013             return false;
1014         }
1016         // Make sure the version is available.
1017         if (!$this->is_remote_plugin_available($component, $version, true)) {
1018             $reason = 'remoteunavailable';
1019             return false;
1020         }
1022         // Make sure the plugin type root directory is writable.
1023         list($plugintype, $pluginname) = core_component::normalize_component($component);
1024         if (!$this->is_plugintype_writable($plugintype)) {
1025             $reason = 'notwritableplugintype';
1026             return false;
1027         }
1029         $remoteinfo = $this->get_remote_plugin_info($component, $version, true);
1030         $localinfo = $this->get_plugin_info($component);
1032         if ($localinfo) {
1033             // If the plugin is already present, prevent downgrade.
1034             if ($localinfo->versiondb > $remoteinfo->version->version) {
1035                 $reason = 'cannotdowngrade';
1036                 return false;
1037             }
1039             // Make sure we have write access to all the existing code.
1040             if (is_dir($localinfo->rootdir)) {
1041                 if (!$this->is_plugin_folder_removable($component)) {
1042                     $reason = 'notwritableplugin';
1043                     return false;
1044                 }
1045             }
1046         }
1048         // Looks like it could work.
1049         return true;
1050     }
1052     /**
1053      * Given the list of remote plugin infos, return just those installable.
1054      *
1055      * This is typically used on lists returned by
1056      * {@link self::available_updates()} or {@link self::missing_dependencies()}
1057      * to perform bulk installation of remote plugins.
1058      *
1059      * @param array $remoteinfos list of {@link \core\update\remote_info}
1060      * @return array
1061      */
1062     public function filter_installable($remoteinfos) {
1063         global $CFG;
1065         if (!empty($CFG->disableupdateautodeploy)) {
1066             return array();
1067         }
1068         if (empty($remoteinfos)) {
1069             return array();
1070         }
1071         $installable = array();
1072         foreach ($remoteinfos as $index => $remoteinfo) {
1073             if ($this->is_remote_plugin_installable($remoteinfo->component, $remoteinfo->version->version)) {
1074                 $installable[$index] = $remoteinfo;
1075             }
1076         }
1077         return $installable;
1078     }
1080     /**
1081      * Returns information about a plugin in the plugins directory.
1082      *
1083      * This is typically used when checking for available dependencies (in
1084      * which case the $version represents minimal version we need), or
1085      * when installing an available update or a new plugin from the plugins
1086      * directory (in which case the $version is exact version we are
1087      * interested in). The interpretation of the $version is controlled
1088      * by the $exactmatch argument.
1089      *
1090      * If a plugin with the given component name is found, data about the
1091      * plugin are returned as an object. The ->version property of the object
1092      * contains the information about the particular plugin version that
1093      * matches best the given critera. The ->version property is false if no
1094      * suitable version of the plugin was found (yet the plugin itself is
1095      * known).
1096      *
1097      * See {@link \core\update\api::validate_pluginfo_format()} for the
1098      * returned data structure.
1099      *
1100      * @param string $component plugin frankenstyle name
1101      * @param string|int $version ANY_VERSION or the version number
1102      * @param bool $exactmatch false if "given version or higher" is requested
1103      * @return \core\update\remote_info|bool
1104      */
1105     public function get_remote_plugin_info($component, $version, $exactmatch) {
1107         if ($exactmatch and $version == ANY_VERSION) {
1108             throw new coding_exception('Invalid request for exactly any version, it does not make sense.');
1109         }
1111         $client = $this->get_update_api_client();
1113         if ($exactmatch) {
1114             // Use client's get_plugin_info() method.
1115             if (!isset($this->remotepluginsinfoexact[$component][$version])) {
1116                 $this->remotepluginsinfoexact[$component][$version] = $client->get_plugin_info($component, $version);
1117             }
1118             return $this->remotepluginsinfoexact[$component][$version];
1120         } else {
1121             // Use client's find_plugin() method.
1122             if (!isset($this->remotepluginsinfoatleast[$component][$version])) {
1123                 $this->remotepluginsinfoatleast[$component][$version] = $client->find_plugin($component, $version);
1124             }
1125             return $this->remotepluginsinfoatleast[$component][$version];
1126         }
1127     }
1129     /**
1130      * Obtain the plugin ZIP file from the given URL
1131      *
1132      * The caller is supposed to know both downloads URL and the MD5 hash of
1133      * the ZIP contents in advance, typically by using the API requests against
1134      * the plugins directory.
1135      *
1136      * @param string $url
1137      * @param string $md5
1138      * @return string|bool full path to the file, false on error
1139      */
1140     public function get_remote_plugin_zip($url, $md5) {
1141         global $CFG;
1143         if (!empty($CFG->disableupdateautodeploy)) {
1144             return false;
1145         }
1146         return $this->get_code_manager()->get_remote_plugin_zip($url, $md5);
1147     }
1149     /**
1150      * Extracts the saved plugin ZIP file.
1151      *
1152      * Returns the list of files found in the ZIP. The format of that list is
1153      * array of (string)filerelpath => (bool|string) where the array value is
1154      * either true or a string describing the problematic file.
1155      *
1156      * @see zip_packer::extract_to_pathname()
1157      * @param string $zipfilepath full path to the saved ZIP file
1158      * @param string $targetdir full path to the directory to extract the ZIP file to
1159      * @param string $rootdir explicitly rename the root directory of the ZIP into this non-empty value
1160      * @return array list of extracted files as returned by {@link zip_packer::extract_to_pathname()}
1161      */
1162     public function unzip_plugin_file($zipfilepath, $targetdir, $rootdir = '') {
1163         return $this->get_code_manager()->unzip_plugin_file($zipfilepath, $targetdir, $rootdir);
1164     }
1166     /**
1167      * Detects the plugin's name from its ZIP file.
1168      *
1169      * Plugin ZIP packages are expected to contain a single directory and the
1170      * directory name would become the plugin name once extracted to the Moodle
1171      * dirroot.
1172      *
1173      * @param string $zipfilepath full path to the ZIP files
1174      * @return string|bool false on error
1175      */
1176     public function get_plugin_zip_root_dir($zipfilepath) {
1177         return $this->get_code_manager()->get_plugin_zip_root_dir($zipfilepath);
1178     }
1180     /**
1181      * Return a list of missing dependencies.
1182      *
1183      * This should provide the full list of plugins that should be installed to
1184      * fulfill the requirements of all plugins, if possible.
1185      *
1186      * @param bool $availableonly return only available missing dependencies
1187      * @return array of \core\update\remote_info|bool indexed by the component name
1188      */
1189     public function missing_dependencies($availableonly=false) {
1191         $dependencies = array();
1193         foreach ($this->get_plugins() as $plugintype => $pluginfos) {
1194             foreach ($pluginfos as $pluginname => $pluginfo) {
1195                 foreach ($this->resolve_requirements($pluginfo) as $reqname => $reqinfo) {
1196                     if ($reqname === 'core') {
1197                         continue;
1198                     }
1199                     if ($reqinfo->status != self::REQUIREMENT_STATUS_OK) {
1200                         if ($reqinfo->availability == self::REQUIREMENT_AVAILABLE) {
1201                             $remoteinfo = $this->get_remote_plugin_info($reqname, $reqinfo->reqver, false);
1203                             if (empty($dependencies[$reqname])) {
1204                                 $dependencies[$reqname] = $remoteinfo;
1205                             } else {
1206                                 // If resolving requirements has led to two different versions of the same
1207                                 // remote plugin, pick the higher version. This can happen in cases like one
1208                                 // plugin requiring ANY_VERSION and another plugin requiring specific higher
1209                                 // version with lower maturity of a remote plugin.
1210                                 if ($remoteinfo->version->version > $dependencies[$reqname]->version->version) {
1211                                     $dependencies[$reqname] = $remoteinfo;
1212                                 }
1213                             }
1215                         } else {
1216                             if (!isset($dependencies[$reqname])) {
1217                                 // Unable to find a plugin fulfilling the requirements.
1218                                 $dependencies[$reqname] = false;
1219                             }
1220                         }
1221                     }
1222                 }
1223             }
1224         }
1226         if ($availableonly) {
1227             foreach ($dependencies as $component => $info) {
1228                 if (empty($info) or empty($info->version)) {
1229                     unset($dependencies[$component]);
1230                 }
1231             }
1232         }
1234         return $dependencies;
1235     }
1237     /**
1238      * Is it possible to uninstall the given plugin?
1239      *
1240      * False is returned if the plugininfo subclass declares the uninstall should
1241      * not be allowed via {@link \core\plugininfo\base::is_uninstall_allowed()} or if the
1242      * core vetoes it (e.g. becase the plugin or some of its subplugins is required
1243      * by some other installed plugin).
1244      *
1245      * @param string $component full frankenstyle name, e.g. mod_foobar
1246      * @return bool
1247      */
1248     public function can_uninstall_plugin($component) {
1250         $pluginfo = $this->get_plugin_info($component);
1252         if (is_null($pluginfo)) {
1253             return false;
1254         }
1256         if (!$this->common_uninstall_check($pluginfo)) {
1257             return false;
1258         }
1260         // Verify only if something else requires the subplugins, do not verify their common_uninstall_check()!
1261         $subplugins = $this->get_subplugins_of_plugin($pluginfo->component);
1262         foreach ($subplugins as $subpluginfo) {
1263             // Check if there are some other plugins requiring this subplugin
1264             // (but the parent and siblings).
1265             foreach ($this->other_plugins_that_require($subpluginfo->component) as $requiresme) {
1266                 $ismyparent = ($pluginfo->component === $requiresme);
1267                 $ismysibling = in_array($requiresme, array_keys($subplugins));
1268                 if (!$ismyparent and !$ismysibling) {
1269                     return false;
1270                 }
1271             }
1272         }
1274         // Check if there are some other plugins requiring this plugin
1275         // (but its subplugins).
1276         foreach ($this->other_plugins_that_require($pluginfo->component) as $requiresme) {
1277             $ismysubplugin = in_array($requiresme, array_keys($subplugins));
1278             if (!$ismysubplugin) {
1279                 return false;
1280             }
1281         }
1283         return true;
1284     }
1286     /**
1287      * Perform the installation of plugins.
1288      *
1289      * If used for installation of remote plugins from the Moodle Plugins
1290      * directory, the $plugins must be list of {@link \core\update\remote_info}
1291      * object that represent installable remote plugins. The caller can use
1292      * {@link self::filter_installable()} to prepare the list.
1293      *
1294      * If used for installation of plugins from locally available ZIP files,
1295      * the $plugins should be list of objects with properties ->component and
1296      * ->zipfilepath.
1297      *
1298      * The method uses {@link mtrace()} to produce direct output and can be
1299      * used in both web and cli interfaces.
1300      *
1301      * @param array $plugins list of plugins
1302      * @param bool $confirmed should the files be really deployed into the dirroot?
1303      * @param bool $silent perform without output
1304      * @return bool true on success
1305      */
1306     public function install_plugins(array $plugins, $confirmed, $silent) {
1307         global $CFG, $OUTPUT;
1309         if (!empty($CFG->disableupdateautodeploy)) {
1310             return false;
1311         }
1313         if (empty($plugins)) {
1314             return false;
1315         }
1317         $ok = get_string('ok', 'core');
1319         // Let admins know they can expect more verbose output.
1320         $silent or $this->mtrace(get_string('packagesdebug', 'core_plugin'), PHP_EOL, DEBUG_NORMAL);
1322         // Download all ZIP packages if we do not have them yet.
1323         $zips = array();
1324         foreach ($plugins as $plugin) {
1325             if ($plugin instanceof \core\update\remote_info) {
1326                 $zips[$plugin->component] = $this->get_remote_plugin_zip($plugin->version->downloadurl,
1327                     $plugin->version->downloadmd5);
1328                 $silent or $this->mtrace(get_string('packagesdownloading', 'core_plugin', $plugin->component), ' ... ');
1329                 $silent or $this->mtrace(PHP_EOL.' <- '.$plugin->version->downloadurl, '', DEBUG_DEVELOPER);
1330                 $silent or $this->mtrace(PHP_EOL.' -> '.$zips[$plugin->component], ' ... ', DEBUG_DEVELOPER);
1331                 if (!$zips[$plugin->component]) {
1332                     $silent or $this->mtrace(get_string('error'));
1333                     return false;
1334                 }
1335                 $silent or $this->mtrace($ok);
1336             } else {
1337                 if (empty($plugin->zipfilepath)) {
1338                     throw new coding_exception('Unexpected data structure provided');
1339                 }
1340                 $zips[$plugin->component] = $plugin->zipfilepath;
1341                 $silent or $this->mtrace('ZIP '.$plugin->zipfilepath, PHP_EOL, DEBUG_DEVELOPER);
1342             }
1343         }
1345         // Validate all downloaded packages.
1346         foreach ($plugins as $plugin) {
1347             $zipfile = $zips[$plugin->component];
1348             $silent or $this->mtrace(get_string('packagesvalidating', 'core_plugin', $plugin->component), ' ... ');
1349             list($plugintype, $pluginname) = core_component::normalize_component($plugin->component);
1350             $tmp = make_request_directory();
1351             $zipcontents = $this->unzip_plugin_file($zipfile, $tmp, $pluginname);
1352             if (empty($zipcontents)) {
1353                 $silent or $this->mtrace(get_string('error'));
1354                 $silent or $this->mtrace('Unable to unzip '.$zipfile, PHP_EOL, DEBUG_DEVELOPER);
1355                 return false;
1356             }
1358             $validator = \core\update\validator::instance($tmp, $zipcontents);
1359             $validator->assert_plugin_type($plugintype);
1360             $validator->assert_moodle_version($CFG->version);
1361             // TODO Check for missing dependencies during validation.
1362             $result = $validator->execute();
1363             if (!$silent) {
1364                 $result ? $this->mtrace($ok) : $this->mtrace(get_string('error'));
1365                 foreach ($validator->get_messages() as $message) {
1366                     if ($message->level === $validator::INFO) {
1367                         // Display [OK] validation messages only if debugging mode is DEBUG_NORMAL.
1368                         $level = DEBUG_NORMAL;
1369                     } else if ($message->level === $validator::DEBUG) {
1370                         // Display [Debug] validation messages only if debugging mode is DEBUG_ALL.
1371                         $level = DEBUG_ALL;
1372                     } else {
1373                         // Display [Warning] and [Error] always.
1374                         $level = null;
1375                     }
1376                     if ($message->level === $validator::WARNING and !CLI_SCRIPT) {
1377                         $this->mtrace('  <strong>['.$validator->message_level_name($message->level).']</strong>', ' ', $level);
1378                     } else {
1379                         $this->mtrace('  ['.$validator->message_level_name($message->level).']', ' ', $level);
1380                     }
1381                     $this->mtrace($validator->message_code_name($message->msgcode), ' ', $level);
1382                     $info = $validator->message_code_info($message->msgcode, $message->addinfo);
1383                     if ($info) {
1384                         $this->mtrace('['.s($info).']', ' ', $level);
1385                     } else if (is_string($message->addinfo)) {
1386                         $this->mtrace('['.s($message->addinfo, true).']', ' ', $level);
1387                     } else {
1388                         $this->mtrace('['.s(json_encode($message->addinfo, true)).']', ' ', $level);
1389                     }
1390                     if ($icon = $validator->message_help_icon($message->msgcode)) {
1391                         if (CLI_SCRIPT) {
1392                             $this->mtrace(PHP_EOL.'  ^^^ '.get_string('help').': '.
1393                                 get_string($icon->identifier.'_help', $icon->component), '', $level);
1394                         } else {
1395                             $this->mtrace($OUTPUT->render($icon), ' ', $level);
1396                         }
1397                     }
1398                     $this->mtrace(PHP_EOL, '', $level);
1399                 }
1400             }
1401             if (!$result) {
1402                 $silent or $this->mtrace(get_string('packagesvalidatingfailed', 'core_plugin'));
1403                 return false;
1404             }
1405         }
1406         $silent or $this->mtrace(PHP_EOL.get_string('packagesvalidatingok', 'core_plugin'));
1408         if (!$confirmed) {
1409             return true;
1410         }
1412         // Extract all ZIP packs do the dirroot.
1413         foreach ($plugins as $plugin) {
1414             $silent or $this->mtrace(get_string('packagesextracting', 'core_plugin', $plugin->component), ' ... ');
1415             $zipfile = $zips[$plugin->component];
1416             list($plugintype, $pluginname) = core_component::normalize_component($plugin->component);
1417             $target = $this->get_plugintype_root($plugintype);
1418             if (file_exists($target.'/'.$pluginname)) {
1419                 $this->remove_plugin_folder($this->get_plugin_info($plugin->component));
1420             }
1421             if (!$this->unzip_plugin_file($zipfile, $target, $pluginname)) {
1422                 $silent or $this->mtrace(get_string('error'));
1423                 $silent or $this->mtrace('Unable to unzip '.$zipfile, PHP_EOL, DEBUG_DEVELOPER);
1424                 if (function_exists('opcache_reset')) {
1425                     opcache_reset();
1426                 }
1427                 return false;
1428             }
1429             $silent or $this->mtrace($ok);
1430         }
1431         if (function_exists('opcache_reset')) {
1432             opcache_reset();
1433         }
1435         return true;
1436     }
1438     /**
1439      * Outputs the given message via {@link mtrace()}.
1440      *
1441      * If $debug is provided, then the message is displayed only at the given
1442      * debugging level (e.g. DEBUG_DEVELOPER to display the message only if the
1443      * site has developer debugging level selected).
1444      *
1445      * @param string $msg message
1446      * @param string $eol end of line
1447      * @param null|int $debug null to display always, int only on given debug level
1448      */
1449     protected function mtrace($msg, $eol=PHP_EOL, $debug=null) {
1450         global $CFG;
1452         if ($debug !== null and !debugging(null, $debug)) {
1453             return;
1454         }
1456         mtrace($msg, $eol);
1457     }
1459     /**
1460      * Returns uninstall URL if exists.
1461      *
1462      * @param string $component
1463      * @param string $return either 'overview' or 'manage'
1464      * @return moodle_url uninstall URL, null if uninstall not supported
1465      */
1466     public function get_uninstall_url($component, $return = 'overview') {
1467         if (!$this->can_uninstall_plugin($component)) {
1468             return null;
1469         }
1471         $pluginfo = $this->get_plugin_info($component);
1473         if (is_null($pluginfo)) {
1474             return null;
1475         }
1477         if (method_exists($pluginfo, 'get_uninstall_url')) {
1478             debugging('plugininfo method get_uninstall_url() is deprecated, all plugins should be uninstalled via standard URL only.');
1479             return $pluginfo->get_uninstall_url($return);
1480         }
1482         return $pluginfo->get_default_uninstall_url($return);
1483     }
1485     /**
1486      * Uninstall the given plugin.
1487      *
1488      * Automatically cleans-up all remaining configuration data, log records, events,
1489      * files from the file pool etc.
1490      *
1491      * In the future, the functionality of {@link uninstall_plugin()} function may be moved
1492      * into this method and all the code should be refactored to use it. At the moment, we
1493      * mimic this future behaviour by wrapping that function call.
1494      *
1495      * @param string $component
1496      * @param progress_trace $progress traces the process
1497      * @return bool true on success, false on errors/problems
1498      */
1499     public function uninstall_plugin($component, progress_trace $progress) {
1501         $pluginfo = $this->get_plugin_info($component);
1503         if (is_null($pluginfo)) {
1504             return false;
1505         }
1507         // Give the pluginfo class a chance to execute some steps.
1508         $result = $pluginfo->uninstall($progress);
1509         if (!$result) {
1510             return false;
1511         }
1513         // Call the legacy core function to uninstall the plugin.
1514         ob_start();
1515         uninstall_plugin($pluginfo->type, $pluginfo->name);
1516         $progress->output(ob_get_clean());
1518         return true;
1519     }
1521     /**
1522      * Checks if there are some plugins with a known available update
1523      *
1524      * @return bool true if there is at least one available update
1525      */
1526     public function some_plugins_updatable() {
1527         foreach ($this->get_plugins() as $type => $plugins) {
1528             foreach ($plugins as $plugin) {
1529                 if ($plugin->available_updates()) {
1530                     return true;
1531                 }
1532             }
1533         }
1535         return false;
1536     }
1538     /**
1539      * Returns list of available updates for the given component.
1540      *
1541      * This method should be considered as internal API and is supposed to be
1542      * called by {@link \core\plugininfo\base::available_updates()} only
1543      * to lazy load the data once they are first requested.
1544      *
1545      * @param string $component frankenstyle name of the plugin
1546      * @return null|array array of \core\update\info objects or null
1547      */
1548     public function load_available_updates_for_plugin($component) {
1549         global $CFG;
1551         $provider = \core\update\checker::instance();
1553         if (!$provider->enabled() or during_initial_install()) {
1554             return null;
1555         }
1557         if (isset($CFG->updateminmaturity)) {
1558             $minmaturity = $CFG->updateminmaturity;
1559         } else {
1560             // This can happen during the very first upgrade to 2.3.
1561             $minmaturity = MATURITY_STABLE;
1562         }
1564         return $provider->get_update_info($component, array('minmaturity' => $minmaturity));
1565     }
1567     /**
1568      * Returns a list of all available updates to be installed.
1569      *
1570      * This is used when "update all plugins" action is performed at the
1571      * administration UI screen.
1572      *
1573      * Returns array of remote info objects indexed by the plugin
1574      * component. If there are multiple updates available (typically a mix of
1575      * stable and non-stable ones), we pick the most mature most recent one.
1576      *
1577      * Plugins without explicit maturity are considered more mature than
1578      * release candidates but less mature than explicit stable (this should be
1579      * pretty rare case).
1580      *
1581      * @return array (string)component => (\core\update\remote_info)remoteinfo
1582      */
1583     public function available_updates() {
1585         $updates = array();
1587         foreach ($this->get_plugins() as $type => $plugins) {
1588             foreach ($plugins as $plugin) {
1589                 $availableupdates = $plugin->available_updates();
1590                 if (empty($availableupdates)) {
1591                     continue;
1592                 }
1593                 foreach ($availableupdates as $update) {
1594                     if (empty($updates[$plugin->component])) {
1595                         $updates[$plugin->component] = $update;
1596                         continue;
1597                     }
1598                     $maturitycurrent = $updates[$plugin->component]->maturity;
1599                     if (empty($maturitycurrent)) {
1600                         $maturitycurrent = MATURITY_STABLE - 25;
1601                     }
1602                     $maturityremote = $update->maturity;
1603                     if (empty($maturityremote)) {
1604                         $maturityremote = MATURITY_STABLE - 25;
1605                     }
1606                     if ($maturityremote < $maturitycurrent) {
1607                         continue;
1608                     }
1609                     if ($maturityremote > $maturitycurrent) {
1610                         $updates[$plugin->component] = $update;
1611                         continue;
1612                     }
1613                     if ($update->version > $updates[$plugin->component]->version) {
1614                         $updates[$plugin->component] = $update;
1615                         continue;
1616                     }
1617                 }
1618             }
1619         }
1621         foreach ($updates as $component => $update) {
1622             $remoteinfo = $this->get_remote_plugin_info($component, $update->version, true);
1623             if (empty($remoteinfo) or empty($remoteinfo->version)) {
1624                 unset($updates[$component]);
1625             } else {
1626                 $updates[$component] = $remoteinfo;
1627             }
1628         }
1630         return $updates;
1631     }
1633     /**
1634      * Check to see if the given plugin folder can be removed by the web server process.
1635      *
1636      * @param string $component full frankenstyle component
1637      * @return bool
1638      */
1639     public function is_plugin_folder_removable($component) {
1641         $pluginfo = $this->get_plugin_info($component);
1643         if (is_null($pluginfo)) {
1644             return false;
1645         }
1647         // To be able to remove the plugin folder, its parent must be writable, too.
1648         if (!is_writable(dirname($pluginfo->rootdir))) {
1649             return false;
1650         }
1652         // Check that the folder and all its content is writable (thence removable).
1653         return $this->is_directory_removable($pluginfo->rootdir);
1654     }
1656     /**
1657      * Is it possible to create a new plugin directory for the given plugin type?
1658      *
1659      * @throws coding_exception for invalid plugin types or non-existing plugin type locations
1660      * @param string $plugintype
1661      * @return boolean
1662      */
1663     public function is_plugintype_writable($plugintype) {
1665         $plugintypepath = $this->get_plugintype_root($plugintype);
1667         if (is_null($plugintypepath)) {
1668             throw new coding_exception('Unknown plugin type: '.$plugintype);
1669         }
1671         if ($plugintypepath === false) {
1672             throw new coding_exception('Plugin type location does not exist: '.$plugintype);
1673         }
1675         return is_writable($plugintypepath);
1676     }
1678     /**
1679      * Returns the full path of the root of the given plugin type
1680      *
1681      * Null is returned if the plugin type is not known. False is returned if
1682      * the plugin type root is expected but not found. Otherwise, string is
1683      * returned.
1684      *
1685      * @param string $plugintype
1686      * @return string|bool|null
1687      */
1688     public function get_plugintype_root($plugintype) {
1690         $plugintypepath = null;
1691         foreach (core_component::get_plugin_types() as $type => $fullpath) {
1692             if ($type === $plugintype) {
1693                 $plugintypepath = $fullpath;
1694                 break;
1695             }
1696         }
1697         if (is_null($plugintypepath)) {
1698             return null;
1699         }
1700         if (!is_dir($plugintypepath)) {
1701             return false;
1702         }
1704         return $plugintypepath;
1705     }
1707     /**
1708      * Defines a list of all plugins that were originally shipped in the standard Moodle distribution,
1709      * but are not anymore and are deleted during upgrades.
1710      *
1711      * The main purpose of this list is to hide missing plugins during upgrade.
1712      *
1713      * @param string $type plugin type
1714      * @param string $name plugin name
1715      * @return bool
1716      */
1717     public static function is_deleted_standard_plugin($type, $name) {
1718         // Do not include plugins that were removed during upgrades to versions that are
1719         // not supported as source versions for upgrade any more. For example, at MOODLE_23_STABLE
1720         // branch, listed should be no plugins that were removed at 1.9.x - 2.1.x versions as
1721         // Moodle 2.3 supports upgrades from 2.2.x only.
1722         $plugins = array(
1723             'qformat' => array('blackboard', 'learnwise'),
1724             'auth' => array('radius', 'fc', 'nntp', 'pam', 'pop3', 'imap'),
1725             'block' => array('course_overview', 'messages', 'community', 'participants'),
1726             'cachestore' => array('memcache'),
1727             'enrol' => array('authorize'),
1728             'report' => array('search'),
1729             'repository' => array('alfresco'),
1730             'tinymce' => array('dragmath'),
1731             'tool' => array('bloglevelupgrade', 'qeupgradehelper', 'timezoneimport', 'assignmentupgrade'),
1732             'theme' => array('bootstrapbase', 'clean', 'more', 'afterburner', 'anomaly', 'arialist', 'base',
1733                 'binarius', 'boxxie', 'brick', 'canvas', 'formal_white', 'formfactor', 'fusion', 'leatherbound',
1734                 'magazine', 'mymobile', 'nimble', 'nonzero', 'overlay', 'serenity', 'sky_high', 'splash',
1735                 'standard', 'standardold'),
1736             'webservice' => array('amf'),
1737         );
1739         if (!isset($plugins[$type])) {
1740             return false;
1741         }
1742         return in_array($name, $plugins[$type]);
1743     }
1745     /**
1746      * Defines a white list of all plugins shipped in the standard Moodle distribution
1747      *
1748      * @param string $type
1749      * @return false|array array of standard plugins or false if the type is unknown
1750      */
1751     public static function standard_plugins_list($type) {
1753         $standard_plugins = array(
1755             'antivirus' => array(
1756                 'clamav'
1757             ),
1759             'atto' => array(
1760                 'accessibilitychecker', 'accessibilityhelper', 'align',
1761                 'backcolor', 'bold', 'charmap', 'clear', 'collapse', 'emoticon',
1762                 'equation', 'fontcolor', 'html', 'image', 'indent', 'italic',
1763                 'link', 'managefiles', 'media', 'noautolink', 'orderedlist',
1764                 'recordrtc', 'rtl', 'strike', 'subscript', 'superscript', 'table',
1765                 'title', 'underline', 'undo', 'unorderedlist', 'h5p', 'emojipicker',
1766             ),
1768             'assignment' => array(
1769                 'offline', 'online', 'upload', 'uploadsingle'
1770             ),
1772             'assignsubmission' => array(
1773                 'comments', 'file', 'onlinetext'
1774             ),
1776             'assignfeedback' => array(
1777                 'comments', 'file', 'offline', 'editpdf'
1778             ),
1780             'auth' => array(
1781                 'cas', 'db', 'email', 'ldap', 'lti', 'manual', 'mnet',
1782                 'nologin', 'none', 'oauth2', 'shibboleth', 'webservice'
1783             ),
1785             'availability' => array(
1786                 'completion', 'date', 'grade', 'group', 'grouping', 'profile'
1787             ),
1789             'block' => array(
1790                 'activity_modules', 'activity_results', 'admin_bookmarks', 'badges',
1791                 'blog_menu', 'blog_recent', 'blog_tags', 'calendar_month',
1792                 'calendar_upcoming', 'comments',
1793                 'completionstatus', 'course_list', 'course_summary',
1794                 'feedback', 'globalsearch', 'glossary_random', 'html',
1795                 'login', 'lp', 'mentees', 'mnet_hosts', 'myoverview', 'myprofile',
1796                 'navigation', 'news_items', 'online_users',
1797                 'private_files', 'quiz_results', 'recent_activity', 'recentlyaccesseditems',
1798                 'recentlyaccessedcourses', 'rss_client', 'search_forums', 'section_links',
1799                 'selfcompletion', 'settings', 'site_main_menu',
1800                 'social_activities', 'starredcourses', 'tag_flickr', 'tag_youtube', 'tags', 'timeline'
1801             ),
1803             'booktool' => array(
1804                 'exportimscp', 'importhtml', 'print'
1805             ),
1807             'cachelock' => array(
1808                 'file'
1809             ),
1811             'cachestore' => array(
1812                 'file', 'memcached', 'mongodb', 'session', 'static', 'apcu', 'redis'
1813             ),
1815             'calendartype' => array(
1816                 'gregorian'
1817             ),
1819             'customfield' => array(
1820                 'checkbox', 'date', 'select', 'text', 'textarea'
1821             ),
1823             'coursereport' => array(
1824                 // Deprecated!
1825             ),
1827             'datafield' => array(
1828                 'checkbox', 'date', 'file', 'latlong', 'menu', 'multimenu',
1829                 'number', 'picture', 'radiobutton', 'text', 'textarea', 'url'
1830             ),
1832             'dataformat' => array(
1833                 'html', 'csv', 'json', 'excel', 'ods', 'pdf',
1834             ),
1836             'datapreset' => array(
1837                 'imagegallery'
1838             ),
1840             'fileconverter' => array(
1841                 'unoconv', 'googledrive'
1842             ),
1844             'editor' => array(
1845                 'atto', 'textarea', 'tinymce'
1846             ),
1848             'enrol' => array(
1849                 'category', 'cohort', 'database', 'flatfile',
1850                 'guest', 'imsenterprise', 'ldap', 'lti', 'manual', 'meta', 'mnet',
1851                 'paypal', 'self'
1852             ),
1854             'filter' => array(
1855                 'activitynames', 'algebra', 'censor', 'emailprotect',
1856                 'emoticon', 'displayh5p', 'mathjaxloader', 'mediaplugin', 'multilang', 'tex', 'tidy',
1857                 'urltolink', 'data', 'glossary'
1858             ),
1860             'format' => array(
1861                 'singleactivity', 'social', 'topics', 'weeks'
1862             ),
1864             'forumreport' => array(
1865                 'summary',
1866             ),
1868             'gradeexport' => array(
1869                 'ods', 'txt', 'xls', 'xml'
1870             ),
1872             'gradeimport' => array(
1873                 'csv', 'direct', 'xml'
1874             ),
1876             'gradereport' => array(
1877                 'grader', 'history', 'outcomes', 'overview', 'user', 'singleview'
1878             ),
1880             'gradingform' => array(
1881                 'rubric', 'guide'
1882             ),
1884             'local' => array(
1885             ),
1887             'logstore' => array(
1888                 'database', 'legacy', 'standard',
1889             ),
1891             'ltiservice' => array(
1892                 'gradebookservices', 'memberships', 'profile', 'toolproxy', 'toolsettings', 'basicoutcomes'
1893             ),
1895             'mlbackend' => array(
1896                 'php', 'python'
1897             ),
1899             'media' => array(
1900                 'html5audio', 'html5video', 'swf', 'videojs', 'vimeo', 'youtube'
1901             ),
1903             'message' => array(
1904                 'airnotifier', 'email', 'jabber', 'popup'
1905             ),
1907             'mnetservice' => array(
1908                 'enrol'
1909             ),
1911             'mod' => array(
1912                 'assign', 'assignment', 'book', 'chat', 'choice', 'data', 'feedback', 'folder',
1913                 'forum', 'glossary', 'imscp', 'label', 'lesson', 'lti', 'page',
1914                 'quiz', 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop'
1915             ),
1917             'plagiarism' => array(
1918             ),
1920             'portfolio' => array(
1921                 'boxnet', 'download', 'flickr', 'googledocs', 'mahara', 'picasa'
1922             ),
1924             'profilefield' => array(
1925                 'checkbox', 'datetime', 'menu', 'text', 'textarea'
1926             ),
1928             'qbehaviour' => array(
1929                 'adaptive', 'adaptivenopenalty', 'deferredcbm',
1930                 'deferredfeedback', 'immediatecbm', 'immediatefeedback',
1931                 'informationitem', 'interactive', 'interactivecountback',
1932                 'manualgraded', 'missing'
1933             ),
1935             'qformat' => array(
1936                 'aiken', 'blackboard_six', 'examview', 'gift',
1937                 'missingword', 'multianswer', 'webct',
1938                 'xhtml', 'xml'
1939             ),
1941             'qtype' => array(
1942                 'calculated', 'calculatedmulti', 'calculatedsimple',
1943                 'ddimageortext', 'ddmarker', 'ddwtos', 'description',
1944                 'essay', 'gapselect', 'match', 'missingtype', 'multianswer',
1945                 'multichoice', 'numerical', 'random', 'randomsamatch',
1946                 'shortanswer', 'truefalse'
1947             ),
1949             'quiz' => array(
1950                 'grading', 'overview', 'responses', 'statistics'
1951             ),
1953             'quizaccess' => array(
1954                 'delaybetweenattempts', 'ipaddress', 'numattempts', 'offlineattempts', 'openclosedate',
1955                 'password', 'safebrowser', 'securewindow', 'timelimit'
1956             ),
1958             'report' => array(
1959                 'backups', 'competency', 'completion', 'configlog', 'courseoverview', 'eventlist',
1960                 'insights', 'log', 'loglive', 'outline', 'participation', 'progress', 'questioninstances',
1961                 'security', 'stats', 'performance', 'usersessions'
1962             ),
1964             'repository' => array(
1965                 'areafiles', 'boxnet', 'coursefiles', 'dropbox', 'equella', 'filesystem',
1966                 'flickr', 'flickr_public', 'googledocs', 'local', 'merlot', 'nextcloud',
1967                 'onedrive', 'picasa', 'recent', 'skydrive', 's3', 'upload', 'url', 'user', 'webdav',
1968                 'wikimedia', 'youtube'
1969             ),
1971             'search' => array(
1972                 'simpledb', 'solr'
1973             ),
1975             'scormreport' => array(
1976                 'basic',
1977                 'interactions',
1978                 'graphs',
1979                 'objectives'
1980             ),
1982             'tinymce' => array(
1983                 'ctrlhelp', 'managefiles', 'moodleemoticon', 'moodleimage',
1984                 'moodlemedia', 'moodlenolink', 'pdw', 'spellchecker', 'wrap'
1985             ),
1987             'theme' => array(
1988                 'boost', 'classic'
1989             ),
1991             'tool' => array(
1992                 'analytics', 'availabilityconditions', 'behat', 'capability', 'cohortroles', 'customlang',
1993                 'dataprivacy', 'dbtransfer', 'filetypes', 'generator', 'health', 'httpsreplace', 'innodb', 'installaddon',
1994                 'langimport', 'log', 'lp', 'lpimportcsv', 'lpmigrate', 'messageinbound', 'mobile', 'multilangupgrade',
1995                 'monitor', 'oauth2', 'phpunit', 'policy', 'profiling', 'recyclebin', 'replace', 'spamcleaner', 'task',
1996                 'templatelibrary', 'uploadcourse', 'uploaduser', 'unsuproles', 'usertours', 'xmldb'
1997             ),
1999             'webservice' => array(
2000                 'rest', 'soap', 'xmlrpc'
2001             ),
2003             'workshopallocation' => array(
2004                 'manual', 'random', 'scheduled'
2005             ),
2007             'workshopeval' => array(
2008                 'best'
2009             ),
2011             'workshopform' => array(
2012                 'accumulative', 'comments', 'numerrors', 'rubric'
2013             )
2014         );
2016         if (isset($standard_plugins[$type])) {
2017             return $standard_plugins[$type];
2018         } else {
2019             return false;
2020         }
2021     }
2023     /**
2024      * Remove the current plugin code from the dirroot.
2025      *
2026      * If removing the currently installed version (which happens during
2027      * updates), we archive the code so that the upgrade can be cancelled.
2028      *
2029      * To prevent accidental data-loss, we also archive the existing plugin
2030      * code if cancelling installation of it, so that the developer does not
2031      * loose the only version of their work-in-progress.
2032      *
2033      * @param \core\plugininfo\base $plugin
2034      */
2035     public function remove_plugin_folder(\core\plugininfo\base $plugin) {
2037         if (!$this->is_plugin_folder_removable($plugin->component)) {
2038             throw new moodle_exception('err_removing_unremovable_folder', 'core_plugin', '',
2039                 array('plugin' => $plugin->component, 'rootdir' => $plugin->rootdir),
2040                 'plugin root folder is not removable as expected');
2041         }
2043         if ($plugin->get_status() === self::PLUGIN_STATUS_UPTODATE or $plugin->get_status() === self::PLUGIN_STATUS_NEW) {
2044             $this->archive_plugin_version($plugin);
2045         }
2047         remove_dir($plugin->rootdir);
2048         clearstatcache();
2049         if (function_exists('opcache_reset')) {
2050             opcache_reset();
2051         }
2052     }
2054     /**
2055      * Can the installation of the new plugin be cancelled?
2056      *
2057      * Subplugins can be cancelled only via their parent plugin, not separately
2058      * (they are considered as implicit requirements if distributed together
2059      * with the main package).
2060      *
2061      * @param \core\plugininfo\base $plugin
2062      * @return bool
2063      */
2064     public function can_cancel_plugin_installation(\core\plugininfo\base $plugin) {
2065         global $CFG;
2067         if (!empty($CFG->disableupdateautodeploy)) {
2068             return false;
2069         }
2071         if (empty($plugin) or $plugin->is_standard() or $plugin->is_subplugin()
2072                 or !$this->is_plugin_folder_removable($plugin->component)) {
2073             return false;
2074         }
2076         if ($plugin->get_status() === self::PLUGIN_STATUS_NEW) {
2077             return true;
2078         }
2080         return false;
2081     }
2083     /**
2084      * Can the upgrade of the existing plugin be cancelled?
2085      *
2086      * Subplugins can be cancelled only via their parent plugin, not separately
2087      * (they are considered as implicit requirements if distributed together
2088      * with the main package).
2089      *
2090      * @param \core\plugininfo\base $plugin
2091      * @return bool
2092      */
2093     public function can_cancel_plugin_upgrade(\core\plugininfo\base $plugin) {
2094         global $CFG;
2096         if (!empty($CFG->disableupdateautodeploy)) {
2097             // Cancelling the plugin upgrade is actually installation of the
2098             // previously archived version.
2099             return false;
2100         }
2102         if (empty($plugin) or $plugin->is_standard() or $plugin->is_subplugin()
2103                 or !$this->is_plugin_folder_removable($plugin->component)) {
2104             return false;
2105         }
2107         if ($plugin->get_status() === self::PLUGIN_STATUS_UPGRADE) {
2108             if ($this->get_code_manager()->get_archived_plugin_version($plugin->component, $plugin->versiondb)) {
2109                 return true;
2110             }
2111         }
2113         return false;
2114     }
2116     /**
2117      * Removes the plugin code directory if it is not installed yet.
2118      *
2119      * This is intended for the plugins check screen to give the admin a chance
2120      * to cancel the installation of just unzipped plugin before the database
2121      * upgrade happens.
2122      *
2123      * @param string $component
2124      */
2125     public function cancel_plugin_installation($component) {
2126         global $CFG;
2128         if (!empty($CFG->disableupdateautodeploy)) {
2129             return false;
2130         }
2132         $plugin = $this->get_plugin_info($component);
2134         if ($this->can_cancel_plugin_installation($plugin)) {
2135             $this->remove_plugin_folder($plugin);
2136         }
2138         return false;
2139     }
2141     /**
2142      * Returns plugins, the installation of which can be cancelled.
2143      *
2144      * @return array [(string)component] => (\core\plugininfo\base)plugin
2145      */
2146     public function list_cancellable_installations() {
2147         global $CFG;
2149         if (!empty($CFG->disableupdateautodeploy)) {
2150             return array();
2151         }
2153         $cancellable = array();
2154         foreach ($this->get_plugins() as $type => $plugins) {
2155             foreach ($plugins as $plugin) {
2156                 if ($this->can_cancel_plugin_installation($plugin)) {
2157                     $cancellable[$plugin->component] = $plugin;
2158                 }
2159             }
2160         }
2162         return $cancellable;
2163     }
2165     /**
2166      * Archive the current on-disk plugin code.
2167      *
2168      * @param \core\plugiinfo\base $plugin
2169      * @return bool
2170      */
2171     public function archive_plugin_version(\core\plugininfo\base $plugin) {
2172         return $this->get_code_manager()->archive_plugin_version($plugin->rootdir, $plugin->component, $plugin->versiondisk);
2173     }
2175     /**
2176      * Returns list of all archives that can be installed to cancel the plugin upgrade.
2177      *
2178      * @return array [(string)component] => {(string)->component, (string)->zipfilepath}
2179      */
2180     public function list_restorable_archives() {
2181         global $CFG;
2183         if (!empty($CFG->disableupdateautodeploy)) {
2184             return false;
2185         }
2187         $codeman = $this->get_code_manager();
2188         $restorable = array();
2189         foreach ($this->get_plugins() as $type => $plugins) {
2190             foreach ($plugins as $plugin) {
2191                 if ($this->can_cancel_plugin_upgrade($plugin)) {
2192                     $restorable[$plugin->component] = (object)array(
2193                         'component' => $plugin->component,
2194                         'zipfilepath' => $codeman->get_archived_plugin_version($plugin->component, $plugin->versiondb)
2195                     );
2196                 }
2197             }
2198         }
2200         return $restorable;
2201     }
2203     /**
2204      * Reorders plugin types into a sequence to be displayed
2205      *
2206      * For technical reasons, plugin types returned by {@link core_component::get_plugin_types()} are
2207      * in a certain order that does not need to fit the expected order for the display.
2208      * Particularly, activity modules should be displayed first as they represent the
2209      * real heart of Moodle. They should be followed by other plugin types that are
2210      * used to build the courses (as that is what one expects from LMS). After that,
2211      * other supportive plugin types follow.
2212      *
2213      * @param array $types associative array
2214      * @return array same array with altered order of items
2215      */
2216     protected function reorder_plugin_types(array $types) {
2217         $fix = array('mod' => $types['mod']);
2218         foreach (core_component::get_plugin_list('mod') as $plugin => $fulldir) {
2219             if (!$subtypes = core_component::get_subplugins('mod_'.$plugin)) {
2220                 continue;
2221             }
2222             foreach ($subtypes as $subtype => $ignored) {
2223                 $fix[$subtype] = $types[$subtype];
2224             }
2225         }
2227         $fix['mod']        = $types['mod'];
2228         $fix['block']      = $types['block'];
2229         $fix['qtype']      = $types['qtype'];
2230         $fix['qbehaviour'] = $types['qbehaviour'];
2231         $fix['qformat']    = $types['qformat'];
2232         $fix['filter']     = $types['filter'];
2234         $fix['editor']     = $types['editor'];
2235         foreach (core_component::get_plugin_list('editor') as $plugin => $fulldir) {
2236             if (!$subtypes = core_component::get_subplugins('editor_'.$plugin)) {
2237                 continue;
2238             }
2239             foreach ($subtypes as $subtype => $ignored) {
2240                 $fix[$subtype] = $types[$subtype];
2241             }
2242         }
2244         $fix['enrol'] = $types['enrol'];
2245         $fix['auth']  = $types['auth'];
2246         $fix['tool']  = $types['tool'];
2247         foreach (core_component::get_plugin_list('tool') as $plugin => $fulldir) {
2248             if (!$subtypes = core_component::get_subplugins('tool_'.$plugin)) {
2249                 continue;
2250             }
2251             foreach ($subtypes as $subtype => $ignored) {
2252                 $fix[$subtype] = $types[$subtype];
2253             }
2254         }
2256         foreach ($types as $type => $path) {
2257             if (!isset($fix[$type])) {
2258                 $fix[$type] = $path;
2259             }
2260         }
2261         return $fix;
2262     }
2264     /**
2265      * Check if the given directory can be removed by the web server process.
2266      *
2267      * This recursively checks that the given directory and all its contents
2268      * it writable.
2269      *
2270      * @param string $fullpath
2271      * @return boolean
2272      */
2273     public function is_directory_removable($fullpath) {
2275         if (!is_writable($fullpath)) {
2276             return false;
2277         }
2279         if (is_dir($fullpath)) {
2280             $handle = opendir($fullpath);
2281         } else {
2282             return false;
2283         }
2285         $result = true;
2287         while ($filename = readdir($handle)) {
2289             if ($filename === '.' or $filename === '..') {
2290                 continue;
2291             }
2293             $subfilepath = $fullpath.'/'.$filename;
2295             if (is_dir($subfilepath)) {
2296                 $result = $result && $this->is_directory_removable($subfilepath);
2298             } else {
2299                 $result = $result && is_writable($subfilepath);
2300             }
2301         }
2303         closedir($handle);
2305         return $result;
2306     }
2308     /**
2309      * Helper method that implements common uninstall prerequisites
2310      *
2311      * @param \core\plugininfo\base $pluginfo
2312      * @return bool
2313      */
2314     protected function common_uninstall_check(\core\plugininfo\base $pluginfo) {
2315         global $CFG;
2316         // Check if uninstall is allowed from the GUI.
2317         if (!empty($CFG->uninstallclionly) && (!CLI_SCRIPT)) {
2318             return false;
2319         }
2321         if (!$pluginfo->is_uninstall_allowed()) {
2322             // The plugin's plugininfo class declares it should not be uninstalled.
2323             return false;
2324         }
2326         if ($pluginfo->get_status() === static::PLUGIN_STATUS_NEW) {
2327             // The plugin is not installed. It should be either installed or removed from the disk.
2328             // Relying on this temporary state may be tricky.
2329             return false;
2330         }
2332         if (method_exists($pluginfo, 'get_uninstall_url') and is_null($pluginfo->get_uninstall_url())) {
2333             // Backwards compatibility.
2334             debugging('\core\plugininfo\base subclasses should use is_uninstall_allowed() instead of returning null in get_uninstall_url()',
2335                 DEBUG_DEVELOPER);
2336             return false;
2337         }
2339         return true;
2340     }
2342     /**
2343      * Returns a code_manager instance to be used for the plugins code operations.
2344      *
2345      * @return \core\update\code_manager
2346      */
2347     protected function get_code_manager() {
2349         if ($this->codemanager === null) {
2350             $this->codemanager = new \core\update\code_manager();
2351         }
2353         return $this->codemanager;
2354     }
2356     /**
2357      * Returns a client for https://download.moodle.org/api/
2358      *
2359      * @return \core\update\api
2360      */
2361     protected function get_update_api_client() {
2363         if ($this->updateapiclient === null) {
2364             $this->updateapiclient = \core\update\api::client();
2365         }
2367         return $this->updateapiclient;
2368     }