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