Merge branch 'w45_MDL-42525_m26_plugininfoperf' of https://github.com/skodak/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     /** @var core_plugin_manager holds the singleton instance */
57     protected static $singletoninstance;
58     /** @var array of raw plugins information */
59     protected $pluginsinfo = null;
60     /** @var array of raw subplugins information */
61     protected $subpluginsinfo = null;
62     /** @var array list of installed plugins $name=>$version */
63     protected $installedplugins = null;
64     /** @var array list of all enabled plugins $name=>$name */
65     protected $enabledplugins = null;
66     /** @var array list of all enabled plugins $name=>$diskversion */
67     protected $presentplugins = null;
68     /** @var array reordered list of plugin types */
69     protected $plugintypes = null;
71     /**
72      * Direct initiation not allowed, use the factory method {@link self::instance()}
73      */
74     protected function __construct() {
75     }
77     /**
78      * Sorry, this is singleton
79      */
80     protected function __clone() {
81     }
83     /**
84      * Factory method for this class
85      *
86      * @return core_plugin_manager the singleton instance
87      */
88     public static function instance() {
89         if (is_null(self::$singletoninstance)) {
90             self::$singletoninstance = new self();
91         }
92         return self::$singletoninstance;
93     }
95     /**
96      * Reset all caches.
97      * @param bool $phpunitreset
98      */
99     public static function reset_caches($phpunitreset = false) {
100         if ($phpunitreset) {
101             self::$singletoninstance = null;
102         } else {
103             if (self::$singletoninstance) {
104                 self::$singletoninstance->pluginsinfo = null;
105                 self::$singletoninstance->subpluginsinfo = null;
106                 self::$singletoninstance->installedplugins = null;
107                 self::$singletoninstance->enabledplugins = null;
108                 self::$singletoninstance->presentplugins = null;
109                 self::$singletoninstance->plugintypes = null;
110             }
111         }
112         $cache = cache::make('core', 'plugin_manager');
113         $cache->purge();
114     }
116     /**
117      * Returns the result of {@link core_component::get_plugin_types()} ordered for humans
118      *
119      * @see self::reorder_plugin_types()
120      * @return array (string)name => (string)location
121      */
122     public function get_plugin_types() {
123         if (func_num_args() > 0) {
124             if (!func_get_arg(0)) {
125                 throw coding_exception('core_plugin_manager->get_plugin_types() does not support relative paths.');
126             }
127         }
128         if ($this->plugintypes) {
129             return $this->plugintypes;
130         }
132         $this->plugintypes = $this->reorder_plugin_types(core_component::get_plugin_types());
133         return $this->plugintypes;
134     }
136     /**
137      * Load list of installed plugins,
138      * always call before using $this->installedplugins.
139      *
140      * This method is caching results for all plugins.
141      */
142     protected function load_installed_plugins() {
143         global $DB, $CFG;
145         if ($this->installedplugins) {
146             return;
147         }
149         if (empty($CFG->version)) {
150             // Nothing installed yet.
151             $this->installedplugins = array();
152             return;
153         }
155         $cache = cache::make('core', 'plugin_manager');
156         $installed = $cache->get('installed');
158         if (is_array($installed)) {
159             $this->installedplugins = $installed;
160             return;
161         }
163         $this->installedplugins = array();
165         if ($CFG->version < 2013092001.02) {
166             // We did not upgrade the database yet.
167             $modules = $DB->get_records('modules', array(), 'name ASC', 'id, name, version');
168             foreach ($modules as $module) {
169                 $this->installedplugins['mod'][$module->name] = $module->version;
170             }
171             $blocks = $DB->get_records('block', array(), 'name ASC', 'id, name, version');
172             foreach ($blocks as $block) {
173                 $this->installedplugins['block'][$block->name] = $block->version;
174             }
175         }
177         $versions = $DB->get_records('config_plugins', array('name'=>'version'));
178         foreach ($versions as $version) {
179             $parts = explode('_', $version->plugin, 2);
180             if (!isset($parts[1])) {
181                 // Invalid component, there must be at least one "_".
182                 continue;
183             }
184             // Do not verify here if plugin type and name are valid.
185             $this->installedplugins[$parts[0]][$parts[1]] = $version->value;
186         }
188         foreach ($this->installedplugins as $key => $value) {
189             ksort($this->installedplugins[$key]);
190         }
192         $cache->set('installed', $this->installedplugins);
193     }
195     /**
196      * Return list of installed plugins of given type.
197      * @param string $type
198      * @return array $name=>$version
199      */
200     public function get_installed_plugins($type) {
201         $this->load_installed_plugins();
202         if (isset($this->installedplugins[$type])) {
203             return $this->installedplugins[$type];
204         }
205         return array();
206     }
208     /**
209      * Load list of all enabled plugins,
210      * call before using $this->enabledplugins.
211      *
212      * This method is caching results from individual plugin info classes.
213      */
214     protected function load_enabled_plugins() {
215         global $CFG;
217         if ($this->enabledplugins) {
218             return;
219         }
221         if (empty($CFG->version)) {
222             $this->enabledplugins = array();
223             return;
224         }
226         $cache = cache::make('core', 'plugin_manager');
227         $enabled = $cache->get('enabled');
229         if (is_array($enabled)) {
230             $this->enabledplugins = $enabled;
231             return;
232         }
234         $this->enabledplugins = array();
236         require_once($CFG->libdir.'/adminlib.php');
238         $plugintypes = core_component::get_plugin_types();
239         foreach ($plugintypes as $plugintype => $fulldir) {
240             $plugininfoclass = self::resolve_plugininfo_class($plugintype);
241             if (class_exists($plugininfoclass)) {
242                 $enabled = $plugininfoclass::get_enabled_plugins();
243                 if (!is_array($enabled)) {
244                     continue;
245                 }
246                 $this->enabledplugins[$plugintype] = $enabled;
247             }
248         }
250         $cache->set('enabled', $this->enabledplugins);
251     }
253     /**
254      * Get list of enabled plugins of given type,
255      * the result may contain missing plugins.
256      *
257      * @param string $type
258      * @return array|null  list of enabled plugins of this type, null if unknown
259      */
260     public function get_enabled_plugins($type) {
261         $this->load_enabled_plugins();
262         if (isset($this->enabledplugins[$type])) {
263             return $this->enabledplugins[$type];
264         }
265         return null;
266     }
268     /**
269      * Load list of all present plugins - call before using $this->presentplugins.
270      */
271     protected function load_present_plugins() {
272         if ($this->presentplugins) {
273             return;
274         }
276         $cache = cache::make('core', 'plugin_manager');
277         $present = $cache->get('present');
279         if (is_array($present)) {
280             $this->presentplugins = $present;
281             return;
282         }
284         $this->presentplugins = array();
286         $plugintypes = core_component::get_plugin_types();
287         foreach ($plugintypes as $type => $typedir) {
288             $plugs = core_component::get_plugin_list($type);
289             foreach ($plugs as $plug => $fullplug) {
290                 $plugin = new stdClass();
291                 $plugin->version = null;
292                 $module = $plugin;
293                 @include($fullplug.'/version.php');
294                 $this->presentplugins[$type][$plug] = $plugin;
295             }
296         }
298         $cache->set('present', $this->presentplugins);
299     }
301     /**
302      * Get list of present plugins of given type.
303      *
304      * @param string $type
305      * @return array|null  list of presnet plugins $name=>$diskversion, null if unknown
306      */
307     public function get_present_plugins($type) {
308         $this->load_present_plugins();
309         if (isset($this->presentplugins[$type])) {
310             return $this->presentplugins[$type];
311         }
312         return null;
313     }
315     /**
316      * Returns a tree of known plugins and information about them
317      *
318      * @return array 2D array. The first keys are plugin type names (e.g. qtype);
319      *      the second keys are the plugin local name (e.g. multichoice); and
320      *      the values are the corresponding objects extending {@link \core\plugininfo\base}
321      */
322     public function get_plugins() {
323         $this->init_pluginsinfo_property();
325         // Make sure all types are initialised.
326         foreach ($this->pluginsinfo as $plugintype => $list) {
327             if ($list === null) {
328                 $this->get_plugins_of_type($plugintype);
329             }
330         }
332         return $this->pluginsinfo;
333     }
335     /**
336      * Returns list of known plugins of the given type.
337      *
338      * This method returns the subset of the tree returned by {@link self::get_plugins()}.
339      * If the given type is not known, empty array is returned.
340      *
341      * @param string $type plugin type, e.g. 'mod' or 'workshopallocation'
342      * @return \core\plugininfo\base[] (string)plugin name (e.g. 'workshop') => corresponding subclass of {@link \core\plugininfo\base}
343      */
344     public function get_plugins_of_type($type) {
345         global $CFG;
347         $this->init_pluginsinfo_property();
349         if (!array_key_exists($type, $this->pluginsinfo)) {
350             return array();
351         }
353         if (is_array($this->pluginsinfo[$type])) {
354             return $this->pluginsinfo[$type];
355         }
357         $types = core_component::get_plugin_types();
359         if (!isset($types[$type])) {
360             // Orphaned subplugins!
361             $plugintypeclass = self::resolve_plugininfo_class($type);
362             $this->pluginsinfo[$type] = $plugintypeclass::get_plugins($type, null, $plugintypeclass);
363             return $this->pluginsinfo[$type];
364         }
366         /** @var \core\plugininfo\base $plugintypeclass */
367         $plugintypeclass = self::resolve_plugininfo_class($type);
368         $plugins = $plugintypeclass::get_plugins($type, $types[$type], $plugintypeclass);
369         $this->pluginsinfo[$type] = $plugins;
371         if (empty($CFG->disableupdatenotifications) and !during_initial_install()) {
372             // Append the information about available updates provided by {@link \core\update\checker()}.
373             $provider = \core\update\checker::instance();
374             foreach ($plugins as $plugininfoholder) {
375                 $plugininfoholder->check_available_updates($provider);
376             }
377         }
379         return $this->pluginsinfo[$type];
380     }
382     /**
383      * Init placeholder array for plugin infos.
384      */
385     protected function init_pluginsinfo_property() {
386         if (is_array($this->pluginsinfo)) {
387             return;
388         }
389         $this->pluginsinfo = array();
391         $plugintypes = $this->get_plugin_types();
393         foreach ($plugintypes as $plugintype => $plugintyperootdir) {
394             $this->pluginsinfo[$plugintype] = null;
395         }
397         // Add orphaned subplugin types.
398         $this->load_installed_plugins();
399         foreach ($this->installedplugins as $plugintype => $unused) {
400             if (!isset($plugintypes[$plugintype])) {
401                 $this->pluginsinfo[$plugintype] = null;
402             }
403         }
404     }
406     /**
407      * Find the plugin info class for given type.
408      *
409      * @param string $type
410      * @return string name of pluginfo class for give plugin type
411      */
412     public static function resolve_plugininfo_class($type) {
413         $plugintypes = core_component::get_plugin_types();
414         if (!isset($plugintypes[$type])) {
415             return '\core\plugininfo\orphaned';
416         }
418         $parent = core_component::get_subtype_parent($type);
420         if ($parent) {
421             $class = '\\'.$parent.'\plugininfo\\' . $type;
422             if (class_exists($class)) {
423                 $plugintypeclass = $class;
424             } else {
425                 if ($dir = core_component::get_component_directory($parent)) {
426                     // BC only - use namespace instead!
427                     if (file_exists("$dir/adminlib.php")) {
428                         global $CFG;
429                         include_once("$dir/adminlib.php");
430                     }
431                     if (class_exists('plugininfo_' . $type)) {
432                         $plugintypeclass = 'plugininfo_' . $type;
433                         debugging('Class "'.$plugintypeclass.'" is deprecated, migrate to "'.$class.'"', DEBUG_DEVELOPER);
434                     } else {
435                         debugging('Subplugin type "'.$type.'" should define class "'.$class.'"', DEBUG_DEVELOPER);
436                         $plugintypeclass = '\core\plugininfo\general';
437                     }
438                 } else {
439                     $plugintypeclass = '\core\plugininfo\general';
440                 }
441             }
442         } else {
443             $class = '\core\plugininfo\\' . $type;
444             if (class_exists($class)) {
445                 $plugintypeclass = $class;
446             } else {
447                 debugging('All standard types including "'.$type.'" should have plugininfo class!', DEBUG_DEVELOPER);
448                 $plugintypeclass = '\core\plugininfo\general';
449             }
450         }
452         if (!in_array('core\plugininfo\base', class_parents($plugintypeclass))) {
453             throw new coding_exception('Class ' . $plugintypeclass . ' must extend \core\plugininfo\base');
454         }
456         return $plugintypeclass;
457     }
459     /**
460      * Returns list of all known subplugins of the given plugin.
461      *
462      * For plugins that do not provide subplugins (i.e. there is no support for it),
463      * empty array is returned.
464      *
465      * @param string $component full component name, e.g. 'mod_workshop'
466      * @return array (string) component name (e.g. 'workshopallocation_random') => subclass of {@link \core\plugininfo\base}
467      */
468     public function get_subplugins_of_plugin($component) {
470         $pluginfo = $this->get_plugin_info($component);
472         if (is_null($pluginfo)) {
473             return array();
474         }
476         $subplugins = $this->get_subplugins();
478         if (!isset($subplugins[$pluginfo->component])) {
479             return array();
480         }
482         $list = array();
484         foreach ($subplugins[$pluginfo->component] as $subdata) {
485             foreach ($this->get_plugins_of_type($subdata->type) as $subpluginfo) {
486                 $list[$subpluginfo->component] = $subpluginfo;
487             }
488         }
490         return $list;
491     }
493     /**
494      * Returns list of plugins that define their subplugins and the information
495      * about them from the db/subplugins.php file.
496      *
497      * @return array with keys like 'mod_quiz', and values the data from the
498      *      corresponding db/subplugins.php file.
499      */
500     public function get_subplugins() {
502         if (is_array($this->subpluginsinfo)) {
503             return $this->subpluginsinfo;
504         }
506         $plugintypes = core_component::get_plugin_types();
508         $this->subpluginsinfo = array();
509         foreach (core_component::get_plugin_types_with_subplugins() as $type => $ignored) {
510             foreach (core_component::get_plugin_list($type) as $plugin => $componentdir) {
511                 $component = $type.'_'.$plugin;
512                 $subplugins = core_component::get_subplugins($component);
513                 if (!$subplugins) {
514                     continue;
515                 }
516                 $this->subpluginsinfo[$component] = array();
517                 foreach ($subplugins as $subplugintype => $ignored) {
518                     $subplugin = new stdClass();
519                     $subplugin->type = $subplugintype;
520                     $subplugin->typerootdir = $plugintypes[$subplugintype];
521                     $this->subpluginsinfo[$component][$subplugintype] = $subplugin;
522                 }
523             }
524         }
525         return $this->subpluginsinfo;
526     }
528     /**
529      * Returns the name of the plugin that defines the given subplugin type
530      *
531      * If the given subplugin type is not actually a subplugin, returns false.
532      *
533      * @param string $subplugintype the name of subplugin type, eg. workshopform or quiz
534      * @return false|string the name of the parent plugin, eg. mod_workshop
535      */
536     public function get_parent_of_subplugin($subplugintype) {
537         $parent = core_component::get_subtype_parent($subplugintype);
538         if (!$parent) {
539             return false;
540         }
541         return $parent;
542     }
544     /**
545      * Returns a localized name of a given plugin
546      *
547      * @param string $component name of the plugin, eg mod_workshop or auth_ldap
548      * @return string
549      */
550     public function plugin_name($component) {
552         $pluginfo = $this->get_plugin_info($component);
554         if (is_null($pluginfo)) {
555             throw new moodle_exception('err_unknown_plugin', 'core_plugin', '', array('plugin' => $component));
556         }
558         return $pluginfo->displayname;
559     }
561     /**
562      * Returns a localized name of a plugin typed in singular form
563      *
564      * Most plugin types define their names in core_plugin lang file. In case of subplugins,
565      * we try to ask the parent plugin for the name. In the worst case, we will return
566      * the value of the passed $type parameter.
567      *
568      * @param string $type the type of the plugin, e.g. mod or workshopform
569      * @return string
570      */
571     public function plugintype_name($type) {
573         if (get_string_manager()->string_exists('type_' . $type, 'core_plugin')) {
574             // For most plugin types, their names are defined in core_plugin lang file.
575             return get_string('type_' . $type, 'core_plugin');
577         } else if ($parent = $this->get_parent_of_subplugin($type)) {
578             // If this is a subplugin, try to ask the parent plugin for the name.
579             if (get_string_manager()->string_exists('subplugintype_' . $type, $parent)) {
580                 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type, $parent);
581             } else {
582                 return $this->plugin_name($parent) . ' / ' . $type;
583             }
585         } else {
586             return $type;
587         }
588     }
590     /**
591      * Returns a localized name of a plugin type in plural form
592      *
593      * Most plugin types define their names in core_plugin lang file. In case of subplugins,
594      * we try to ask the parent plugin for the name. In the worst case, we will return
595      * the value of the passed $type parameter.
596      *
597      * @param string $type the type of the plugin, e.g. mod or workshopform
598      * @return string
599      */
600     public function plugintype_name_plural($type) {
602         if (get_string_manager()->string_exists('type_' . $type . '_plural', 'core_plugin')) {
603             // For most plugin types, their names are defined in core_plugin lang file.
604             return get_string('type_' . $type . '_plural', 'core_plugin');
606         } else if ($parent = $this->get_parent_of_subplugin($type)) {
607             // If this is a subplugin, try to ask the parent plugin for the name.
608             if (get_string_manager()->string_exists('subplugintype_' . $type . '_plural', $parent)) {
609                 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type . '_plural', $parent);
610             } else {
611                 return $this->plugin_name($parent) . ' / ' . $type;
612             }
614         } else {
615             return $type;
616         }
617     }
619     /**
620      * Returns information about the known plugin, or null
621      *
622      * @param string $component frankenstyle component name.
623      * @return \core\plugininfo\base|null the corresponding plugin information.
624      */
625     public function get_plugin_info($component) {
626         list($type, $name) = core_component::normalize_component($component);
627         $plugins = $this->get_plugins_of_type($type);
628         if (isset($plugins[$name])) {
629             return $plugins[$name];
630         } else {
631             return null;
632         }
633     }
635     /**
636      * Check to see if the current version of the plugin seems to be a checkout of an external repository.
637      *
638      * @see \core\update\deployer::plugin_external_source()
639      * @param string $component frankenstyle component name
640      * @return false|string
641      */
642     public function plugin_external_source($component) {
644         $plugininfo = $this->get_plugin_info($component);
646         if (is_null($plugininfo)) {
647             return false;
648         }
650         $pluginroot = $plugininfo->rootdir;
652         if (is_dir($pluginroot.'/.git')) {
653             return 'git';
654         }
656         if (is_dir($pluginroot.'/CVS')) {
657             return 'cvs';
658         }
660         if (is_dir($pluginroot.'/.svn')) {
661             return 'svn';
662         }
664         return false;
665     }
667     /**
668      * Get a list of any other plugins that require this one.
669      * @param string $component frankenstyle component name.
670      * @return array of frankensyle component names that require this one.
671      */
672     public function other_plugins_that_require($component) {
673         $others = array();
674         foreach ($this->get_plugins() as $type => $plugins) {
675             foreach ($plugins as $plugin) {
676                 $required = $plugin->get_other_required_plugins();
677                 if (isset($required[$component])) {
678                     $others[] = $plugin->component;
679                 }
680             }
681         }
682         return $others;
683     }
685     /**
686      * Check a dependencies list against the list of installed plugins.
687      * @param array $dependencies compenent name to required version or ANY_VERSION.
688      * @return bool true if all the dependencies are satisfied.
689      */
690     public function are_dependencies_satisfied($dependencies) {
691         foreach ($dependencies as $component => $requiredversion) {
692             $otherplugin = $this->get_plugin_info($component);
693             if (is_null($otherplugin)) {
694                 return false;
695             }
697             if ($requiredversion != ANY_VERSION and $otherplugin->versiondisk < $requiredversion) {
698                 return false;
699             }
700         }
702         return true;
703     }
705     /**
706      * Checks all dependencies for all installed plugins
707      *
708      * This is used by install and upgrade. The array passed by reference as the second
709      * argument is populated with the list of plugins that have failed dependencies (note that
710      * a single plugin can appear multiple times in the $failedplugins).
711      *
712      * @param int $moodleversion the version from version.php.
713      * @param array $failedplugins to return the list of plugins with non-satisfied dependencies
714      * @return bool true if all the dependencies are satisfied for all plugins.
715      */
716     public function all_plugins_ok($moodleversion, &$failedplugins = array()) {
718         $return = true;
719         foreach ($this->get_plugins() as $type => $plugins) {
720             foreach ($plugins as $plugin) {
722                 if (!$plugin->is_core_dependency_satisfied($moodleversion)) {
723                     $return = false;
724                     $failedplugins[] = $plugin->component;
725                 }
727                 if (!$this->are_dependencies_satisfied($plugin->get_other_required_plugins())) {
728                     $return = false;
729                     $failedplugins[] = $plugin->component;
730                 }
731             }
732         }
734         return $return;
735     }
737     /**
738      * Is it possible to uninstall the given plugin?
739      *
740      * False is returned if the plugininfo subclass declares the uninstall should
741      * not be allowed via {@link \core\plugininfo\base::is_uninstall_allowed()} or if the
742      * core vetoes it (e.g. becase the plugin or some of its subplugins is required
743      * by some other installed plugin).
744      *
745      * @param string $component full frankenstyle name, e.g. mod_foobar
746      * @return bool
747      */
748     public function can_uninstall_plugin($component) {
750         $pluginfo = $this->get_plugin_info($component);
752         if (is_null($pluginfo)) {
753             return false;
754         }
756         if (!$this->common_uninstall_check($pluginfo)) {
757             return false;
758         }
760         // Verify only if something else requires the subplugins, do not verify their common_uninstall_check()!
761         $subplugins = $this->get_subplugins_of_plugin($pluginfo->component);
762         foreach ($subplugins as $subpluginfo) {
763             // Check if there are some other plugins requiring this subplugin
764             // (but the parent and siblings).
765             foreach ($this->other_plugins_that_require($subpluginfo->component) as $requiresme) {
766                 $ismyparent = ($pluginfo->component === $requiresme);
767                 $ismysibling = in_array($requiresme, array_keys($subplugins));
768                 if (!$ismyparent and !$ismysibling) {
769                     return false;
770                 }
771             }
772         }
774         // Check if there are some other plugins requiring this plugin
775         // (but its subplugins).
776         foreach ($this->other_plugins_that_require($pluginfo->component) as $requiresme) {
777             $ismysubplugin = in_array($requiresme, array_keys($subplugins));
778             if (!$ismysubplugin) {
779                 return false;
780             }
781         }
783         return true;
784     }
786     /**
787      * Returns uninstall URL if exists.
788      *
789      * @param string $component
790      * @param string $return either 'overview' or 'manage'
791      * @return moodle_url uninstall URL, null if uninstall not supported
792      */
793     public function get_uninstall_url($component, $return = 'overview') {
794         if (!$this->can_uninstall_plugin($component)) {
795             return null;
796         }
798         $pluginfo = $this->get_plugin_info($component);
800         if (is_null($pluginfo)) {
801             return null;
802         }
804         if (method_exists($pluginfo, 'get_uninstall_url')) {
805             debugging('plugininfo method get_uninstall_url() is deprecated, all plugins should be uninstalled via standard URL only.');
806             return $pluginfo->get_uninstall_url($return);
807         }
809         return $pluginfo->get_default_uninstall_url($return);
810     }
812     /**
813      * Uninstall the given plugin.
814      *
815      * Automatically cleans-up all remaining configuration data, log records, events,
816      * files from the file pool etc.
817      *
818      * In the future, the functionality of {@link uninstall_plugin()} function may be moved
819      * into this method and all the code should be refactored to use it. At the moment, we
820      * mimic this future behaviour by wrapping that function call.
821      *
822      * @param string $component
823      * @param progress_trace $progress traces the process
824      * @return bool true on success, false on errors/problems
825      */
826     public function uninstall_plugin($component, progress_trace $progress) {
828         $pluginfo = $this->get_plugin_info($component);
830         if (is_null($pluginfo)) {
831             return false;
832         }
834         // Give the pluginfo class a chance to execute some steps.
835         $result = $pluginfo->uninstall($progress);
836         if (!$result) {
837             return false;
838         }
840         // Call the legacy core function to uninstall the plugin.
841         ob_start();
842         uninstall_plugin($pluginfo->type, $pluginfo->name);
843         $progress->output(ob_get_clean());
845         return true;
846     }
848     /**
849      * Checks if there are some plugins with a known available update
850      *
851      * @return bool true if there is at least one available update
852      */
853     public function some_plugins_updatable() {
854         foreach ($this->get_plugins() as $type => $plugins) {
855             foreach ($plugins as $plugin) {
856                 if ($plugin->available_updates()) {
857                     return true;
858                 }
859             }
860         }
862         return false;
863     }
865     /**
866      * Check to see if the given plugin folder can be removed by the web server process.
867      *
868      * @param string $component full frankenstyle component
869      * @return bool
870      */
871     public function is_plugin_folder_removable($component) {
873         $pluginfo = $this->get_plugin_info($component);
875         if (is_null($pluginfo)) {
876             return false;
877         }
879         // To be able to remove the plugin folder, its parent must be writable, too.
880         if (!is_writable(dirname($pluginfo->rootdir))) {
881             return false;
882         }
884         // Check that the folder and all its content is writable (thence removable).
885         return $this->is_directory_removable($pluginfo->rootdir);
886     }
888     /**
889      * Defines a list of all plugins that were originally shipped in the standard Moodle distribution,
890      * but are not anymore and are deleted during upgrades.
891      *
892      * The main purpose of this list is to hide missing plugins during upgrade.
893      *
894      * @param string $type plugin type
895      * @param string $name plugin name
896      * @return bool
897      */
898     public static function is_deleted_standard_plugin($type, $name) {
899         // Do not include plugins that were removed during upgrades to versions that are
900         // not supported as source versions for upgrade any more. For example, at MOODLE_23_STABLE
901         // branch, listed should be no plugins that were removed at 1.9.x - 2.1.x versions as
902         // Moodle 2.3 supports upgrades from 2.2.x only.
903         $plugins = array(
904             'qformat' => array('blackboard'),
905             'enrol' => array('authorize'),
906             'tool' => array('bloglevelupgrade'),
907         );
909         if (!isset($plugins[$type])) {
910             return false;
911         }
912         return in_array($name, $plugins[$type]);
913     }
915     /**
916      * Defines a white list of all plugins shipped in the standard Moodle distribution
917      *
918      * @param string $type
919      * @return false|array array of standard plugins or false if the type is unknown
920      */
921     public static function standard_plugins_list($type) {
923         $standard_plugins = array(
925             'assignment' => array(
926                 'offline', 'online', 'upload', 'uploadsingle'
927             ),
929             'assignsubmission' => array(
930                 'comments', 'file', 'onlinetext'
931             ),
933             'assignfeedback' => array(
934                 'comments', 'file', 'offline', 'editpdf'
935             ),
937             'auth' => array(
938                 'cas', 'db', 'email', 'fc', 'imap', 'ldap', 'manual', 'mnet',
939                 'nntp', 'nologin', 'none', 'pam', 'pop3', 'radius',
940                 'shibboleth', 'webservice'
941             ),
943             'block' => array(
944                 'activity_modules', 'admin_bookmarks', 'badges', 'blog_menu',
945                 'blog_recent', 'blog_tags', 'calendar_month',
946                 'calendar_upcoming', 'comments', 'community',
947                 'completionstatus', 'course_list', 'course_overview',
948                 'course_summary', 'feedback', 'glossary_random', 'html',
949                 'login', 'mentees', 'messages', 'mnet_hosts', 'myprofile',
950                 'navigation', 'news_items', 'online_users', 'participants',
951                 'private_files', 'quiz_results', 'recent_activity',
952                 'rss_client', 'search_forums', 'section_links',
953                 'selfcompletion', 'settings', 'site_main_menu',
954                 'social_activities', 'tag_flickr', 'tag_youtube', 'tags'
955             ),
957             'booktool' => array(
958                 'exportimscp', 'importhtml', 'print'
959             ),
961             'cachelock' => array(
962                 'file'
963             ),
965             'cachestore' => array(
966                 'file', 'memcache', 'memcached', 'mongodb', 'session', 'static'
967             ),
969             'calendartype' => array(
970                 'gregorian'
971             ),
973             'coursereport' => array(
974                 // Deprecated!
975             ),
977             'datafield' => array(
978                 'checkbox', 'date', 'file', 'latlong', 'menu', 'multimenu',
979                 'number', 'picture', 'radiobutton', 'text', 'textarea', 'url'
980             ),
982             'datapreset' => array(
983                 'imagegallery'
984             ),
986             'editor' => array(
987                 'textarea', 'tinymce'
988             ),
990             'enrol' => array(
991                 'category', 'cohort', 'database', 'flatfile',
992                 'guest', 'imsenterprise', 'ldap', 'manual', 'meta', 'mnet',
993                 'paypal', 'self'
994             ),
996             'filter' => array(
997                 'activitynames', 'algebra', 'censor', 'emailprotect',
998                 'emoticon', 'mediaplugin', 'multilang', 'tex', 'tidy',
999                 'urltolink', 'data', 'glossary'
1000             ),
1002             'format' => array(
1003                 'singleactivity', 'social', 'topics', 'weeks'
1004             ),
1006             'gradeexport' => array(
1007                 'ods', 'txt', 'xls', 'xml'
1008             ),
1010             'gradeimport' => array(
1011                 'csv', 'xml'
1012             ),
1014             'gradereport' => array(
1015                 'grader', 'outcomes', 'overview', 'user'
1016             ),
1018             'gradingform' => array(
1019                 'rubric', 'guide'
1020             ),
1022             'local' => array(
1023             ),
1025             'message' => array(
1026                 'email', 'jabber', 'popup'
1027             ),
1029             'mnetservice' => array(
1030                 'enrol'
1031             ),
1033             'mod' => array(
1034                 'assign', 'assignment', 'book', 'chat', 'choice', 'data', 'feedback', 'folder',
1035                 'forum', 'glossary', 'imscp', 'label', 'lesson', 'lti', 'page',
1036                 'quiz', 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop'
1037             ),
1039             'plagiarism' => array(
1040             ),
1042             'portfolio' => array(
1043                 'boxnet', 'download', 'flickr', 'googledocs', 'mahara', 'picasa'
1044             ),
1046             'profilefield' => array(
1047                 'checkbox', 'datetime', 'menu', 'text', 'textarea'
1048             ),
1050             'qbehaviour' => array(
1051                 'adaptive', 'adaptivenopenalty', 'deferredcbm',
1052                 'deferredfeedback', 'immediatecbm', 'immediatefeedback',
1053                 'informationitem', 'interactive', 'interactivecountback',
1054                 'manualgraded', 'missing'
1055             ),
1057             'qformat' => array(
1058                 'aiken', 'blackboard_six', 'examview', 'gift',
1059                 'learnwise', 'missingword', 'multianswer', 'webct',
1060                 'xhtml', 'xml'
1061             ),
1063             'qtype' => array(
1064                 'calculated', 'calculatedmulti', 'calculatedsimple',
1065                 'description', 'essay', 'match', 'missingtype', 'multianswer',
1066                 'multichoice', 'numerical', 'random', 'randomsamatch',
1067                 'shortanswer', 'truefalse'
1068             ),
1070             'quiz' => array(
1071                 'grading', 'overview', 'responses', 'statistics'
1072             ),
1074             'quizaccess' => array(
1075                 'delaybetweenattempts', 'ipaddress', 'numattempts', 'openclosedate',
1076                 'password', 'safebrowser', 'securewindow', 'timelimit'
1077             ),
1079             'report' => array(
1080                 'backups', 'completion', 'configlog', 'courseoverview',
1081                 'log', 'loglive', 'outline', 'participation', 'progress', 'questioninstances', 'security', 'stats', 'performance'
1082             ),
1084             'repository' => array(
1085                 'alfresco', 'areafiles', 'boxnet', 'coursefiles', 'dropbox', 'equella', 'filesystem',
1086                 'flickr', 'flickr_public', 'googledocs', 'local', 'merlot',
1087                 'picasa', 'recent', 'skydrive', 's3', 'upload', 'url', 'user', 'webdav',
1088                 'wikimedia', 'youtube'
1089             ),
1091             'scormreport' => array(
1092                 'basic',
1093                 'interactions',
1094                 'graphs',
1095                 'objectives'
1096             ),
1098             'tinymce' => array(
1099                 'ctrlhelp', 'dragmath', 'managefiles', 'moodleemoticon', 'moodleimage',
1100                 'moodlemedia', 'moodlenolink', 'pdw', 'spellchecker', 'wrap'
1101             ),
1103             'theme' => array(
1104                 'afterburner', 'anomaly', 'arialist', 'base', 'binarius', 'bootstrapbase',
1105                 'boxxie', 'brick', 'canvas', 'clean', 'formal_white', 'formfactor',
1106                 'fusion', 'leatherbound', 'magazine', 'nimble',
1107                 'nonzero', 'overlay', 'serenity', 'sky_high', 'splash',
1108                 'standard', 'standardold'
1109             ),
1111             'tool' => array(
1112                 'assignmentupgrade', 'behat', 'capability', 'customlang',
1113                 'dbtransfer', 'generator', 'health', 'innodb', 'installaddon',
1114                 'langimport', 'multilangupgrade', 'phpunit', 'profiling',
1115                 'qeupgradehelper', 'replace', 'spamcleaner', 'timezoneimport',
1116                 'unittest', 'uploadcourse', 'uploaduser', 'unsuproles', 'xmldb'
1117             ),
1119             'webservice' => array(
1120                 'amf', 'rest', 'soap', 'xmlrpc'
1121             ),
1123             'workshopallocation' => array(
1124                 'manual', 'random', 'scheduled'
1125             ),
1127             'workshopeval' => array(
1128                 'best'
1129             ),
1131             'workshopform' => array(
1132                 'accumulative', 'comments', 'numerrors', 'rubric'
1133             )
1134         );
1136         if (isset($standard_plugins[$type])) {
1137             return $standard_plugins[$type];
1138         } else {
1139             return false;
1140         }
1141     }
1143     /**
1144      * Reorders plugin types into a sequence to be displayed
1145      *
1146      * For technical reasons, plugin types returned by {@link core_component::get_plugin_types()} are
1147      * in a certain order that does not need to fit the expected order for the display.
1148      * Particularly, activity modules should be displayed first as they represent the
1149      * real heart of Moodle. They should be followed by other plugin types that are
1150      * used to build the courses (as that is what one expects from LMS). After that,
1151      * other supportive plugin types follow.
1152      *
1153      * @param array $types associative array
1154      * @return array same array with altered order of items
1155      */
1156     protected function reorder_plugin_types(array $types) {
1157         $fix = array('mod' => $types['mod']);
1158         foreach (core_component::get_plugin_list('mod') as $plugin => $fulldir) {
1159             if (!$subtypes = core_component::get_subplugins('mod_'.$plugin)) {
1160                 continue;
1161             }
1162             foreach ($subtypes as $subtype => $ignored) {
1163                 $fix[$subtype] = $types[$subtype];
1164             }
1165         }
1167         $fix['mod']        = $types['mod'];
1168         $fix['block']      = $types['block'];
1169         $fix['qtype']      = $types['qtype'];
1170         $fix['qbehaviour'] = $types['qbehaviour'];
1171         $fix['qformat']    = $types['qformat'];
1172         $fix['filter']     = $types['filter'];
1174         $fix['editor']     = $types['editor'];
1175         foreach (core_component::get_plugin_list('editor') as $plugin => $fulldir) {
1176             if (!$subtypes = core_component::get_subplugins('editor_'.$plugin)) {
1177                 continue;
1178             }
1179             foreach ($subtypes as $subtype => $ignored) {
1180                 $fix[$subtype] = $types[$subtype];
1181             }
1182         }
1184         $fix['enrol'] = $types['enrol'];
1185         $fix['auth']  = $types['auth'];
1186         $fix['tool']  = $types['tool'];
1187         foreach (core_component::get_plugin_list('tool') as $plugin => $fulldir) {
1188             if (!$subtypes = core_component::get_subplugins('tool_'.$plugin)) {
1189                 continue;
1190             }
1191             foreach ($subtypes as $subtype => $ignored) {
1192                 $fix[$subtype] = $types[$subtype];
1193             }
1194         }
1196         foreach ($types as $type => $path) {
1197             if (!isset($fix[$type])) {
1198                 $fix[$type] = $path;
1199             }
1200         }
1201         return $fix;
1202     }
1204     /**
1205      * Check if the given directory can be removed by the web server process.
1206      *
1207      * This recursively checks that the given directory and all its contents
1208      * it writable.
1209      *
1210      * @param string $fullpath
1211      * @return boolean
1212      */
1213     protected function is_directory_removable($fullpath) {
1215         if (!is_writable($fullpath)) {
1216             return false;
1217         }
1219         if (is_dir($fullpath)) {
1220             $handle = opendir($fullpath);
1221         } else {
1222             return false;
1223         }
1225         $result = true;
1227         while ($filename = readdir($handle)) {
1229             if ($filename === '.' or $filename === '..') {
1230                 continue;
1231             }
1233             $subfilepath = $fullpath.'/'.$filename;
1235             if (is_dir($subfilepath)) {
1236                 $result = $result && $this->is_directory_removable($subfilepath);
1238             } else {
1239                 $result = $result && is_writable($subfilepath);
1240             }
1241         }
1243         closedir($handle);
1245         return $result;
1246     }
1248     /**
1249      * Helper method that implements common uninstall prerequisites
1250      *
1251      * @param \core\plugininfo\base $pluginfo
1252      * @return bool
1253      */
1254     protected function common_uninstall_check(\core\plugininfo\base $pluginfo) {
1256         if (!$pluginfo->is_uninstall_allowed()) {
1257             // The plugin's plugininfo class declares it should not be uninstalled.
1258             return false;
1259         }
1261         if ($pluginfo->get_status() === self::PLUGIN_STATUS_NEW) {
1262             // The plugin is not installed. It should be either installed or removed from the disk.
1263             // Relying on this temporary state may be tricky.
1264             return false;
1265         }
1267         if (method_exists($pluginfo, 'get_uninstall_url') and is_null($pluginfo->get_uninstall_url())) {
1268             // Backwards compatibility.
1269             debugging('\core\plugininfo\base subclasses should use is_uninstall_allowed() instead of returning null in get_uninstall_url()',
1270                 DEBUG_DEVELOPER);
1271             return false;
1272         }
1274         return true;
1275     }