Commit | Line | Data |
---|---|---|
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 | ||
29 | defined('MOODLE_INTERNAL') || die(); | |
30 | ||
31 | /** | |
32 | * Singleton class providing general plugins management functionality. | |
33 | */ | |
34 | class 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 | } |