f6543e8e1a587088eeb69f6eb3c66ae0323d418f
[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         // TODO: Delete this block once Moodle 2.6 or later becomes minimum required version to upgrade.
166         if ($CFG->version < 2013092001.02) {
167             // We did not upgrade the database yet.
168             $modules = $DB->get_records('modules', array(), 'name ASC', 'id, name, version');
169             foreach ($modules as $module) {
170                 $this->installedplugins['mod'][$module->name] = $module->version;
171             }
172             $blocks = $DB->get_records('block', array(), 'name ASC', 'id, name, version');
173             foreach ($blocks as $block) {
174                 $this->installedplugins['block'][$block->name] = $block->version;
175             }
176         }
178         $versions = $DB->get_records('config_plugins', array('name'=>'version'));
179         foreach ($versions as $version) {
180             $parts = explode('_', $version->plugin, 2);
181             if (!isset($parts[1])) {
182                 // Invalid component, there must be at least one "_".
183                 continue;
184             }
185             // Do not verify here if plugin type and name are valid.
186             $this->installedplugins[$parts[0]][$parts[1]] = $version->value;
187         }
189         foreach ($this->installedplugins as $key => $value) {
190             ksort($this->installedplugins[$key]);
191         }
193         $cache->set('installed', $this->installedplugins);
194     }
196     /**
197      * Return list of installed plugins of given type.
198      * @param string $type
199      * @return array $name=>$version
200      */
201     public function get_installed_plugins($type) {
202         $this->load_installed_plugins();
203         if (isset($this->installedplugins[$type])) {
204             return $this->installedplugins[$type];
205         }
206         return array();
207     }
209     /**
210      * Load list of all enabled plugins,
211      * call before using $this->enabledplugins.
212      *
213      * This method is caching results from individual plugin info classes.
214      */
215     protected function load_enabled_plugins() {
216         global $CFG;
218         if ($this->enabledplugins) {
219             return;
220         }
222         if (empty($CFG->version)) {
223             $this->enabledplugins = array();
224             return;
225         }
227         $cache = cache::make('core', 'plugin_manager');
228         $enabled = $cache->get('enabled');
230         if (is_array($enabled)) {
231             $this->enabledplugins = $enabled;
232             return;
233         }
235         $this->enabledplugins = array();
237         require_once($CFG->libdir.'/adminlib.php');
239         $plugintypes = core_component::get_plugin_types();
240         foreach ($plugintypes as $plugintype => $fulldir) {
241             $plugininfoclass = self::resolve_plugininfo_class($plugintype);
242             if (class_exists($plugininfoclass)) {
243                 $enabled = $plugininfoclass::get_enabled_plugins();
244                 if (!is_array($enabled)) {
245                     continue;
246                 }
247                 $this->enabledplugins[$plugintype] = $enabled;
248             }
249         }
251         $cache->set('enabled', $this->enabledplugins);
252     }
254     /**
255      * Get list of enabled plugins of given type,
256      * the result may contain missing plugins.
257      *
258      * @param string $type
259      * @return array|null  list of enabled plugins of this type, null if unknown
260      */
261     public function get_enabled_plugins($type) {
262         $this->load_enabled_plugins();
263         if (isset($this->enabledplugins[$type])) {
264             return $this->enabledplugins[$type];
265         }
266         return null;
267     }
269     /**
270      * Load list of all present plugins - call before using $this->presentplugins.
271      */
272     protected function load_present_plugins() {
273         if ($this->presentplugins) {
274             return;
275         }
277         $cache = cache::make('core', 'plugin_manager');
278         $present = $cache->get('present');
280         if (is_array($present)) {
281             $this->presentplugins = $present;
282             return;
283         }
285         $this->presentplugins = array();
287         $plugintypes = core_component::get_plugin_types();
288         foreach ($plugintypes as $type => $typedir) {
289             $plugs = core_component::get_plugin_list($type);
290             foreach ($plugs as $plug => $fullplug) {
291                 $module = new stdClass();
292                 $plugin = new stdClass();
293                 $plugin->version = null;
294                 include($fullplug.'/version.php');
296                 // Check if the legacy $module syntax is still used.
297                 if (!is_object($module) or (count((array)$module) > 0)) {
298                     debugging('Unsupported $module syntax detected in version.php of the '.$type.'_'.$plug.' plugin.');
299                     $skipcache = true;
300                 }
302                 // Check if the component is properly declared.
303                 if (empty($plugin->component) or ($plugin->component !== $type.'_'.$plug)) {
304                     debugging('Plugin '.$type.'_'.$plug.' does not declare valid $plugin->component in its version.php.');
305                     $skipcache = true;
306                 }
308                 $this->presentplugins[$type][$plug] = $plugin;
309             }
310         }
312         if (empty($skipcache)) {
313             $cache->set('present', $this->presentplugins);
314         }
315     }
317     /**
318      * Get list of present plugins of given type.
319      *
320      * @param string $type
321      * @return array|null  list of presnet plugins $name=>$diskversion, null if unknown
322      */
323     public function get_present_plugins($type) {
324         $this->load_present_plugins();
325         if (isset($this->presentplugins[$type])) {
326             return $this->presentplugins[$type];
327         }
328         return null;
329     }
331     /**
332      * Returns a tree of known plugins and information about them
333      *
334      * @return array 2D array. The first keys are plugin type names (e.g. qtype);
335      *      the second keys are the plugin local name (e.g. multichoice); and
336      *      the values are the corresponding objects extending {@link \core\plugininfo\base}
337      */
338     public function get_plugins() {
339         $this->init_pluginsinfo_property();
341         // Make sure all types are initialised.
342         foreach ($this->pluginsinfo as $plugintype => $list) {
343             if ($list === null) {
344                 $this->get_plugins_of_type($plugintype);
345             }
346         }
348         return $this->pluginsinfo;
349     }
351     /**
352      * Returns list of known plugins of the given type.
353      *
354      * This method returns the subset of the tree returned by {@link self::get_plugins()}.
355      * If the given type is not known, empty array is returned.
356      *
357      * @param string $type plugin type, e.g. 'mod' or 'workshopallocation'
358      * @return \core\plugininfo\base[] (string)plugin name (e.g. 'workshop') => corresponding subclass of {@link \core\plugininfo\base}
359      */
360     public function get_plugins_of_type($type) {
361         global $CFG;
363         $this->init_pluginsinfo_property();
365         if (!array_key_exists($type, $this->pluginsinfo)) {
366             return array();
367         }
369         if (is_array($this->pluginsinfo[$type])) {
370             return $this->pluginsinfo[$type];
371         }
373         $types = core_component::get_plugin_types();
375         if (!isset($types[$type])) {
376             // Orphaned subplugins!
377             $plugintypeclass = self::resolve_plugininfo_class($type);
378             $this->pluginsinfo[$type] = $plugintypeclass::get_plugins($type, null, $plugintypeclass);
379             return $this->pluginsinfo[$type];
380         }
382         /** @var \core\plugininfo\base $plugintypeclass */
383         $plugintypeclass = self::resolve_plugininfo_class($type);
384         $plugins = $plugintypeclass::get_plugins($type, $types[$type], $plugintypeclass);
385         $this->pluginsinfo[$type] = $plugins;
387         if (empty($CFG->disableupdatenotifications) and !during_initial_install()) {
388             // Append the information about available updates provided by {@link \core\update\checker()}.
389             $provider = \core\update\checker::instance();
390             foreach ($plugins as $plugininfoholder) {
391                 $plugininfoholder->check_available_updates($provider);
392             }
393         }
395         return $this->pluginsinfo[$type];
396     }
398     /**
399      * Init placeholder array for plugin infos.
400      */
401     protected function init_pluginsinfo_property() {
402         if (is_array($this->pluginsinfo)) {
403             return;
404         }
405         $this->pluginsinfo = array();
407         $plugintypes = $this->get_plugin_types();
409         foreach ($plugintypes as $plugintype => $plugintyperootdir) {
410             $this->pluginsinfo[$plugintype] = null;
411         }
413         // Add orphaned subplugin types.
414         $this->load_installed_plugins();
415         foreach ($this->installedplugins as $plugintype => $unused) {
416             if (!isset($plugintypes[$plugintype])) {
417                 $this->pluginsinfo[$plugintype] = null;
418             }
419         }
420     }
422     /**
423      * Find the plugin info class for given type.
424      *
425      * @param string $type
426      * @return string name of pluginfo class for give plugin type
427      */
428     public static function resolve_plugininfo_class($type) {
429         $plugintypes = core_component::get_plugin_types();
430         if (!isset($plugintypes[$type])) {
431             return '\core\plugininfo\orphaned';
432         }
434         $parent = core_component::get_subtype_parent($type);
436         if ($parent) {
437             $class = '\\'.$parent.'\plugininfo\\' . $type;
438             if (class_exists($class)) {
439                 $plugintypeclass = $class;
440             } else {
441                 if ($dir = core_component::get_component_directory($parent)) {
442                     // BC only - use namespace instead!
443                     if (file_exists("$dir/adminlib.php")) {
444                         global $CFG;
445                         include_once("$dir/adminlib.php");
446                     }
447                     if (class_exists('plugininfo_' . $type)) {
448                         $plugintypeclass = 'plugininfo_' . $type;
449                         debugging('Class "'.$plugintypeclass.'" is deprecated, migrate to "'.$class.'"', DEBUG_DEVELOPER);
450                     } else {
451                         debugging('Subplugin type "'.$type.'" should define class "'.$class.'"', DEBUG_DEVELOPER);
452                         $plugintypeclass = '\core\plugininfo\general';
453                     }
454                 } else {
455                     $plugintypeclass = '\core\plugininfo\general';
456                 }
457             }
458         } else {
459             $class = '\core\plugininfo\\' . $type;
460             if (class_exists($class)) {
461                 $plugintypeclass = $class;
462             } else {
463                 debugging('All standard types including "'.$type.'" should have plugininfo class!', DEBUG_DEVELOPER);
464                 $plugintypeclass = '\core\plugininfo\general';
465             }
466         }
468         if (!in_array('core\plugininfo\base', class_parents($plugintypeclass))) {
469             throw new coding_exception('Class ' . $plugintypeclass . ' must extend \core\plugininfo\base');
470         }
472         return $plugintypeclass;
473     }
475     /**
476      * Returns list of all known subplugins of the given plugin.
477      *
478      * For plugins that do not provide subplugins (i.e. there is no support for it),
479      * empty array is returned.
480      *
481      * @param string $component full component name, e.g. 'mod_workshop'
482      * @return array (string) component name (e.g. 'workshopallocation_random') => subclass of {@link \core\plugininfo\base}
483      */
484     public function get_subplugins_of_plugin($component) {
486         $pluginfo = $this->get_plugin_info($component);
488         if (is_null($pluginfo)) {
489             return array();
490         }
492         $subplugins = $this->get_subplugins();
494         if (!isset($subplugins[$pluginfo->component])) {
495             return array();
496         }
498         $list = array();
500         foreach ($subplugins[$pluginfo->component] as $subdata) {
501             foreach ($this->get_plugins_of_type($subdata->type) as $subpluginfo) {
502                 $list[$subpluginfo->component] = $subpluginfo;
503             }
504         }
506         return $list;
507     }
509     /**
510      * Returns list of plugins that define their subplugins and the information
511      * about them from the db/subplugins.php file.
512      *
513      * @return array with keys like 'mod_quiz', and values the data from the
514      *      corresponding db/subplugins.php file.
515      */
516     public function get_subplugins() {
518         if (is_array($this->subpluginsinfo)) {
519             return $this->subpluginsinfo;
520         }
522         $plugintypes = core_component::get_plugin_types();
524         $this->subpluginsinfo = array();
525         foreach (core_component::get_plugin_types_with_subplugins() as $type => $ignored) {
526             foreach (core_component::get_plugin_list($type) as $plugin => $componentdir) {
527                 $component = $type.'_'.$plugin;
528                 $subplugins = core_component::get_subplugins($component);
529                 if (!$subplugins) {
530                     continue;
531                 }
532                 $this->subpluginsinfo[$component] = array();
533                 foreach ($subplugins as $subplugintype => $ignored) {
534                     $subplugin = new stdClass();
535                     $subplugin->type = $subplugintype;
536                     $subplugin->typerootdir = $plugintypes[$subplugintype];
537                     $this->subpluginsinfo[$component][$subplugintype] = $subplugin;
538                 }
539             }
540         }
541         return $this->subpluginsinfo;
542     }
544     /**
545      * Returns the name of the plugin that defines the given subplugin type
546      *
547      * If the given subplugin type is not actually a subplugin, returns false.
548      *
549      * @param string $subplugintype the name of subplugin type, eg. workshopform or quiz
550      * @return false|string the name of the parent plugin, eg. mod_workshop
551      */
552     public function get_parent_of_subplugin($subplugintype) {
553         $parent = core_component::get_subtype_parent($subplugintype);
554         if (!$parent) {
555             return false;
556         }
557         return $parent;
558     }
560     /**
561      * Returns a localized name of a given plugin
562      *
563      * @param string $component name of the plugin, eg mod_workshop or auth_ldap
564      * @return string
565      */
566     public function plugin_name($component) {
568         $pluginfo = $this->get_plugin_info($component);
570         if (is_null($pluginfo)) {
571             throw new moodle_exception('err_unknown_plugin', 'core_plugin', '', array('plugin' => $component));
572         }
574         return $pluginfo->displayname;
575     }
577     /**
578      * Returns a localized name of a plugin typed in singular form
579      *
580      * Most plugin types define their names in core_plugin lang file. In case of subplugins,
581      * we try to ask the parent plugin for the name. In the worst case, we will return
582      * the value of the passed $type parameter.
583      *
584      * @param string $type the type of the plugin, e.g. mod or workshopform
585      * @return string
586      */
587     public function plugintype_name($type) {
589         if (get_string_manager()->string_exists('type_' . $type, 'core_plugin')) {
590             // For most plugin types, their names are defined in core_plugin lang file.
591             return get_string('type_' . $type, 'core_plugin');
593         } else if ($parent = $this->get_parent_of_subplugin($type)) {
594             // If this is a subplugin, try to ask the parent plugin for the name.
595             if (get_string_manager()->string_exists('subplugintype_' . $type, $parent)) {
596                 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type, $parent);
597             } else {
598                 return $this->plugin_name($parent) . ' / ' . $type;
599             }
601         } else {
602             return $type;
603         }
604     }
606     /**
607      * Returns a localized name of a plugin type in plural form
608      *
609      * Most plugin types define their names in core_plugin lang file. In case of subplugins,
610      * we try to ask the parent plugin for the name. In the worst case, we will return
611      * the value of the passed $type parameter.
612      *
613      * @param string $type the type of the plugin, e.g. mod or workshopform
614      * @return string
615      */
616     public function plugintype_name_plural($type) {
618         if (get_string_manager()->string_exists('type_' . $type . '_plural', 'core_plugin')) {
619             // For most plugin types, their names are defined in core_plugin lang file.
620             return get_string('type_' . $type . '_plural', 'core_plugin');
622         } else if ($parent = $this->get_parent_of_subplugin($type)) {
623             // If this is a subplugin, try to ask the parent plugin for the name.
624             if (get_string_manager()->string_exists('subplugintype_' . $type . '_plural', $parent)) {
625                 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type . '_plural', $parent);
626             } else {
627                 return $this->plugin_name($parent) . ' / ' . $type;
628             }
630         } else {
631             return $type;
632         }
633     }
635     /**
636      * Returns information about the known plugin, or null
637      *
638      * @param string $component frankenstyle component name.
639      * @return \core\plugininfo\base|null the corresponding plugin information.
640      */
641     public function get_plugin_info($component) {
642         list($type, $name) = core_component::normalize_component($component);
643         $plugins = $this->get_plugins_of_type($type);
644         if (isset($plugins[$name])) {
645             return $plugins[$name];
646         } else {
647             return null;
648         }
649     }
651     /**
652      * Check to see if the current version of the plugin seems to be a checkout of an external repository.
653      *
654      * @see \core\update\deployer::plugin_external_source()
655      * @param string $component frankenstyle component name
656      * @return false|string
657      */
658     public function plugin_external_source($component) {
660         $plugininfo = $this->get_plugin_info($component);
662         if (is_null($plugininfo)) {
663             return false;
664         }
666         $pluginroot = $plugininfo->rootdir;
668         if (is_dir($pluginroot.'/.git')) {
669             return 'git';
670         }
672         if (is_file($pluginroot.'/.git')) {
673             return 'git-submodule';
674         }
676         if (is_dir($pluginroot.'/CVS')) {
677             return 'cvs';
678         }
680         if (is_dir($pluginroot.'/.svn')) {
681             return 'svn';
682         }
684         if (is_dir($pluginroot.'/.hg')) {
685             return 'mercurial';
686         }
688         return false;
689     }
691     /**
692      * Get a list of any other plugins that require this one.
693      * @param string $component frankenstyle component name.
694      * @return array of frankensyle component names that require this one.
695      */
696     public function other_plugins_that_require($component) {
697         $others = array();
698         foreach ($this->get_plugins() as $type => $plugins) {
699             foreach ($plugins as $plugin) {
700                 $required = $plugin->get_other_required_plugins();
701                 if (isset($required[$component])) {
702                     $others[] = $plugin->component;
703                 }
704             }
705         }
706         return $others;
707     }
709     /**
710      * Check a dependencies list against the list of installed plugins.
711      * @param array $dependencies compenent name to required version or ANY_VERSION.
712      * @return bool true if all the dependencies are satisfied.
713      */
714     public function are_dependencies_satisfied($dependencies) {
715         foreach ($dependencies as $component => $requiredversion) {
716             $otherplugin = $this->get_plugin_info($component);
717             if (is_null($otherplugin)) {
718                 return false;
719             }
721             if ($requiredversion != ANY_VERSION and $otherplugin->versiondisk < $requiredversion) {
722                 return false;
723             }
724         }
726         return true;
727     }
729     /**
730      * Checks all dependencies for all installed plugins
731      *
732      * This is used by install and upgrade. The array passed by reference as the second
733      * argument is populated with the list of plugins that have failed dependencies (note that
734      * a single plugin can appear multiple times in the $failedplugins).
735      *
736      * @param int $moodleversion the version from version.php.
737      * @param array $failedplugins to return the list of plugins with non-satisfied dependencies
738      * @return bool true if all the dependencies are satisfied for all plugins.
739      */
740     public function all_plugins_ok($moodleversion, &$failedplugins = array()) {
742         $return = true;
743         foreach ($this->get_plugins() as $type => $plugins) {
744             foreach ($plugins as $plugin) {
746                 if (!$plugin->is_core_dependency_satisfied($moodleversion)) {
747                     $return = false;
748                     $failedplugins[] = $plugin->component;
749                 }
751                 if (!$this->are_dependencies_satisfied($plugin->get_other_required_plugins())) {
752                     $return = false;
753                     $failedplugins[] = $plugin->component;
754                 }
755             }
756         }
758         return $return;
759     }
761     /**
762      * Is it possible to uninstall the given plugin?
763      *
764      * False is returned if the plugininfo subclass declares the uninstall should
765      * not be allowed via {@link \core\plugininfo\base::is_uninstall_allowed()} or if the
766      * core vetoes it (e.g. becase the plugin or some of its subplugins is required
767      * by some other installed plugin).
768      *
769      * @param string $component full frankenstyle name, e.g. mod_foobar
770      * @return bool
771      */
772     public function can_uninstall_plugin($component) {
774         $pluginfo = $this->get_plugin_info($component);
776         if (is_null($pluginfo)) {
777             return false;
778         }
780         if (!$this->common_uninstall_check($pluginfo)) {
781             return false;
782         }
784         // Verify only if something else requires the subplugins, do not verify their common_uninstall_check()!
785         $subplugins = $this->get_subplugins_of_plugin($pluginfo->component);
786         foreach ($subplugins as $subpluginfo) {
787             // Check if there are some other plugins requiring this subplugin
788             // (but the parent and siblings).
789             foreach ($this->other_plugins_that_require($subpluginfo->component) as $requiresme) {
790                 $ismyparent = ($pluginfo->component === $requiresme);
791                 $ismysibling = in_array($requiresme, array_keys($subplugins));
792                 if (!$ismyparent and !$ismysibling) {
793                     return false;
794                 }
795             }
796         }
798         // Check if there are some other plugins requiring this plugin
799         // (but its subplugins).
800         foreach ($this->other_plugins_that_require($pluginfo->component) as $requiresme) {
801             $ismysubplugin = in_array($requiresme, array_keys($subplugins));
802             if (!$ismysubplugin) {
803                 return false;
804             }
805         }
807         return true;
808     }
810     /**
811      * Returns uninstall URL if exists.
812      *
813      * @param string $component
814      * @param string $return either 'overview' or 'manage'
815      * @return moodle_url uninstall URL, null if uninstall not supported
816      */
817     public function get_uninstall_url($component, $return = 'overview') {
818         if (!$this->can_uninstall_plugin($component)) {
819             return null;
820         }
822         $pluginfo = $this->get_plugin_info($component);
824         if (is_null($pluginfo)) {
825             return null;
826         }
828         if (method_exists($pluginfo, 'get_uninstall_url')) {
829             debugging('plugininfo method get_uninstall_url() is deprecated, all plugins should be uninstalled via standard URL only.');
830             return $pluginfo->get_uninstall_url($return);
831         }
833         return $pluginfo->get_default_uninstall_url($return);
834     }
836     /**
837      * Uninstall the given plugin.
838      *
839      * Automatically cleans-up all remaining configuration data, log records, events,
840      * files from the file pool etc.
841      *
842      * In the future, the functionality of {@link uninstall_plugin()} function may be moved
843      * into this method and all the code should be refactored to use it. At the moment, we
844      * mimic this future behaviour by wrapping that function call.
845      *
846      * @param string $component
847      * @param progress_trace $progress traces the process
848      * @return bool true on success, false on errors/problems
849      */
850     public function uninstall_plugin($component, progress_trace $progress) {
852         $pluginfo = $this->get_plugin_info($component);
854         if (is_null($pluginfo)) {
855             return false;
856         }
858         // Give the pluginfo class a chance to execute some steps.
859         $result = $pluginfo->uninstall($progress);
860         if (!$result) {
861             return false;
862         }
864         // Call the legacy core function to uninstall the plugin.
865         ob_start();
866         uninstall_plugin($pluginfo->type, $pluginfo->name);
867         $progress->output(ob_get_clean());
869         return true;
870     }
872     /**
873      * Checks if there are some plugins with a known available update
874      *
875      * @return bool true if there is at least one available update
876      */
877     public function some_plugins_updatable() {
878         foreach ($this->get_plugins() as $type => $plugins) {
879             foreach ($plugins as $plugin) {
880                 if ($plugin->available_updates()) {
881                     return true;
882                 }
883             }
884         }
886         return false;
887     }
889     /**
890      * Check to see if the given plugin folder can be removed by the web server process.
891      *
892      * @param string $component full frankenstyle component
893      * @return bool
894      */
895     public function is_plugin_folder_removable($component) {
897         $pluginfo = $this->get_plugin_info($component);
899         if (is_null($pluginfo)) {
900             return false;
901         }
903         // To be able to remove the plugin folder, its parent must be writable, too.
904         if (!is_writable(dirname($pluginfo->rootdir))) {
905             return false;
906         }
908         // Check that the folder and all its content is writable (thence removable).
909         return $this->is_directory_removable($pluginfo->rootdir);
910     }
912     /**
913      * Defines a list of all plugins that were originally shipped in the standard Moodle distribution,
914      * but are not anymore and are deleted during upgrades.
915      *
916      * The main purpose of this list is to hide missing plugins during upgrade.
917      *
918      * @param string $type plugin type
919      * @param string $name plugin name
920      * @return bool
921      */
922     public static function is_deleted_standard_plugin($type, $name) {
923         // Do not include plugins that were removed during upgrades to versions that are
924         // not supported as source versions for upgrade any more. For example, at MOODLE_23_STABLE
925         // branch, listed should be no plugins that were removed at 1.9.x - 2.1.x versions as
926         // Moodle 2.3 supports upgrades from 2.2.x only.
927         $plugins = array(
928             'qformat' => array('blackboard', 'learnwise'),
929             'enrol' => array('authorize'),
930             'tinymce' => array('dragmath'),
931             'tool' => array('bloglevelupgrade', 'qeupgradehelper', 'timezoneimport'),
932             'theme' => array('mymobile', 'afterburner', 'anomaly', 'arialist', 'binarius', 'boxxie', 'brick', 'formal_white',
933                 'formfactor', 'fusion', 'leatherbound', 'magazine', 'nimble', 'nonzero', 'overlay', 'serenity', 'sky_high',
934                 'splash', 'standard', 'standardold'),
935         );
937         if (!isset($plugins[$type])) {
938             return false;
939         }
940         return in_array($name, $plugins[$type]);
941     }
943     /**
944      * Defines a white list of all plugins shipped in the standard Moodle distribution
945      *
946      * @param string $type
947      * @return false|array array of standard plugins or false if the type is unknown
948      */
949     public static function standard_plugins_list($type) {
951         $standard_plugins = array(
953             'atto' => array(
954                 'accessibilitychecker', 'accessibilityhelper', 'align',
955                 'backcolor', 'bold', 'charmap', 'clear', 'collapse', 'emoticon',
956                 'equation', 'fontcolor', 'html', 'image', 'indent', 'italic',
957                 'link', 'managefiles', 'media', 'noautolink', 'orderedlist',
958                 'rtl', 'strike', 'subscript', 'superscript', 'table', 'title',
959                 'underline', 'undo', 'unorderedlist'
960             ),
962             'assignment' => array(
963                 'offline', 'online', 'upload', 'uploadsingle'
964             ),
966             'assignsubmission' => array(
967                 'comments', 'file', 'onlinetext'
968             ),
970             'assignfeedback' => array(
971                 'comments', 'file', 'offline', 'editpdf'
972             ),
974             'auth' => array(
975                 'cas', 'db', 'email', 'fc', 'imap', 'ldap', 'manual', 'mnet',
976                 'nntp', 'nologin', 'none', 'pam', 'pop3', 'radius',
977                 'shibboleth', 'webservice'
978             ),
980             'availability' => array(
981                 'completion', 'date', 'grade', 'group', 'grouping', 'profile'
982             ),
984             'block' => array(
985                 'activity_modules', 'activity_results', 'admin_bookmarks', 'badges',
986                 'blog_menu', 'blog_recent', 'blog_tags', 'calendar_month',
987                 'calendar_upcoming', 'comments', 'community',
988                 'completionstatus', 'course_list', 'course_overview',
989                 'course_summary', 'feedback', 'glossary_random', 'html',
990                 'login', 'mentees', 'messages', 'mnet_hosts', 'myprofile',
991                 'navigation', 'news_items', 'online_users', 'participants',
992                 'private_files', 'quiz_results', 'recent_activity',
993                 'rss_client', 'search_forums', 'section_links',
994                 'selfcompletion', 'settings', 'site_main_menu',
995                 'social_activities', 'tag_flickr', 'tag_youtube', 'tags'
996             ),
998             'booktool' => array(
999                 'exportimscp', 'importhtml', 'print'
1000             ),
1002             'cachelock' => array(
1003                 'file'
1004             ),
1006             'cachestore' => array(
1007                 'file', 'memcache', 'memcached', 'mongodb', 'session', 'static'
1008             ),
1010             'calendartype' => array(
1011                 'gregorian'
1012             ),
1014             'coursereport' => array(
1015                 // Deprecated!
1016             ),
1018             'datafield' => array(
1019                 'checkbox', 'date', 'file', 'latlong', 'menu', 'multimenu',
1020                 'number', 'picture', 'radiobutton', 'text', 'textarea', 'url'
1021             ),
1023             'datapreset' => array(
1024                 'imagegallery'
1025             ),
1027             'editor' => array(
1028                 'atto', 'textarea', 'tinymce'
1029             ),
1031             'enrol' => array(
1032                 'category', 'cohort', 'database', 'flatfile',
1033                 'guest', 'imsenterprise', 'ldap', 'manual', 'meta', 'mnet',
1034                 'paypal', 'self'
1035             ),
1037             'filter' => array(
1038                 'activitynames', 'algebra', 'censor', 'emailprotect',
1039                 'emoticon', 'mathjaxloader', 'mediaplugin', 'multilang', 'tex', 'tidy',
1040                 'urltolink', 'data', 'glossary'
1041             ),
1043             'format' => array(
1044                 'singleactivity', 'social', 'topics', 'weeks'
1045             ),
1047             'gradeexport' => array(
1048                 'ods', 'txt', 'xls', 'xml'
1049             ),
1051             'gradeimport' => array(
1052                 'csv', 'direct', 'xml'
1053             ),
1055             'gradereport' => array(
1056                 'grader', 'history', 'outcomes', 'overview', 'user', 'singleview'
1057             ),
1059             'gradingform' => array(
1060                 'rubric', 'guide'
1061             ),
1063             'local' => array(
1064             ),
1066             'logstore' => array(
1067                 'database', 'legacy', 'standard',
1068             ),
1070             'ltiservice' => array(
1071                 'profile', 'toolproxy', 'toolsettings'
1072             ),
1074             'message' => array(
1075                 'airnotifier', 'email', 'jabber', 'popup'
1076             ),
1078             'mnetservice' => array(
1079                 'enrol'
1080             ),
1082             'mod' => array(
1083                 'assign', 'assignment', 'book', 'chat', 'choice', 'data', 'feedback', 'folder',
1084                 'forum', 'glossary', 'imscp', 'label', 'lesson', 'lti', 'page',
1085                 'quiz', 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop'
1086             ),
1088             'plagiarism' => array(
1089             ),
1091             'portfolio' => array(
1092                 'boxnet', 'download', 'flickr', 'googledocs', 'mahara', 'picasa'
1093             ),
1095             'profilefield' => array(
1096                 'checkbox', 'datetime', 'menu', 'text', 'textarea'
1097             ),
1099             'qbehaviour' => array(
1100                 'adaptive', 'adaptivenopenalty', 'deferredcbm',
1101                 'deferredfeedback', 'immediatecbm', 'immediatefeedback',
1102                 'informationitem', 'interactive', 'interactivecountback',
1103                 'manualgraded', 'missing'
1104             ),
1106             'qformat' => array(
1107                 'aiken', 'blackboard_six', 'examview', 'gift',
1108                 'missingword', 'multianswer', 'webct',
1109                 'xhtml', 'xml'
1110             ),
1112             'qtype' => array(
1113                 'calculated', 'calculatedmulti', 'calculatedsimple',
1114                 'ddimageortext', 'ddmarker', 'ddwtos', 'description',
1115                 'essay', 'gapselect', 'match', 'missingtype', 'multianswer',
1116                 'multichoice', 'numerical', 'random', 'randomsamatch',
1117                 'shortanswer', 'truefalse'
1118             ),
1120             'quiz' => array(
1121                 'grading', 'overview', 'responses', 'statistics'
1122             ),
1124             'quizaccess' => array(
1125                 'delaybetweenattempts', 'ipaddress', 'numattempts', 'openclosedate',
1126                 'password', 'safebrowser', 'securewindow', 'timelimit'
1127             ),
1129             'report' => array(
1130                 'backups', 'completion', 'configlog', 'courseoverview', 'eventlist',
1131                 'log', 'loglive', 'outline', 'participation', 'progress', 'questioninstances', 'security', 'stats', 'performance',
1132                 'usersessions',
1133             ),
1135             'repository' => array(
1136                 'alfresco', 'areafiles', 'boxnet', 'coursefiles', 'dropbox', 'equella', 'filesystem',
1137                 'flickr', 'flickr_public', 'googledocs', 'local', 'merlot',
1138                 'picasa', 'recent', 'skydrive', 's3', 'upload', 'url', 'user', 'webdav',
1139                 'wikimedia', 'youtube'
1140             ),
1142             'scormreport' => array(
1143                 'basic',
1144                 'interactions',
1145                 'graphs',
1146                 'objectives'
1147             ),
1149             'tinymce' => array(
1150                 'ctrlhelp', 'managefiles', 'moodleemoticon', 'moodleimage',
1151                 'moodlemedia', 'moodlenolink', 'pdw', 'spellchecker', 'wrap'
1152             ),
1154             'theme' => array(
1155                 'base', 'bootstrapbase', 'canvas', 'clean', 'more'
1156             ),
1158             'tool' => array(
1159                 'assignmentupgrade', 'availabilityconditions', 'behat', 'capability', 'customlang',
1160                 'dbtransfer', 'filetypes', 'generator', 'health', 'innodb', 'installaddon',
1161                 'langimport', 'log', 'messageinbound', 'multilangupgrade', 'monitor', 'phpunit', 'profiling',
1162                 'replace', 'spamcleaner', 'task', 'templatelibrary',
1163                 'unittest', 'uploadcourse', 'uploaduser', 'unsuproles', 'xmldb'
1164             ),
1166             'webservice' => array(
1167                 'amf', 'rest', 'soap', 'xmlrpc'
1168             ),
1170             'workshopallocation' => array(
1171                 'manual', 'random', 'scheduled'
1172             ),
1174             'workshopeval' => array(
1175                 'best'
1176             ),
1178             'workshopform' => array(
1179                 'accumulative', 'comments', 'numerrors', 'rubric'
1180             )
1181         );
1183         if (isset($standard_plugins[$type])) {
1184             return $standard_plugins[$type];
1185         } else {
1186             return false;
1187         }
1188     }
1190     /**
1191      * Reorders plugin types into a sequence to be displayed
1192      *
1193      * For technical reasons, plugin types returned by {@link core_component::get_plugin_types()} are
1194      * in a certain order that does not need to fit the expected order for the display.
1195      * Particularly, activity modules should be displayed first as they represent the
1196      * real heart of Moodle. They should be followed by other plugin types that are
1197      * used to build the courses (as that is what one expects from LMS). After that,
1198      * other supportive plugin types follow.
1199      *
1200      * @param array $types associative array
1201      * @return array same array with altered order of items
1202      */
1203     protected function reorder_plugin_types(array $types) {
1204         $fix = array('mod' => $types['mod']);
1205         foreach (core_component::get_plugin_list('mod') as $plugin => $fulldir) {
1206             if (!$subtypes = core_component::get_subplugins('mod_'.$plugin)) {
1207                 continue;
1208             }
1209             foreach ($subtypes as $subtype => $ignored) {
1210                 $fix[$subtype] = $types[$subtype];
1211             }
1212         }
1214         $fix['mod']        = $types['mod'];
1215         $fix['block']      = $types['block'];
1216         $fix['qtype']      = $types['qtype'];
1217         $fix['qbehaviour'] = $types['qbehaviour'];
1218         $fix['qformat']    = $types['qformat'];
1219         $fix['filter']     = $types['filter'];
1221         $fix['editor']     = $types['editor'];
1222         foreach (core_component::get_plugin_list('editor') as $plugin => $fulldir) {
1223             if (!$subtypes = core_component::get_subplugins('editor_'.$plugin)) {
1224                 continue;
1225             }
1226             foreach ($subtypes as $subtype => $ignored) {
1227                 $fix[$subtype] = $types[$subtype];
1228             }
1229         }
1231         $fix['enrol'] = $types['enrol'];
1232         $fix['auth']  = $types['auth'];
1233         $fix['tool']  = $types['tool'];
1234         foreach (core_component::get_plugin_list('tool') as $plugin => $fulldir) {
1235             if (!$subtypes = core_component::get_subplugins('tool_'.$plugin)) {
1236                 continue;
1237             }
1238             foreach ($subtypes as $subtype => $ignored) {
1239                 $fix[$subtype] = $types[$subtype];
1240             }
1241         }
1243         foreach ($types as $type => $path) {
1244             if (!isset($fix[$type])) {
1245                 $fix[$type] = $path;
1246             }
1247         }
1248         return $fix;
1249     }
1251     /**
1252      * Check if the given directory can be removed by the web server process.
1253      *
1254      * This recursively checks that the given directory and all its contents
1255      * it writable.
1256      *
1257      * @param string $fullpath
1258      * @return boolean
1259      */
1260     protected function is_directory_removable($fullpath) {
1262         if (!is_writable($fullpath)) {
1263             return false;
1264         }
1266         if (is_dir($fullpath)) {
1267             $handle = opendir($fullpath);
1268         } else {
1269             return false;
1270         }
1272         $result = true;
1274         while ($filename = readdir($handle)) {
1276             if ($filename === '.' or $filename === '..') {
1277                 continue;
1278             }
1280             $subfilepath = $fullpath.'/'.$filename;
1282             if (is_dir($subfilepath)) {
1283                 $result = $result && $this->is_directory_removable($subfilepath);
1285             } else {
1286                 $result = $result && is_writable($subfilepath);
1287             }
1288         }
1290         closedir($handle);
1292         return $result;
1293     }
1295     /**
1296      * Helper method that implements common uninstall prerequisites
1297      *
1298      * @param \core\plugininfo\base $pluginfo
1299      * @return bool
1300      */
1301     protected function common_uninstall_check(\core\plugininfo\base $pluginfo) {
1303         if (!$pluginfo->is_uninstall_allowed()) {
1304             // The plugin's plugininfo class declares it should not be uninstalled.
1305             return false;
1306         }
1308         if ($pluginfo->get_status() === self::PLUGIN_STATUS_NEW) {
1309             // The plugin is not installed. It should be either installed or removed from the disk.
1310             // Relying on this temporary state may be tricky.
1311             return false;
1312         }
1314         if (method_exists($pluginfo, 'get_uninstall_url') and is_null($pluginfo->get_uninstall_url())) {
1315             // Backwards compatibility.
1316             debugging('\core\plugininfo\base subclasses should use is_uninstall_allowed() instead of returning null in get_uninstall_url()',
1317                 DEBUG_DEVELOPER);
1318             return false;
1319         }
1321         return true;
1322     }