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