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