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

Last change on this file since 1331 was 1331, checked in by jrpelegrina, 3 years ago

Updated to moodle 3.0.3

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