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