222fbf32da6405b0953517e86cfbc18a5716f7d9
[moodle.git] / lib / classes / plugin_manager.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Defines classes used for plugins management
19  *
20  * This library provides a unified interface to various plugin types in
21  * Moodle. It is mainly used by the plugins management admin page and the
22  * plugins check page during the upgrade.
23  *
24  * @package    core
25  * @copyright  2011 David Mudrak <david@moodle.com>
26  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27  */
29 defined('MOODLE_INTERNAL') || die();
31 /**
32  * Singleton class providing general plugins management functionality.
33  */
34 class core_plugin_manager {
36     /** the plugin is shipped with standard Moodle distribution */
37     const PLUGIN_SOURCE_STANDARD    = 'std';
38     /** the plugin is added extension */
39     const PLUGIN_SOURCE_EXTENSION   = 'ext';
41     /** the plugin uses neither database nor capabilities, no versions */
42     const PLUGIN_STATUS_NODB        = 'nodb';
43     /** the plugin is up-to-date */
44     const PLUGIN_STATUS_UPTODATE    = 'uptodate';
45     /** the plugin is about to be installed */
46     const PLUGIN_STATUS_NEW         = 'new';
47     /** the plugin is about to be upgraded */
48     const PLUGIN_STATUS_UPGRADE     = 'upgrade';
49     /** the standard plugin is about to be deleted */
50     const PLUGIN_STATUS_DELETE     = 'delete';
51     /** the version at the disk is lower than the one already installed */
52     const PLUGIN_STATUS_DOWNGRADE   = 'downgrade';
53     /** the plugin is installed but missing from disk */
54     const PLUGIN_STATUS_MISSING     = 'missing';
56     /** the given requirement/dependency is fulfilled */
57     const REQUIREMENT_STATUS_OK = 'ok';
58     /** the plugin requires higher core/other plugin version than is currently installed */
59     const REQUIREMENT_STATUS_OUTDATED = 'outdated';
60     /** the required dependency is not installed */
61     const REQUIREMENT_STATUS_MISSING = 'missing';
63     /** the required dependency is available in the plugins directory */
64     const REQUIREMENT_AVAILABLE = 'available';
65     /** the required dependency is available in the plugins directory */
66     const REQUIREMENT_UNAVAILABLE = 'unavailable';
68     /** @var core_plugin_manager holds the singleton instance */
69     protected static $singletoninstance;
70     /** @var array of raw plugins information */
71     protected $pluginsinfo = null;
72     /** @var array of raw subplugins information */
73     protected $subpluginsinfo = null;
74     /** @var array cache information about availability in the plugins directory if requesting "at least" version */
75     protected $remotepluginsinfoatleast = null;
76     /** @var array cache information about availability in the plugins directory if requesting exact version */
77     protected $remotepluginsinfoexact = null;
78     /** @var array list of installed plugins $name=>$version */
79     protected $installedplugins = null;
80     /** @var array list of all enabled plugins $name=>$name */
81     protected $enabledplugins = null;
82     /** @var array list of all enabled plugins $name=>$diskversion */
83     protected $presentplugins = null;
84     /** @var array reordered list of plugin types */
85     protected $plugintypes = null;
86     /** @var \core\update\code_manager code manager to use for plugins code operations */
87     protected $codemanager = null;
88     /** @var \core\update\api client instance to use for accessing download.moodle.org/api/ */
89     protected $updateapiclient = null;
91     /**
92      * Direct initiation not allowed, use the factory method {@link self::instance()}
93      */
94     protected function __construct() {
95     }
97     /**
98      * Sorry, this is singleton
99      */
100     protected function __clone() {
101     }
103     /**
104      * Factory method for this class
105      *
106      * @return core_plugin_manager the singleton instance
107      */
108     public static function instance() {
109         if (is_null(static::$singletoninstance)) {
110             static::$singletoninstance = new static();
111         }
112         return static::$singletoninstance;
113     }
115     /**
116      * Reset all caches.
117      * @param bool $phpunitreset
118      */
119     public static function reset_caches($phpunitreset = false) {
120         if ($phpunitreset) {
121             static::$singletoninstance = null;
122         } else {
123             if (static::$singletoninstance) {
124                 static::$singletoninstance->pluginsinfo = null;
125                 static::$singletoninstance->subpluginsinfo = null;
126                 static::$singletoninstance->remotepluginsinfoatleast = null;
127                 static::$singletoninstance->remotepluginsinfoexact = null;
128                 static::$singletoninstance->installedplugins = null;
129                 static::$singletoninstance->enabledplugins = null;
130                 static::$singletoninstance->presentplugins = null;
131                 static::$singletoninstance->plugintypes = null;
132                 static::$singletoninstance->codemanager = null;
133                 static::$singletoninstance->updateapiclient = null;
134             }
135         }
136         $cache = cache::make('core', 'plugin_manager');
137         $cache->purge();
138     }
140     /**
141      * Returns the result of {@link core_component::get_plugin_types()} ordered for humans
142      *
143      * @see self::reorder_plugin_types()
144      * @return array (string)name => (string)location
145      */
146     public function get_plugin_types() {
147         if (func_num_args() > 0) {
148             if (!func_get_arg(0)) {
149                 throw coding_exception('core_plugin_manager->get_plugin_types() does not support relative paths.');
150             }
151         }
152         if ($this->plugintypes) {
153             return $this->plugintypes;
154         }
156         $this->plugintypes = $this->reorder_plugin_types(core_component::get_plugin_types());
157         return $this->plugintypes;
158     }
160     /**
161      * Load list of installed plugins,
162      * always call before using $this->installedplugins.
163      *
164      * This method is caching results for all plugins.
165      */
166     protected function load_installed_plugins() {
167         global $DB, $CFG;
169         if ($this->installedplugins) {
170             return;
171         }
173         if (empty($CFG->version)) {
174             // Nothing installed yet.
175             $this->installedplugins = array();
176             return;
177         }
179         $cache = cache::make('core', 'plugin_manager');
180         $installed = $cache->get('installed');
182         if (is_array($installed)) {
183             $this->installedplugins = $installed;
184             return;
185         }
187         $this->installedplugins = array();
189         // TODO: Delete this block once Moodle 2.6 or later becomes minimum required version to upgrade.
190         if ($CFG->version < 2013092001.02) {
191             // We did not upgrade the database yet.
192             $modules = $DB->get_records('modules', array(), 'name ASC', 'id, name, version');
193             foreach ($modules as $module) {
194                 $this->installedplugins['mod'][$module->name] = $module->version;
195             }
196             $blocks = $DB->get_records('block', array(), 'name ASC', 'id, name, version');
197             foreach ($blocks as $block) {
198                 $this->installedplugins['block'][$block->name] = $block->version;
199             }
200         }
202         $versions = $DB->get_records('config_plugins', array('name'=>'version'));
203         foreach ($versions as $version) {
204             $parts = explode('_', $version->plugin, 2);
205             if (!isset($parts[1])) {
206                 // Invalid component, there must be at least one "_".
207                 continue;
208             }
209             // Do not verify here if plugin type and name are valid.
210             $this->installedplugins[$parts[0]][$parts[1]] = $version->value;
211         }
213         foreach ($this->installedplugins as $key => $value) {
214             ksort($this->installedplugins[$key]);
215         }
217         $cache->set('installed', $this->installedplugins);
218     }
220     /**
221      * Return list of installed plugins of given type.
222      * @param string $type
223      * @return array $name=>$version
224      */
225     public function get_installed_plugins($type) {
226         $this->load_installed_plugins();
227         if (isset($this->installedplugins[$type])) {
228             return $this->installedplugins[$type];
229         }
230         return array();
231     }
233     /**
234      * Load list of all enabled plugins,
235      * call before using $this->enabledplugins.
236      *
237      * This method is caching results from individual plugin info classes.
238      */
239     protected function load_enabled_plugins() {
240         global $CFG;
242         if ($this->enabledplugins) {
243             return;
244         }
246         if (empty($CFG->version)) {
247             $this->enabledplugins = array();
248             return;
249         }
251         $cache = cache::make('core', 'plugin_manager');
252         $enabled = $cache->get('enabled');
254         if (is_array($enabled)) {
255             $this->enabledplugins = $enabled;
256             return;
257         }
259         $this->enabledplugins = array();
261         require_once($CFG->libdir.'/adminlib.php');
263         $plugintypes = core_component::get_plugin_types();
264         foreach ($plugintypes as $plugintype => $fulldir) {
265             $plugininfoclass = static::resolve_plugininfo_class($plugintype);
266             if (class_exists($plugininfoclass)) {
267                 $enabled = $plugininfoclass::get_enabled_plugins();
268                 if (!is_array($enabled)) {
269                     continue;
270                 }
271                 $this->enabledplugins[$plugintype] = $enabled;
272             }
273         }
275         $cache->set('enabled', $this->enabledplugins);
276     }
278     /**
279      * Get list of enabled plugins of given type,
280      * the result may contain missing plugins.
281      *
282      * @param string $type
283      * @return array|null  list of enabled plugins of this type, null if unknown
284      */
285     public function get_enabled_plugins($type) {
286         $this->load_enabled_plugins();
287         if (isset($this->enabledplugins[$type])) {
288             return $this->enabledplugins[$type];
289         }
290         return null;
291     }
293     /**
294      * Load list of all present plugins - call before using $this->presentplugins.
295      */
296     protected function load_present_plugins() {
297         if ($this->presentplugins) {
298             return;
299         }
301         $cache = cache::make('core', 'plugin_manager');
302         $present = $cache->get('present');
304         if (is_array($present)) {
305             $this->presentplugins = $present;
306             return;
307         }
309         $this->presentplugins = array();
311         $plugintypes = core_component::get_plugin_types();
312         foreach ($plugintypes as $type => $typedir) {
313             $plugs = core_component::get_plugin_list($type);
314             foreach ($plugs as $plug => $fullplug) {
315                 $module = new stdClass();
316                 $plugin = new stdClass();
317                 $plugin->version = null;
318                 include($fullplug.'/version.php');
320                 // Check if the legacy $module syntax is still used.
321                 if (!is_object($module) or (count((array)$module) > 0)) {
322                     debugging('Unsupported $module syntax detected in version.php of the '.$type.'_'.$plug.' plugin.');
323                     $skipcache = true;
324                 }
326                 // Check if the component is properly declared.
327                 if (empty($plugin->component) or ($plugin->component !== $type.'_'.$plug)) {
328                     debugging('Plugin '.$type.'_'.$plug.' does not declare valid $plugin->component in its version.php.');
329                     $skipcache = true;
330                 }
332                 $this->presentplugins[$type][$plug] = $plugin;
333             }
334         }
336         if (empty($skipcache)) {
337             $cache->set('present', $this->presentplugins);
338         }
339     }
341     /**
342      * Get list of present plugins of given type.
343      *
344      * @param string $type
345      * @return array|null  list of presnet plugins $name=>$diskversion, null if unknown
346      */
347     public function get_present_plugins($type) {
348         $this->load_present_plugins();
349         if (isset($this->presentplugins[$type])) {
350             return $this->presentplugins[$type];
351         }
352         return null;
353     }
355     /**
356      * Returns a tree of known plugins and information about them
357      *
358      * @return array 2D array. The first keys are plugin type names (e.g. qtype);
359      *      the second keys are the plugin local name (e.g. multichoice); and
360      *      the values are the corresponding objects extending {@link \core\plugininfo\base}
361      */
362     public function get_plugins() {
363         $this->init_pluginsinfo_property();
365         // Make sure all types are initialised.
366         foreach ($this->pluginsinfo as $plugintype => $list) {
367             if ($list === null) {
368                 $this->get_plugins_of_type($plugintype);
369             }
370         }
372         return $this->pluginsinfo;
373     }
375     /**
376      * Returns list of known plugins of the given type.
377      *
378      * This method returns the subset of the tree returned by {@link self::get_plugins()}.
379      * If the given type is not known, empty array is returned.
380      *
381      * @param string $type plugin type, e.g. 'mod' or 'workshopallocation'
382      * @return \core\plugininfo\base[] (string)plugin name (e.g. 'workshop') => corresponding subclass of {@link \core\plugininfo\base}
383      */
384     public function get_plugins_of_type($type) {
385         global $CFG;
387         $this->init_pluginsinfo_property();
389         if (!array_key_exists($type, $this->pluginsinfo)) {
390             return array();
391         }
393         if (is_array($this->pluginsinfo[$type])) {
394             return $this->pluginsinfo[$type];
395         }
397         $types = core_component::get_plugin_types();
399         if (!isset($types[$type])) {
400             // Orphaned subplugins!
401             $plugintypeclass = static::resolve_plugininfo_class($type);
402             $this->pluginsinfo[$type] = $plugintypeclass::get_plugins($type, null, $plugintypeclass, $this);
403             return $this->pluginsinfo[$type];
404         }
406         /** @var \core\plugininfo\base $plugintypeclass */
407         $plugintypeclass = static::resolve_plugininfo_class($type);
408         $plugins = $plugintypeclass::get_plugins($type, $types[$type], $plugintypeclass, $this);
409         $this->pluginsinfo[$type] = $plugins;
411         return $this->pluginsinfo[$type];
412     }
414     /**
415      * Init placeholder array for plugin infos.
416      */
417     protected function init_pluginsinfo_property() {
418         if (is_array($this->pluginsinfo)) {
419             return;
420         }
421         $this->pluginsinfo = array();
423         $plugintypes = $this->get_plugin_types();
425         foreach ($plugintypes as $plugintype => $plugintyperootdir) {
426             $this->pluginsinfo[$plugintype] = null;
427         }
429         // Add orphaned subplugin types.
430         $this->load_installed_plugins();
431         foreach ($this->installedplugins as $plugintype => $unused) {
432             if (!isset($plugintypes[$plugintype])) {
433                 $this->pluginsinfo[$plugintype] = null;
434             }
435         }
436     }
438     /**
439      * Find the plugin info class for given type.
440      *
441      * @param string $type
442      * @return string name of pluginfo class for give plugin type
443      */
444     public static function resolve_plugininfo_class($type) {
445         $plugintypes = core_component::get_plugin_types();
446         if (!isset($plugintypes[$type])) {
447             return '\core\plugininfo\orphaned';
448         }
450         $parent = core_component::get_subtype_parent($type);
452         if ($parent) {
453             $class = '\\'.$parent.'\plugininfo\\' . $type;
454             if (class_exists($class)) {
455                 $plugintypeclass = $class;
456             } else {
457                 if ($dir = core_component::get_component_directory($parent)) {
458                     // BC only - use namespace instead!
459                     if (file_exists("$dir/adminlib.php")) {
460                         global $CFG;
461                         include_once("$dir/adminlib.php");
462                     }
463                     if (class_exists('plugininfo_' . $type)) {
464                         $plugintypeclass = 'plugininfo_' . $type;
465                         debugging('Class "'.$plugintypeclass.'" is deprecated, migrate to "'.$class.'"', DEBUG_DEVELOPER);
466                     } else {
467                         debugging('Subplugin type "'.$type.'" should define class "'.$class.'"', DEBUG_DEVELOPER);
468                         $plugintypeclass = '\core\plugininfo\general';
469                     }
470                 } else {
471                     $plugintypeclass = '\core\plugininfo\general';
472                 }
473             }
474         } else {
475             $class = '\core\plugininfo\\' . $type;
476             if (class_exists($class)) {
477                 $plugintypeclass = $class;
478             } else {
479                 debugging('All standard types including "'.$type.'" should have plugininfo class!', DEBUG_DEVELOPER);
480                 $plugintypeclass = '\core\plugininfo\general';
481             }
482         }
484         if (!in_array('core\plugininfo\base', class_parents($plugintypeclass))) {
485             throw new coding_exception('Class ' . $plugintypeclass . ' must extend \core\plugininfo\base');
486         }
488         return $plugintypeclass;
489     }
491     /**
492      * Returns list of all known subplugins of the given plugin.
493      *
494      * For plugins that do not provide subplugins (i.e. there is no support for it),
495      * empty array is returned.
496      *
497      * @param string $component full component name, e.g. 'mod_workshop'
498      * @return array (string) component name (e.g. 'workshopallocation_random') => subclass of {@link \core\plugininfo\base}
499      */
500     public function get_subplugins_of_plugin($component) {
502         $pluginfo = $this->get_plugin_info($component);
504         if (is_null($pluginfo)) {
505             return array();
506         }
508         $subplugins = $this->get_subplugins();
510         if (!isset($subplugins[$pluginfo->component])) {
511             return array();
512         }
514         $list = array();
516         foreach ($subplugins[$pluginfo->component] as $subdata) {
517             foreach ($this->get_plugins_of_type($subdata->type) as $subpluginfo) {
518                 $list[$subpluginfo->component] = $subpluginfo;
519             }
520         }
522         return $list;
523     }
525     /**
526      * Returns list of plugins that define their subplugins and the information
527      * about them from the db/subplugins.php file.
528      *
529      * @return array with keys like 'mod_quiz', and values the data from the
530      *      corresponding db/subplugins.php file.
531      */
532     public function get_subplugins() {
534         if (is_array($this->subpluginsinfo)) {
535             return $this->subpluginsinfo;
536         }
538         $plugintypes = core_component::get_plugin_types();
540         $this->subpluginsinfo = array();
541         foreach (core_component::get_plugin_types_with_subplugins() as $type => $ignored) {
542             foreach (core_component::get_plugin_list($type) as $plugin => $componentdir) {
543                 $component = $type.'_'.$plugin;
544                 $subplugins = core_component::get_subplugins($component);
545                 if (!$subplugins) {
546                     continue;
547                 }
548                 $this->subpluginsinfo[$component] = array();
549                 foreach ($subplugins as $subplugintype => $ignored) {
550                     $subplugin = new stdClass();
551                     $subplugin->type = $subplugintype;
552                     $subplugin->typerootdir = $plugintypes[$subplugintype];
553                     $this->subpluginsinfo[$component][$subplugintype] = $subplugin;
554                 }
555             }
556         }
557         return $this->subpluginsinfo;
558     }
560     /**
561      * Returns the name of the plugin that defines the given subplugin type
562      *
563      * If the given subplugin type is not actually a subplugin, returns false.
564      *
565      * @param string $subplugintype the name of subplugin type, eg. workshopform or quiz
566      * @return false|string the name of the parent plugin, eg. mod_workshop
567      */
568     public function get_parent_of_subplugin($subplugintype) {
569         $parent = core_component::get_subtype_parent($subplugintype);
570         if (!$parent) {
571             return false;
572         }
573         return $parent;
574     }
576     /**
577      * Returns a localized name of a given plugin
578      *
579      * @param string $component name of the plugin, eg mod_workshop or auth_ldap
580      * @return string
581      */
582     public function plugin_name($component) {
584         $pluginfo = $this->get_plugin_info($component);
586         if (is_null($pluginfo)) {
587             throw new moodle_exception('err_unknown_plugin', 'core_plugin', '', array('plugin' => $component));
588         }
590         return $pluginfo->displayname;
591     }
593     /**
594      * Returns a localized name of a plugin typed in singular form
595      *
596      * Most plugin types define their names in core_plugin lang file. In case of subplugins,
597      * we try to ask the parent plugin for the name. In the worst case, we will return
598      * the value of the passed $type parameter.
599      *
600      * @param string $type the type of the plugin, e.g. mod or workshopform
601      * @return string
602      */
603     public function plugintype_name($type) {
605         if (get_string_manager()->string_exists('type_' . $type, 'core_plugin')) {
606             // For most plugin types, their names are defined in core_plugin lang file.
607             return get_string('type_' . $type, 'core_plugin');
609         } else if ($parent = $this->get_parent_of_subplugin($type)) {
610             // If this is a subplugin, try to ask the parent plugin for the name.
611             if (get_string_manager()->string_exists('subplugintype_' . $type, $parent)) {
612                 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type, $parent);
613             } else {
614                 return $this->plugin_name($parent) . ' / ' . $type;
615             }
617         } else {
618             return $type;
619         }
620     }
622     /**
623      * Returns a localized name of a plugin type in plural form
624      *
625      * Most plugin types define their names in core_plugin lang file. In case of subplugins,
626      * we try to ask the parent plugin for the name. In the worst case, we will return
627      * the value of the passed $type parameter.
628      *
629      * @param string $type the type of the plugin, e.g. mod or workshopform
630      * @return string
631      */
632     public function plugintype_name_plural($type) {
634         if (get_string_manager()->string_exists('type_' . $type . '_plural', 'core_plugin')) {
635             // For most plugin types, their names are defined in core_plugin lang file.
636             return get_string('type_' . $type . '_plural', 'core_plugin');
638         } else if ($parent = $this->get_parent_of_subplugin($type)) {
639             // If this is a subplugin, try to ask the parent plugin for the name.
640             if (get_string_manager()->string_exists('subplugintype_' . $type . '_plural', $parent)) {
641                 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type . '_plural', $parent);
642             } else {
643                 return $this->plugin_name($parent) . ' / ' . $type;
644             }
646         } else {
647             return $type;
648         }
649     }
651     /**
652      * Returns information about the known plugin, or null
653      *
654      * @param string $component frankenstyle component name.
655      * @return \core\plugininfo\base|null the corresponding plugin information.
656      */
657     public function get_plugin_info($component) {
658         list($type, $name) = core_component::normalize_component($component);
659         $plugins = $this->get_plugins_of_type($type);
660         if (isset($plugins[$name])) {
661             return $plugins[$name];
662         } else {
663             return null;
664         }
665     }
667     /**
668      * Check to see if the current version of the plugin seems to be a checkout of an external repository.
669      *
670      * @param string $component frankenstyle component name
671      * @return false|string
672      */
673     public function plugin_external_source($component) {
675         $plugininfo = $this->get_plugin_info($component);
677         if (is_null($plugininfo)) {
678             return false;
679         }
681         $pluginroot = $plugininfo->rootdir;
683         if (is_dir($pluginroot.'/.git')) {
684             return 'git';
685         }
687         if (is_file($pluginroot.'/.git')) {
688             return 'git-submodule';
689         }
691         if (is_dir($pluginroot.'/CVS')) {
692             return 'cvs';
693         }
695         if (is_dir($pluginroot.'/.svn')) {
696             return 'svn';
697         }
699         if (is_dir($pluginroot.'/.hg')) {
700             return 'mercurial';
701         }
703         return false;
704     }
706     /**
707      * Get a list of any other plugins that require this one.
708      * @param string $component frankenstyle component name.
709      * @return array of frankensyle component names that require this one.
710      */
711     public function other_plugins_that_require($component) {
712         $others = array();
713         foreach ($this->get_plugins() as $type => $plugins) {
714             foreach ($plugins as $plugin) {
715                 $required = $plugin->get_other_required_plugins();
716                 if (isset($required[$component])) {
717                     $others[] = $plugin->component;
718                 }
719             }
720         }
721         return $others;
722     }
724     /**
725      * Check a dependencies list against the list of installed plugins.
726      * @param array $dependencies compenent name to required version or ANY_VERSION.
727      * @return bool true if all the dependencies are satisfied.
728      */
729     public function are_dependencies_satisfied($dependencies) {
730         foreach ($dependencies as $component => $requiredversion) {
731             $otherplugin = $this->get_plugin_info($component);
732             if (is_null($otherplugin)) {
733                 return false;
734             }
736             if ($requiredversion != ANY_VERSION and $otherplugin->versiondisk < $requiredversion) {
737                 return false;
738             }
739         }
741         return true;
742     }
744     /**
745      * Checks all dependencies for all installed plugins
746      *
747      * This is used by install and upgrade. The array passed by reference as the second
748      * argument is populated with the list of plugins that have failed dependencies (note that
749      * a single plugin can appear multiple times in the $failedplugins).
750      *
751      * @param int $moodleversion the version from version.php.
752      * @param array $failedplugins to return the list of plugins with non-satisfied dependencies
753      * @return bool true if all the dependencies are satisfied for all plugins.
754      */
755     public function all_plugins_ok($moodleversion, &$failedplugins = array()) {
757         $return = true;
758         foreach ($this->get_plugins() as $type => $plugins) {
759             foreach ($plugins as $plugin) {
761                 if (!$plugin->is_core_dependency_satisfied($moodleversion)) {
762                     $return = false;
763                     $failedplugins[] = $plugin->component;
764                 }
766                 if (!$this->are_dependencies_satisfied($plugin->get_other_required_plugins())) {
767                     $return = false;
768                     $failedplugins[] = $plugin->component;
769                 }
770             }
771         }
773         return $return;
774     }
776     /**
777      * Resolve requirements and dependencies of a plugin.
778      *
779      * Returns an array of objects describing the requirement/dependency,
780      * indexed by the frankenstyle name of the component. The returned array
781      * can be empty. The objects in the array have following properties:
782      *
783      *  ->(numeric)hasver
784      *  ->(numeric)reqver
785      *  ->(string)status
786      *  ->(string)availability
787      *
788      * @param \core\plugininfo\base $plugin the plugin we are checking
789      * @param null|string|int|double $moodleversion explicit moodle core version to check against, defaults to $CFG->version
790      * @param null|string|int $moodlebranch explicit moodle core branch to check against, defaults to $CFG->branch
791      * @return array of objects
792      */
793     public function resolve_requirements(\core\plugininfo\base $plugin, $moodleversion=null, $moodlebranch=null) {
794         global $CFG;
796         if ($plugin->versiondisk === null) {
797             // Missing from disk, we have no version.php to read from.
798             return array();
799         }
801         if ($moodleversion === null) {
802             $moodleversion = $CFG->version;
803         }
805         if ($moodlebranch === null) {
806             $moodlebranch = $CFG->branch;
807         }
809         $reqs = array();
810         $reqcore = $this->resolve_core_requirements($plugin, $moodleversion);
812         if (!empty($reqcore)) {
813             $reqs['core'] = $reqcore;
814         }
816         foreach ($plugin->get_other_required_plugins() as $reqplug => $reqver) {
817             $reqs[$reqplug] = $this->resolve_dependency_requirements($plugin, $reqplug, $reqver, $moodlebranch);
818         }
820         return $reqs;
821     }
823     /**
824      * Helper method to resolve plugin's requirements on the moodle core.
825      *
826      * @param \core\plugininfo\base $plugin the plugin we are checking
827      * @param string|int|double $moodleversion moodle core branch to check against
828      * @return stdObject
829      */
830     protected function resolve_core_requirements(\core\plugininfo\base $plugin, $moodleversion) {
832         $reqs = (object)array(
833             'hasver' => null,
834             'reqver' => null,
835             'status' => null,
836             'availability' => null,
837         );
839         $reqs->hasver = $moodleversion;
841         if (empty($plugin->versionrequires)) {
842             $reqs->reqver = ANY_VERSION;
843         } else {
844             $reqs->reqver = $plugin->versionrequires;
845         }
847         if ($plugin->is_core_dependency_satisfied($moodleversion)) {
848             $reqs->status = self::REQUIREMENT_STATUS_OK;
849         } else {
850             $reqs->status = self::REQUIREMENT_STATUS_OUTDATED;
851         }
853         return $reqs;
854     }
856     /**
857      * Helper method to resolve plugin's dependecies on other plugins.
858      *
859      * @param \core\plugininfo\base $plugin the plugin we are checking
860      * @param string $otherpluginname
861      * @param string|int $requiredversion
862      * @param string|int $moodlebranch explicit moodle core branch to check against, defaults to $CFG->branch
863      * @return stdClass
864      */
865     protected function resolve_dependency_requirements(\core\plugininfo\base $plugin, $otherpluginname,
866             $requiredversion, $moodlebranch) {
868         $reqs = (object)array(
869             'hasver' => null,
870             'reqver' => null,
871             'status' => null,
872             'availability' => null,
873         );
875         $otherplugin = $this->get_plugin_info($otherpluginname);
877         if ($otherplugin !== null) {
878             // The required plugin is installed.
879             $reqs->hasver = $otherplugin->versiondisk;
880             $reqs->reqver = $requiredversion;
881             // Check it has sufficient version.
882             if ($requiredversion == ANY_VERSION or $otherplugin->versiondisk >= $requiredversion) {
883                 $reqs->status = self::REQUIREMENT_STATUS_OK;
884             } else {
885                 $reqs->status = self::REQUIREMENT_STATUS_OUTDATED;
886             }
888         } else {
889             // The required plugin is not installed.
890             $reqs->hasver = null;
891             $reqs->reqver = $requiredversion;
892             $reqs->status = self::REQUIREMENT_STATUS_MISSING;
893         }
895         if ($reqs->status !== self::REQUIREMENT_STATUS_OK) {
896             if ($this->is_remote_plugin_available($otherpluginname, $requiredversion, false)) {
897                 $reqs->availability = self::REQUIREMENT_AVAILABLE;
898             } else {
899                 $reqs->availability = self::REQUIREMENT_UNAVAILABLE;
900             }
901         }
903         return $reqs;
904     }
906     /**
907      * Is the given plugin version available in the plugins directory?
908      *
909      * See {@link self::get_remote_plugin_info()} for the full explanation of how the $version
910      * parameter is interpretted.
911      *
912      * @param string $component plugin frankenstyle name
913      * @param string|int $version ANY_VERSION or the version number
914      * @param bool $exactmatch false if "given version or higher" is requested
915      * @return boolean
916      */
917     public function is_remote_plugin_available($component, $version, $exactmatch) {
919         $info = $this->get_remote_plugin_info($component, $version, $exactmatch);
921         if (empty($info)) {
922             // There is no available plugin of that name.
923             return false;
924         }
926         if (empty($info->version)) {
927             // Plugin is known, but no suitable version was found.
928             return false;
929         }
931         return true;
932     }
934     /**
935      * Can the given plugin version be installed via the admin UI?
936      *
937      * This check should be used whenever attempting to install a plugin from
938      * the plugins directory (new install, available update, missing dependency).
939      *
940      * @param string $component
941      * @param int $version version number
942      * $param string $reason returned code of the reason why it is not
943      * @return boolean
944      */
945     public function is_remote_plugin_installable($component, $version, &$reason=null) {
946         global $CFG;
948         // Make sure the feature is not disabled.
949         if (!empty($CFG->disableonclickaddoninstall)) {
950             $reason = 'disabled';
951             return false;
952         }
954         // Make sure the version is available.
955         if (!$this->is_remote_plugin_available($component, $version, true)) {
956             $reason = 'remoteunavailable';
957             return false;
958         }
960         // Make sure the plugin type root directory is writable.
961         list($plugintype, $pluginname) = core_component::normalize_component($component);
962         if (!$this->is_plugintype_writable($plugintype)) {
963             $reason = 'notwritableplugintype';
964             return false;
965         }
967         $remoteinfo = $this->get_remote_plugin_info($component, $version, true);
968         $localinfo = $this->get_plugin_info($component);
970         if ($localinfo) {
971             // If the plugin is already present, prevent downgrade.
972             if ($localinfo->versiondb > $remoteinfo->version->version) {
973                 $reason = 'cannotdowngrade';
974                 return false;
975             }
977             // Make sure we have write access to all the existing code.
978             if (is_dir($localinfo->rootdir)) {
979                 if (!$this->is_plugin_folder_removable($component)) {
980                     $reason = 'notwritableplugin';
981                     return false;
982                 }
983             }
984         }
986         // Looks like it could work.
987         return true;
988     }
990     /**
991      * Given the list of remote plugin infos, return just those installable.
992      *
993      * This is typically used on lists returned by
994      * {@link self::available_updates()} or {@link self::missing_dependencies()}
995      * to perform bulk installation of remote plugins.
996      *
997      * @param array $remoteinfos list of {@link \core\update\remote_info}
998      * @return array
999      */
1000     public function filter_installable($remoteinfos) {
1002         if (empty($remoteinfos)) {
1003             return array();
1004         }
1005         $installable = array();
1006         foreach ($remoteinfos as $index => $remoteinfo) {
1007             if ($this->is_remote_plugin_installable($remoteinfo->component, $remoteinfo->version->version)) {
1008                 $installable[$index] = $remoteinfo;
1009             }
1010         }
1011         return $installable;
1012     }
1014     /**
1015      * Returns information about a plugin in the plugins directory.
1016      *
1017      * This is typically used when checking for available dependencies (in
1018      * which case the $version represents minimal version we need), or
1019      * when installing an available update or a new plugin from the plugins
1020      * directory (in which case the $version is exact version we are
1021      * interested in). The interpretation of the $version is controlled
1022      * by the $exactmatch argument.
1023      *
1024      * If a plugin with the given component name is found, data about the
1025      * plugin are returned as an object. The ->version property of the object
1026      * contains the information about the particular plugin version that
1027      * matches best the given critera. The ->version property is false if no
1028      * suitable version of the plugin was found (yet the plugin itself is
1029      * known).
1030      *
1031      * See {@link \core\update\api::validate_pluginfo_format()} for the
1032      * returned data structure.
1033      *
1034      * @param string $component plugin frankenstyle name
1035      * @param string|int $version ANY_VERSION or the version number
1036      * @param bool $exactmatch false if "given version or higher" is requested
1037      * @return \core\update\remote_info|bool
1038      */
1039     public function get_remote_plugin_info($component, $version, $exactmatch) {
1041         if ($exactmatch and $version == ANY_VERSION) {
1042             throw new coding_exception('Invalid request for exactly any version, it does not make sense.');
1043         }
1045         $client = $this->get_update_api_client();
1047         if ($exactmatch) {
1048             // Use client's get_plugin_info() method.
1049             if (!isset($this->remotepluginsinfoexact[$component][$version])) {
1050                 $this->remotepluginsinfoexact[$component][$version] = $client->get_plugin_info($component, $version);
1051             }
1052             return $this->remotepluginsinfoexact[$component][$version];
1054         } else {
1055             // Use client's find_plugin() method.
1056             if (!isset($this->remotepluginsinfoatleast[$component][$version])) {
1057                 $this->remotepluginsinfoatleast[$component][$version] = $client->find_plugin($component, $version);
1058             }
1059             return $this->remotepluginsinfoatleast[$component][$version];
1060         }
1061     }
1063     /**
1064      * Obtain the plugin ZIP file from the given URL
1065      *
1066      * The caller is supposed to know both downloads URL and the MD5 hash of
1067      * the ZIP contents in advance, typically by using the API requests against
1068      * the plugins directory.
1069      *
1070      * @param string $url
1071      * @param string $md5
1072      * @return string|bool full path to the file, false on error
1073      */
1074     public function get_remote_plugin_zip($url, $md5) {
1075         return $this->get_code_manager()->get_remote_plugin_zip($url, $md5);
1076     }
1078     /**
1079      * Extracts the saved plugin ZIP file.
1080      *
1081      * Returns the list of files found in the ZIP. The format of that list is
1082      * array of (string)filerelpath => (bool|string) where the array value is
1083      * either true or a string describing the problematic file.
1084      *
1085      * @see zip_packer::extract_to_pathname()
1086      * @param string $zipfilepath full path to the saved ZIP file
1087      * @param string $targetdir full path to the directory to extract the ZIP file to
1088      * @param string $rootdir explicitly rename the root directory of the ZIP into this non-empty value
1089      * @param array list of extracted files as returned by {@link zip_packer::extract_to_pathname()}
1090      */
1091     public function unzip_plugin_file($zipfilepath, $targetdir, $rootdir = '') {
1092         return $this->get_code_manager()->unzip_plugin_file($zipfilepath, $targetdir, $rootdir);
1093     }
1095     /**
1096      * Detects the plugin's name from its ZIP file.
1097      *
1098      * Plugin ZIP packages are expected to contain a single directory and the
1099      * directory name would become the plugin name once extracted to the Moodle
1100      * dirroot.
1101      *
1102      * @param string $zipfilepath full path to the ZIP files
1103      * @return string|bool false on error
1104      */
1105     public function get_plugin_zip_root_dir($zipfilepath) {
1106         return $this->get_code_manager()->get_plugin_zip_root_dir($zipfilepath);
1107     }
1109     /**
1110      * Return a list of missing dependencies.
1111      *
1112      * This should provide the full list of plugins that should be installed to
1113      * fulfill the requirements of all plugins, if possible.
1114      *
1115      * @param bool $availableonly return only available missing dependencies
1116      * @return array of \core\update\remote_info|bool indexed by the component name
1117      */
1118     public function missing_dependencies($availableonly=false) {
1120         $dependencies = array();
1122         foreach ($this->get_plugins() as $plugintype => $pluginfos) {
1123             foreach ($pluginfos as $pluginname => $pluginfo) {
1124                 foreach ($this->resolve_requirements($pluginfo) as $reqname => $reqinfo) {
1125                     if ($reqname === 'core') {
1126                         continue;
1127                     }
1128                     if ($reqinfo->status != self::REQUIREMENT_STATUS_OK) {
1129                         if ($reqinfo->availability == self::REQUIREMENT_AVAILABLE) {
1130                             $remoteinfo = $this->get_remote_plugin_info($reqname, $reqinfo->reqver, false);
1132                             if (empty($dependencies[$reqname])) {
1133                                 $dependencies[$reqname] = $remoteinfo;
1134                             } else {
1135                                 // If resolving requirements has led to two different versions of the same
1136                                 // remote plugin, pick the higher version. This can happen in cases like one
1137                                 // plugin requiring ANY_VERSION and another plugin requiring specific higher
1138                                 // version with lower maturity of a remote plugin.
1139                                 if ($remoteinfo->version->version > $dependencies[$reqname]->version->version) {
1140                                     $dependencies[$reqname] = $remoteinfo;
1141                                 }
1142                             }
1144                         } else {
1145                             if (!isset($dependencies[$reqname])) {
1146                                 // Unable to find a plugin fulfilling the requirements.
1147                                 $dependencies[$reqname] = false;
1148                             }
1149                         }
1150                     }
1151                 }
1152             }
1153         }
1155         if ($availableonly) {
1156             foreach ($dependencies as $component => $info) {
1157                 if (empty($info) or empty($info->version)) {
1158                     unset($dependencies[$component]);
1159                 }
1160             }
1161         }
1163         return $dependencies;
1164     }
1166     /**
1167      * Is it possible to uninstall the given plugin?
1168      *
1169      * False is returned if the plugininfo subclass declares the uninstall should
1170      * not be allowed via {@link \core\plugininfo\base::is_uninstall_allowed()} or if the
1171      * core vetoes it (e.g. becase the plugin or some of its subplugins is required
1172      * by some other installed plugin).
1173      *
1174      * @param string $component full frankenstyle name, e.g. mod_foobar
1175      * @return bool
1176      */
1177     public function can_uninstall_plugin($component) {
1179         $pluginfo = $this->get_plugin_info($component);
1181         if (is_null($pluginfo)) {
1182             return false;
1183         }
1185         if (!$this->common_uninstall_check($pluginfo)) {
1186             return false;
1187         }
1189         // Verify only if something else requires the subplugins, do not verify their common_uninstall_check()!
1190         $subplugins = $this->get_subplugins_of_plugin($pluginfo->component);
1191         foreach ($subplugins as $subpluginfo) {
1192             // Check if there are some other plugins requiring this subplugin
1193             // (but the parent and siblings).
1194             foreach ($this->other_plugins_that_require($subpluginfo->component) as $requiresme) {
1195                 $ismyparent = ($pluginfo->component === $requiresme);
1196                 $ismysibling = in_array($requiresme, array_keys($subplugins));
1197                 if (!$ismyparent and !$ismysibling) {
1198                     return false;
1199                 }
1200             }
1201         }
1203         // Check if there are some other plugins requiring this plugin
1204         // (but its subplugins).
1205         foreach ($this->other_plugins_that_require($pluginfo->component) as $requiresme) {
1206             $ismysubplugin = in_array($requiresme, array_keys($subplugins));
1207             if (!$ismysubplugin) {
1208                 return false;
1209             }
1210         }
1212         return true;
1213     }
1215     /**
1216      * Perform the installation of plugins.
1217      *
1218      * If used for installation of remote plugins from the Moodle Plugins
1219      * directory, the $plugins must be list of {@link \core\update\remote_info}
1220      * object that represent installable remote plugins. The caller can use
1221      * {@link self::filter_installable()} to prepare the list.
1222      *
1223      * If used for installation of plugins from locally available ZIP files,
1224      * the $plugins should be list of objects with properties ->component and
1225      * ->zipfilepath.
1226      *
1227      * The method uses {@link mtrace()} to produce direct output and can be
1228      * used in both web and cli interfaces.
1229      *
1230      * @param array $plugins list of plugins
1231      * @param bool $confirmed should the files be really deployed into the dirroot?
1232      * @param bool $silent perform without output
1233      * @return bool true on success
1234      */
1235     public function install_plugins(array $plugins, $confirmed, $silent) {
1236         global $CFG, $OUTPUT;
1238         if (empty($plugins)) {
1239             return false;
1240         }
1242         $ok = get_string('ok', 'core');
1244         // Let admins know they can expect more verbose output.
1245         $silent or $this->mtrace(get_string('packagesdebug', 'core_plugin'), PHP_EOL, DEBUG_NORMAL);
1247         // Download all ZIP packages if we do not have them yet.
1248         $zips = array();
1249         foreach ($plugins as $plugin) {
1250             if ($plugin instanceof \core\update\remote_info) {
1251                 $zips[$plugin->component] = $this->get_remote_plugin_zip($plugin->version->downloadurl,
1252                     $plugin->version->downloadmd5);
1253                 $silent or $this->mtrace(get_string('packagesdownloading', 'core_plugin', $plugin->component), ' ... ');
1254                 $silent or $this->mtrace(PHP_EOL.' <- '.$plugin->version->downloadurl, '', DEBUG_DEVELOPER);
1255                 $silent or $this->mtrace(PHP_EOL.' -> '.$zips[$plugin->component], ' ... ', DEBUG_DEVELOPER);
1256                 if (!$zips[$plugin->component]) {
1257                     $silent or $this->mtrace(get_string('error'));
1258                     return false;
1259                 }
1260                 $silent or $this->mtrace($ok);
1261             } else {
1262                 if (empty($plugin->zipfilepath)) {
1263                     throw new coding_exception('Unexpected data structure provided');
1264                 }
1265                 $zips[$plugin->component] = $plugin->zipfilepath;
1266                 $silent or $this->mtrace('ZIP '.$plugin->zipfilepath, PHP_EOL, DEBUG_DEVELOPER);
1267             }
1268         }
1270         // Validate all downloaded packages.
1271         foreach ($plugins as $plugin) {
1272             $zipfile = $zips[$plugin->component];
1273             $silent or $this->mtrace(get_string('packagesvalidating', 'core_plugin', $plugin->component), ' ... ');
1274             list($plugintype, $pluginname) = core_component::normalize_component($plugin->component);
1275             $tmp = make_request_directory();
1276             $zipcontents = $this->unzip_plugin_file($zipfile, $tmp, $pluginname);
1277             if (empty($zipcontents)) {
1278                 $silent or $this->mtrace(get_string('error'));
1279                 $silent or $this->mtrace('Unable to unzip '.$zipfile, PHP_EOL, DEBUG_DEVELOPER);
1280                 return false;
1281             }
1283             $validator = \core\update\validator::instance($tmp, $zipcontents);
1284             $validator->assert_plugin_type($plugintype);
1285             $validator->assert_moodle_version($CFG->version);
1286             // TODO Check for missing dependencies during validation.
1287             $result = $validator->execute();
1288             if (!$silent) {
1289                 $result ? $this->mtrace($ok) : $this->mtrace(get_string('error'));
1290                 foreach ($validator->get_messages() as $message) {
1291                     if ($message->level === $validator::INFO) {
1292                         // Display [OK] validation messages only if debugging mode is DEBUG_NORMAL.
1293                         $level = DEBUG_NORMAL;
1294                     } else if ($message->level === $validator::DEBUG) {
1295                         // Display [Debug] validation messages only if debugging mode is DEBUG_ALL.
1296                         $level = DEBUG_ALL;
1297                     } else {
1298                         // Display [Warning] and [Error] always.
1299                         $level = null;
1300                     }
1301                     if ($message->level === $validator::WARNING and !CLI_SCRIPT) {
1302                         $this->mtrace('  <strong>['.$validator->message_level_name($message->level).']</strong>', ' ', $level);
1303                     } else {
1304                         $this->mtrace('  ['.$validator->message_level_name($message->level).']', ' ', $level);
1305                     }
1306                     $this->mtrace($validator->message_code_name($message->msgcode), ' ', $level);
1307                     $info = $validator->message_code_info($message->msgcode, $message->addinfo);
1308                     if ($info) {
1309                         $this->mtrace('['.s($info).']', ' ', $level);
1310                     } else if (is_string($message->addinfo)) {
1311                         $this->mtrace('['.s($message->addinfo, true).']', ' ', $level);
1312                     } else {
1313                         $this->mtrace('['.s(json_encode($message->addinfo, true)).']', ' ', $level);
1314                     }
1315                     if ($icon = $validator->message_help_icon($message->msgcode)) {
1316                         if (CLI_SCRIPT) {
1317                             $this->mtrace(PHP_EOL.'  ^^^ '.get_string('help').': '.
1318                                 get_string($icon->identifier.'_help', $icon->component), '', $level);
1319                         } else {
1320                             $this->mtrace($OUTPUT->render($icon), ' ', $level);
1321                         }
1322                     }
1323                     $this->mtrace(PHP_EOL, '', $level);
1324                 }
1325             }
1326             if (!$result) {
1327                 $silent or $this->mtrace(get_string('packagesvalidatingfailed', 'core_plugin'));
1328                 return false;
1329             }
1330         }
1331         $silent or $this->mtrace(PHP_EOL.get_string('packagesvalidatingok', 'core_plugin'));
1333         if (!$confirmed) {
1334             return true;
1335         }
1337         // Extract all ZIP packs do the dirroot.
1338         foreach ($plugins as $plugin) {
1339             $silent or $this->mtrace(get_string('packagesextracting', 'core_plugin', $plugin->component), ' ... ');
1340             $zipfile = $zips[$plugin->component];
1341             list($plugintype, $pluginname) = core_component::normalize_component($plugin->component);
1342             $target = $this->get_plugintype_root($plugintype);
1343             if (file_exists($target.'/'.$pluginname)) {
1344                 $current = $this->get_plugin_info($plugin->component);
1345                 if ($current->versiondb and $current->versiondb == $current->versiondisk) {
1346                     // TODO Archive existing version so that we can revert.
1347                 }
1348                 remove_dir($target.'/'.$pluginname);
1349             }
1350             if (!$this->unzip_plugin_file($zipfile, $target, $pluginname)) {
1351                 $silent or $this->mtrace(get_string('error'));
1352                 $silent or $this->mtrace('Unable to unzip '.$zipfile, PHP_EOL, DEBUG_DEVELOPER);
1353                 if (function_exists('opcache_reset')) {
1354                     opcache_reset();
1355                 }
1356                 return false;
1357             }
1358             $silent or $this->mtrace($ok);
1359         }
1360         if (function_exists('opcache_reset')) {
1361             opcache_reset();
1362         }
1364         return true;
1365     }
1367     /**
1368      * Outputs the given message via {@link mtrace()}.
1369      *
1370      * If $debug is provided, then the message is displayed only at the given
1371      * debugging level (e.g. DEBUG_DEVELOPER to display the message only if the
1372      * site has developer debugging level selected).
1373      *
1374      * @param string $msg message
1375      * @param string $eol end of line
1376      * @param null|int $debug null to display always, int only on given debug level
1377      */
1378     protected function mtrace($msg, $eol=PHP_EOL, $debug=null) {
1379         global $CFG;
1381         if ($debug !== null and !debugging(null, $debug)) {
1382             return;
1383         }
1385         mtrace($msg, $eol);
1386     }
1388     /**
1389      * Returns uninstall URL if exists.
1390      *
1391      * @param string $component
1392      * @param string $return either 'overview' or 'manage'
1393      * @return moodle_url uninstall URL, null if uninstall not supported
1394      */
1395     public function get_uninstall_url($component, $return = 'overview') {
1396         if (!$this->can_uninstall_plugin($component)) {
1397             return null;
1398         }
1400         $pluginfo = $this->get_plugin_info($component);
1402         if (is_null($pluginfo)) {
1403             return null;
1404         }
1406         if (method_exists($pluginfo, 'get_uninstall_url')) {
1407             debugging('plugininfo method get_uninstall_url() is deprecated, all plugins should be uninstalled via standard URL only.');
1408             return $pluginfo->get_uninstall_url($return);
1409         }
1411         return $pluginfo->get_default_uninstall_url($return);
1412     }
1414     /**
1415      * Uninstall the given plugin.
1416      *
1417      * Automatically cleans-up all remaining configuration data, log records, events,
1418      * files from the file pool etc.
1419      *
1420      * In the future, the functionality of {@link uninstall_plugin()} function may be moved
1421      * into this method and all the code should be refactored to use it. At the moment, we
1422      * mimic this future behaviour by wrapping that function call.
1423      *
1424      * @param string $component
1425      * @param progress_trace $progress traces the process
1426      * @return bool true on success, false on errors/problems
1427      */
1428     public function uninstall_plugin($component, progress_trace $progress) {
1430         $pluginfo = $this->get_plugin_info($component);
1432         if (is_null($pluginfo)) {
1433             return false;
1434         }
1436         // Give the pluginfo class a chance to execute some steps.
1437         $result = $pluginfo->uninstall($progress);
1438         if (!$result) {
1439             return false;
1440         }
1442         // Call the legacy core function to uninstall the plugin.
1443         ob_start();
1444         uninstall_plugin($pluginfo->type, $pluginfo->name);
1445         $progress->output(ob_get_clean());
1447         return true;
1448     }
1450     /**
1451      * Checks if there are some plugins with a known available update
1452      *
1453      * @return bool true if there is at least one available update
1454      */
1455     public function some_plugins_updatable() {
1456         foreach ($this->get_plugins() as $type => $plugins) {
1457             foreach ($plugins as $plugin) {
1458                 if ($plugin->available_updates()) {
1459                     return true;
1460                 }
1461             }
1462         }
1464         return false;
1465     }
1467     /**
1468      * Returns list of available updates for the given component.
1469      *
1470      * This method should be considered as internal API and is supposed to be
1471      * called by {@link \core\plugininfo\base::available_updates()} only
1472      * to lazy load the data once they are first requested.
1473      *
1474      * @param string $component frankenstyle name of the plugin
1475      * @return null|array array of \core\update\info objects or null
1476      */
1477     public function load_available_updates_for_plugin($component) {
1478         global $CFG;
1480         $provider = \core\update\checker::instance();
1482         if (!$provider->enabled() or during_initial_install()) {
1483             return null;
1484         }
1486         if (isset($CFG->updateminmaturity)) {
1487             $minmaturity = $CFG->updateminmaturity;
1488         } else {
1489             // This can happen during the very first upgrade to 2.3.
1490             $minmaturity = MATURITY_STABLE;
1491         }
1493         return $provider->get_update_info($component, array('minmaturity' => $minmaturity));
1494     }
1496     /**
1497      * Returns a list of all available updates to be installed.
1498      *
1499      * This is used when "update all plugins" action is performed at the
1500      * administration UI screen.
1501      *
1502      * Returns array of remote info objects indexed by the plugin
1503      * component. If there are multiple updates available (typically a mix of
1504      * stable and non-stable ones), we pick the most mature most recent one.
1505      *
1506      * Plugins without explicit maturity are considered more mature than
1507      * release candidates but less mature than explicit stable (this should be
1508      * pretty rare case).
1509      *
1510      * @return array (string)component => (\core\update\remote_info)remoteinfo
1511      */
1512     public function available_updates() {
1514         $updates = array();
1516         foreach ($this->get_plugins() as $type => $plugins) {
1517             foreach ($plugins as $plugin) {
1518                 $availableupdates = $plugin->available_updates();
1519                 if (empty($availableupdates)) {
1520                     continue;
1521                 }
1522                 foreach ($availableupdates as $update) {
1523                     if (empty($updates[$plugin->component])) {
1524                         $updates[$plugin->component] = $update;
1525                         continue;
1526                     }
1527                     $maturitycurrent = $updates[$plugin->component]->maturity;
1528                     if (empty($maturitycurrent)) {
1529                         $maturitycurrent = MATURITY_STABLE - 25;
1530                     }
1531                     $maturityremote = $update->maturity;
1532                     if (empty($maturityremote)) {
1533                         $maturityremote = MATURITY_STABLE - 25;
1534                     }
1535                     if ($maturityremote < $maturitycurrent) {
1536                         continue;
1537                     }
1538                     if ($maturityremote > $maturitycurrent) {
1539                         $updates[$plugin->component] = $update;
1540                         continue;
1541                     }
1542                     if ($update->version > $updates[$plugin->component]->version) {
1543                         $updates[$plugin->component] = $update;
1544                         continue;
1545                     }
1546                 }
1547             }
1548         }
1550         foreach ($updates as $component => $update) {
1551             $remoteinfo = $this->get_remote_plugin_info($component, $update->version, true);
1552             if (empty($remoteinfo) or empty($remoteinfo->version)) {
1553                 unset($updates[$component]);
1554             } else {
1555                 $updates[$component] = $remoteinfo;
1556             }
1557         }
1559         return $updates;
1560     }
1562     /**
1563      * Check to see if the given plugin folder can be removed by the web server process.
1564      *
1565      * @param string $component full frankenstyle component
1566      * @return bool
1567      */
1568     public function is_plugin_folder_removable($component) {
1570         $pluginfo = $this->get_plugin_info($component);
1572         if (is_null($pluginfo)) {
1573             return false;
1574         }
1576         // To be able to remove the plugin folder, its parent must be writable, too.
1577         if (!is_writable(dirname($pluginfo->rootdir))) {
1578             return false;
1579         }
1581         // Check that the folder and all its content is writable (thence removable).
1582         return $this->is_directory_removable($pluginfo->rootdir);
1583     }
1585     /**
1586      * Is it possible to create a new plugin directory for the given plugin type?
1587      *
1588      * @throws coding_exception for invalid plugin types or non-existing plugin type locations
1589      * @param string $plugintype
1590      * @return boolean
1591      */
1592     public function is_plugintype_writable($plugintype) {
1594         $plugintypepath = $this->get_plugintype_root($plugintype);
1596         if (is_null($plugintypepath)) {
1597             throw new coding_exception('Unknown plugin type: '.$plugintype);
1598         }
1600         if ($plugintypepath === false) {
1601             throw new coding_exception('Plugin type location does not exist: '.$plugintype);
1602         }
1604         return is_writable($plugintypepath);
1605     }
1607     /**
1608      * Returns the full path of the root of the given plugin type
1609      *
1610      * Null is returned if the plugin type is not known. False is returned if
1611      * the plugin type root is expected but not found. Otherwise, string is
1612      * returned.
1613      *
1614      * @param string $plugintype
1615      * @return string|bool|null
1616      */
1617     public function get_plugintype_root($plugintype) {
1619         $plugintypepath = null;
1620         foreach (core_component::get_plugin_types() as $type => $fullpath) {
1621             if ($type === $plugintype) {
1622                 $plugintypepath = $fullpath;
1623                 break;
1624             }
1625         }
1626         if (is_null($plugintypepath)) {
1627             return null;
1628         }
1629         if (!is_dir($plugintypepath)) {
1630             return false;
1631         }
1633         return $plugintypepath;
1634     }
1636     /**
1637      * Defines a list of all plugins that were originally shipped in the standard Moodle distribution,
1638      * but are not anymore and are deleted during upgrades.
1639      *
1640      * The main purpose of this list is to hide missing plugins during upgrade.
1641      *
1642      * @param string $type plugin type
1643      * @param string $name plugin name
1644      * @return bool
1645      */
1646     public static function is_deleted_standard_plugin($type, $name) {
1647         // Do not include plugins that were removed during upgrades to versions that are
1648         // not supported as source versions for upgrade any more. For example, at MOODLE_23_STABLE
1649         // branch, listed should be no plugins that were removed at 1.9.x - 2.1.x versions as
1650         // Moodle 2.3 supports upgrades from 2.2.x only.
1651         $plugins = array(
1652             'qformat' => array('blackboard', 'learnwise'),
1653             'enrol' => array('authorize'),
1654             'tinymce' => array('dragmath'),
1655             'tool' => array('bloglevelupgrade', 'qeupgradehelper', 'timezoneimport'),
1656             'theme' => array('mymobile', 'afterburner', 'anomaly', 'arialist', 'binarius', 'boxxie', 'brick', 'formal_white',
1657                 'formfactor', 'fusion', 'leatherbound', 'magazine', 'nimble', 'nonzero', 'overlay', 'serenity', 'sky_high',
1658                 'splash', 'standard', 'standardold'),
1659         );
1661         if (!isset($plugins[$type])) {
1662             return false;
1663         }
1664         return in_array($name, $plugins[$type]);
1665     }
1667     /**
1668      * Defines a white list of all plugins shipped in the standard Moodle distribution
1669      *
1670      * @param string $type
1671      * @return false|array array of standard plugins or false if the type is unknown
1672      */
1673     public static function standard_plugins_list($type) {
1675         $standard_plugins = array(
1677             'atto' => array(
1678                 'accessibilitychecker', 'accessibilityhelper', 'align',
1679                 'backcolor', 'bold', 'charmap', 'clear', 'collapse', 'emoticon',
1680                 'equation', 'fontcolor', 'html', 'image', 'indent', 'italic',
1681                 'link', 'managefiles', 'media', 'noautolink', 'orderedlist',
1682                 'rtl', 'strike', 'subscript', 'superscript', 'table', 'title',
1683                 'underline', 'undo', 'unorderedlist'
1684             ),
1686             'assignment' => array(
1687                 'offline', 'online', 'upload', 'uploadsingle'
1688             ),
1690             'assignsubmission' => array(
1691                 'comments', 'file', 'onlinetext'
1692             ),
1694             'assignfeedback' => array(
1695                 'comments', 'file', 'offline', 'editpdf'
1696             ),
1698             'auth' => array(
1699                 'cas', 'db', 'email', 'fc', 'imap', 'ldap', 'manual', 'mnet',
1700                 'nntp', 'nologin', 'none', 'pam', 'pop3', 'radius',
1701                 'shibboleth', 'webservice'
1702             ),
1704             'availability' => array(
1705                 'completion', 'date', 'grade', 'group', 'grouping', 'profile'
1706             ),
1708             'block' => array(
1709                 'activity_modules', 'activity_results', 'admin_bookmarks', 'badges',
1710                 'blog_menu', 'blog_recent', 'blog_tags', 'calendar_month',
1711                 'calendar_upcoming', 'comments', 'community',
1712                 'completionstatus', 'course_list', 'course_overview',
1713                 'course_summary', 'feedback', 'glossary_random', 'html',
1714                 'login', 'mentees', 'messages', 'mnet_hosts', 'myprofile',
1715                 'navigation', 'news_items', 'online_users', 'participants',
1716                 'private_files', 'quiz_results', 'recent_activity',
1717                 'rss_client', 'search_forums', 'section_links',
1718                 'selfcompletion', 'settings', 'site_main_menu',
1719                 'social_activities', 'tag_flickr', 'tag_youtube', 'tags'
1720             ),
1722             'booktool' => array(
1723                 'exportimscp', 'importhtml', 'print'
1724             ),
1726             'cachelock' => array(
1727                 'file'
1728             ),
1730             'cachestore' => array(
1731                 'file', 'memcache', 'memcached', 'mongodb', 'session', 'static'
1732             ),
1734             'calendartype' => array(
1735                 'gregorian'
1736             ),
1738             'coursereport' => array(
1739                 // Deprecated!
1740             ),
1742             'datafield' => array(
1743                 'checkbox', 'date', 'file', 'latlong', 'menu', 'multimenu',
1744                 'number', 'picture', 'radiobutton', 'text', 'textarea', 'url'
1745             ),
1747             'datapreset' => array(
1748                 'imagegallery'
1749             ),
1751             'editor' => array(
1752                 'atto', 'textarea', 'tinymce'
1753             ),
1755             'enrol' => array(
1756                 'category', 'cohort', 'database', 'flatfile',
1757                 'guest', 'imsenterprise', 'ldap', 'manual', 'meta', 'mnet',
1758                 'paypal', 'self'
1759             ),
1761             'filter' => array(
1762                 'activitynames', 'algebra', 'censor', 'emailprotect',
1763                 'emoticon', 'mathjaxloader', 'mediaplugin', 'multilang', 'tex', 'tidy',
1764                 'urltolink', 'data', 'glossary'
1765             ),
1767             'format' => array(
1768                 'singleactivity', 'social', 'topics', 'weeks'
1769             ),
1771             'gradeexport' => array(
1772                 'ods', 'txt', 'xls', 'xml'
1773             ),
1775             'gradeimport' => array(
1776                 'csv', 'direct', 'xml'
1777             ),
1779             'gradereport' => array(
1780                 'grader', 'history', 'outcomes', 'overview', 'user', 'singleview'
1781             ),
1783             'gradingform' => array(
1784                 'rubric', 'guide'
1785             ),
1787             'local' => array(
1788             ),
1790             'logstore' => array(
1791                 'database', 'legacy', 'standard',
1792             ),
1794             'ltiservice' => array(
1795                 'memberships', 'profile', 'toolproxy', 'toolsettings'
1796             ),
1798             'message' => array(
1799                 'airnotifier', 'email', 'jabber', 'popup'
1800             ),
1802             'mnetservice' => array(
1803                 'enrol'
1804             ),
1806             'mod' => array(
1807                 'assign', 'assignment', 'book', 'chat', 'choice', 'data', 'feedback', 'folder',
1808                 'forum', 'glossary', 'imscp', 'label', 'lesson', 'lti', 'page',
1809                 'quiz', 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop'
1810             ),
1812             'plagiarism' => array(
1813             ),
1815             'portfolio' => array(
1816                 'boxnet', 'download', 'flickr', 'googledocs', 'mahara', 'picasa'
1817             ),
1819             'profilefield' => array(
1820                 'checkbox', 'datetime', 'menu', 'text', 'textarea'
1821             ),
1823             'qbehaviour' => array(
1824                 'adaptive', 'adaptivenopenalty', 'deferredcbm',
1825                 'deferredfeedback', 'immediatecbm', 'immediatefeedback',
1826                 'informationitem', 'interactive', 'interactivecountback',
1827                 'manualgraded', 'missing'
1828             ),
1830             'qformat' => array(
1831                 'aiken', 'blackboard_six', 'examview', 'gift',
1832                 'missingword', 'multianswer', 'webct',
1833                 'xhtml', 'xml'
1834             ),
1836             'qtype' => array(
1837                 'calculated', 'calculatedmulti', 'calculatedsimple',
1838                 'ddimageortext', 'ddmarker', 'ddwtos', 'description',
1839                 'essay', 'gapselect', 'match', 'missingtype', 'multianswer',
1840                 'multichoice', 'numerical', 'random', 'randomsamatch',
1841                 'shortanswer', 'truefalse'
1842             ),
1844             'quiz' => array(
1845                 'grading', 'overview', 'responses', 'statistics'
1846             ),
1848             'quizaccess' => array(
1849                 'delaybetweenattempts', 'ipaddress', 'numattempts', 'openclosedate',
1850                 'password', 'safebrowser', 'securewindow', 'timelimit'
1851             ),
1853             'report' => array(
1854                 'backups', 'completion', 'configlog', 'courseoverview', 'eventlist',
1855                 'log', 'loglive', 'outline', 'participation', 'progress', 'questioninstances', 'security', 'stats', 'performance',
1856                 'usersessions',
1857             ),
1859             'repository' => array(
1860                 'alfresco', 'areafiles', 'boxnet', 'coursefiles', 'dropbox', 'equella', 'filesystem',
1861                 'flickr', 'flickr_public', 'googledocs', 'local', 'merlot',
1862                 'picasa', 'recent', 'skydrive', 's3', 'upload', 'url', 'user', 'webdav',
1863                 'wikimedia', 'youtube'
1864             ),
1866             'scormreport' => array(
1867                 'basic',
1868                 'interactions',
1869                 'graphs',
1870                 'objectives'
1871             ),
1873             'tinymce' => array(
1874                 'ctrlhelp', 'managefiles', 'moodleemoticon', 'moodleimage',
1875                 'moodlemedia', 'moodlenolink', 'pdw', 'spellchecker', 'wrap'
1876             ),
1878             'theme' => array(
1879                 'base', 'bootstrapbase', 'canvas', 'clean', 'more'
1880             ),
1882             'tool' => array(
1883                 'assignmentupgrade', 'availabilityconditions', 'behat', 'capability', 'customlang',
1884                 'dbtransfer', 'filetypes', 'generator', 'health', 'innodb', 'installaddon',
1885                 'langimport', 'log', 'messageinbound', 'multilangupgrade', 'monitor', 'phpunit', 'profiling',
1886                 'replace', 'spamcleaner', 'task', 'templatelibrary',
1887                 'unittest', 'uploadcourse', 'uploaduser', 'unsuproles', 'xmldb'
1888             ),
1890             'webservice' => array(
1891                 'amf', 'rest', 'soap', 'xmlrpc'
1892             ),
1894             'workshopallocation' => array(
1895                 'manual', 'random', 'scheduled'
1896             ),
1898             'workshopeval' => array(
1899                 'best'
1900             ),
1902             'workshopform' => array(
1903                 'accumulative', 'comments', 'numerrors', 'rubric'
1904             )
1905         );
1907         if (isset($standard_plugins[$type])) {
1908             return $standard_plugins[$type];
1909         } else {
1910             return false;
1911         }
1912     }
1914     /**
1915      * Can the installation of the new plugin be cancelled?
1916      *
1917      * @param \core\plugininfo\base $plugin
1918      * @return bool
1919      */
1920     public function can_cancel_plugin_installation(\core\plugininfo\base $plugin) {
1922         if (empty($plugin) or $plugin->is_standard() or !$this->is_plugin_folder_removable($plugin->component)) {
1923             return false;
1924         }
1926         if ($plugin->get_status() === self::PLUGIN_STATUS_NEW) {
1927             return true;
1928         }
1930         return false;
1931     }
1933     /**
1934      * Removes the plugin code directory if it is not installed yet.
1935      *
1936      * This is intended for the plugins check screen to give the admin a chance
1937      * to cancel the installation of just unzipped plugin before the database
1938      * upgrade happens.
1939      *
1940      * @param string $component
1941      * @return bool
1942      */
1943     public function cancel_plugin_installation($component) {
1945         $plugin = $this->get_plugin_info($component);
1947         if ($this->can_cancel_plugin_installation($plugin)) {
1948             if ($this->archive_plugin_version($plugin)) {
1949                 return remove_dir($plugin->rootdir);
1950             }
1951         }
1953         return false;
1954     }
1956     /**
1957      * Cancels installation of all new additional plugins.
1958      */
1959     public function cancel_all_plugin_installations() {
1961         foreach ($this->get_plugins() as $type => $plugins) {
1962             foreach ($plugins as $plugin) {
1963                 if ($this->can_cancel_plugin_installation($plugin)) {
1964                     $this->cancel_plugin_installation($plugin->component);
1965                 }
1966             }
1967         }
1968     }
1970     /**
1971      * Archive the current on-disk plugin code.
1972      *
1973      * @param \core\plugiinfo\base $plugin
1974      * @return bool
1975      */
1976     public function archive_plugin_version(\core\plugininfo\base $plugin) {
1977         // TODO use code_manager to do it.
1978         return true;
1979     }
1981     /**
1982      * Reorders plugin types into a sequence to be displayed
1983      *
1984      * For technical reasons, plugin types returned by {@link core_component::get_plugin_types()} are
1985      * in a certain order that does not need to fit the expected order for the display.
1986      * Particularly, activity modules should be displayed first as they represent the
1987      * real heart of Moodle. They should be followed by other plugin types that are
1988      * used to build the courses (as that is what one expects from LMS). After that,
1989      * other supportive plugin types follow.
1990      *
1991      * @param array $types associative array
1992      * @return array same array with altered order of items
1993      */
1994     protected function reorder_plugin_types(array $types) {
1995         $fix = array('mod' => $types['mod']);
1996         foreach (core_component::get_plugin_list('mod') as $plugin => $fulldir) {
1997             if (!$subtypes = core_component::get_subplugins('mod_'.$plugin)) {
1998                 continue;
1999             }
2000             foreach ($subtypes as $subtype => $ignored) {
2001                 $fix[$subtype] = $types[$subtype];
2002             }
2003         }
2005         $fix['mod']        = $types['mod'];
2006         $fix['block']      = $types['block'];
2007         $fix['qtype']      = $types['qtype'];
2008         $fix['qbehaviour'] = $types['qbehaviour'];
2009         $fix['qformat']    = $types['qformat'];
2010         $fix['filter']     = $types['filter'];
2012         $fix['editor']     = $types['editor'];
2013         foreach (core_component::get_plugin_list('editor') as $plugin => $fulldir) {
2014             if (!$subtypes = core_component::get_subplugins('editor_'.$plugin)) {
2015                 continue;
2016             }
2017             foreach ($subtypes as $subtype => $ignored) {
2018                 $fix[$subtype] = $types[$subtype];
2019             }
2020         }
2022         $fix['enrol'] = $types['enrol'];
2023         $fix['auth']  = $types['auth'];
2024         $fix['tool']  = $types['tool'];
2025         foreach (core_component::get_plugin_list('tool') as $plugin => $fulldir) {
2026             if (!$subtypes = core_component::get_subplugins('tool_'.$plugin)) {
2027                 continue;
2028             }
2029             foreach ($subtypes as $subtype => $ignored) {
2030                 $fix[$subtype] = $types[$subtype];
2031             }
2032         }
2034         foreach ($types as $type => $path) {
2035             if (!isset($fix[$type])) {
2036                 $fix[$type] = $path;
2037             }
2038         }
2039         return $fix;
2040     }
2042     /**
2043      * Check if the given directory can be removed by the web server process.
2044      *
2045      * This recursively checks that the given directory and all its contents
2046      * it writable.
2047      *
2048      * @param string $fullpath
2049      * @return boolean
2050      */
2051     public function is_directory_removable($fullpath) {
2053         if (!is_writable($fullpath)) {
2054             return false;
2055         }
2057         if (is_dir($fullpath)) {
2058             $handle = opendir($fullpath);
2059         } else {
2060             return false;
2061         }
2063         $result = true;
2065         while ($filename = readdir($handle)) {
2067             if ($filename === '.' or $filename === '..') {
2068                 continue;
2069             }
2071             $subfilepath = $fullpath.'/'.$filename;
2073             if (is_dir($subfilepath)) {
2074                 $result = $result && $this->is_directory_removable($subfilepath);
2076             } else {
2077                 $result = $result && is_writable($subfilepath);
2078             }
2079         }
2081         closedir($handle);
2083         return $result;
2084     }
2086     /**
2087      * Helper method that implements common uninstall prerequisites
2088      *
2089      * @param \core\plugininfo\base $pluginfo
2090      * @return bool
2091      */
2092     protected function common_uninstall_check(\core\plugininfo\base $pluginfo) {
2094         if (!$pluginfo->is_uninstall_allowed()) {
2095             // The plugin's plugininfo class declares it should not be uninstalled.
2096             return false;
2097         }
2099         if ($pluginfo->get_status() === static::PLUGIN_STATUS_NEW) {
2100             // The plugin is not installed. It should be either installed or removed from the disk.
2101             // Relying on this temporary state may be tricky.
2102             return false;
2103         }
2105         if (method_exists($pluginfo, 'get_uninstall_url') and is_null($pluginfo->get_uninstall_url())) {
2106             // Backwards compatibility.
2107             debugging('\core\plugininfo\base subclasses should use is_uninstall_allowed() instead of returning null in get_uninstall_url()',
2108                 DEBUG_DEVELOPER);
2109             return false;
2110         }
2112         return true;
2113     }
2115     /**
2116      * Returns a code_manager instance to be used for the plugins code operations.
2117      *
2118      * @return \core\update\code_manager
2119      */
2120     protected function get_code_manager() {
2122         if ($this->codemanager === null) {
2123             $this->codemanager = new \core\update\code_manager();
2124         }
2126         return $this->codemanager;
2127     }
2129     /**
2130      * Returns a client for https://download.moodle.org/api/
2131      *
2132      * @return \core\update\api
2133      */
2134     protected function get_update_api_client() {
2136         if ($this->updateapiclient === null) {
2137             $this->updateapiclient = \core\update\api::client();
2138         }
2140         return $this->updateapiclient;
2141     }