source: moodle/trunk/fuentes/lib/classes/plugin_manager.php @ 136

Last change on this file since 136 was 136, checked in by mabarracus, 4 years ago

Ported code to xenial

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