MDL-69166 core: define payment as a subsystem and pg as a plugin type
[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';
147a895a
PB
62 /** the current Moodle version is too high for plugin. */
63 const REQUIREMENT_STATUS_NEWER = 'newer';
7eb87eff 64
5a92cd0b
DM
65 /** the required dependency is available in the plugins directory */
66 const REQUIREMENT_AVAILABLE = 'available';
67 /** the required dependency is available in the plugins directory */
68 const REQUIREMENT_UNAVAILABLE = 'unavailable';
69
147a895a
PB
70 /** the moodle version is explicitly supported */
71 const VERSION_SUPPORTED = 'supported';
72 /** the moodle version is not explicitly supported */
73 const VERSION_NOT_SUPPORTED = 'notsupported';
74 /** the plugin does not specify supports */
75 const VERSION_NO_SUPPORTS = 'nosupports';
76
e87214bd
PS
77 /** @var core_plugin_manager holds the singleton instance */
78 protected static $singletoninstance;
79 /** @var array of raw plugins information */
80 protected $pluginsinfo = null;
81 /** @var array of raw subplugins information */
82 protected $subpluginsinfo = null;
35f2b674
DM
83 /** @var array cache information about availability in the plugins directory if requesting "at least" version */
84 protected $remotepluginsinfoatleast = null;
85 /** @var array cache information about availability in the plugins directory if requesting exact version */
86 protected $remotepluginsinfoexact = null;
e87214bd
PS
87 /** @var array list of installed plugins $name=>$version */
88 protected $installedplugins = null;
89 /** @var array list of all enabled plugins $name=>$name */
90 protected $enabledplugins = null;
91 /** @var array list of all enabled plugins $name=>$diskversion */
92 protected $presentplugins = null;
93 /** @var array reordered list of plugin types */
94 protected $plugintypes = null;
0e442ee7
DM
95 /** @var \core\update\code_manager code manager to use for plugins code operations */
96 protected $codemanager = null;
35f2b674
DM
97 /** @var \core\update\api client instance to use for accessing download.moodle.org/api/ */
98 protected $updateapiclient = null;
e87214bd
PS
99
100 /**
101 * Direct initiation not allowed, use the factory method {@link self::instance()}
102 */
103 protected function __construct() {
104 }
105
106 /**
107 * Sorry, this is singleton
108 */
109 protected function __clone() {
110 }
111
112 /**
113 * Factory method for this class
114 *
115 * @return core_plugin_manager the singleton instance
116 */
117 public static function instance() {
361feecd
DM
118 if (is_null(static::$singletoninstance)) {
119 static::$singletoninstance = new static();
e87214bd 120 }
361feecd 121 return static::$singletoninstance;
e87214bd
PS
122 }
123
124 /**
125 * Reset all caches.
126 * @param bool $phpunitreset
127 */
128 public static function reset_caches($phpunitreset = false) {
129 if ($phpunitreset) {
361feecd 130 static::$singletoninstance = null;
e87214bd 131 } else {
361feecd
DM
132 if (static::$singletoninstance) {
133 static::$singletoninstance->pluginsinfo = null;
134 static::$singletoninstance->subpluginsinfo = null;
35f2b674
DM
135 static::$singletoninstance->remotepluginsinfoatleast = null;
136 static::$singletoninstance->remotepluginsinfoexact = null;
361feecd
DM
137 static::$singletoninstance->installedplugins = null;
138 static::$singletoninstance->enabledplugins = null;
139 static::$singletoninstance->presentplugins = null;
140 static::$singletoninstance->plugintypes = null;
35f2b674
DM
141 static::$singletoninstance->codemanager = null;
142 static::$singletoninstance->updateapiclient = null;
e87214bd
PS
143 }
144 }
145 $cache = cache::make('core', 'plugin_manager');
146 $cache->purge();
147 }
148
149 /**
150 * Returns the result of {@link core_component::get_plugin_types()} ordered for humans
151 *
152 * @see self::reorder_plugin_types()
153 * @return array (string)name => (string)location
154 */
155 public function get_plugin_types() {
156 if (func_num_args() > 0) {
157 if (!func_get_arg(0)) {
6d721cab 158 throw new coding_exception('core_plugin_manager->get_plugin_types() does not support relative paths.');
e87214bd
PS
159 }
160 }
161 if ($this->plugintypes) {
162 return $this->plugintypes;
163 }
164
165 $this->plugintypes = $this->reorder_plugin_types(core_component::get_plugin_types());
166 return $this->plugintypes;
167 }
168
169 /**
170 * Load list of installed plugins,
171 * always call before using $this->installedplugins.
172 *
173 * This method is caching results for all plugins.
174 */
175 protected function load_installed_plugins() {
176 global $DB, $CFG;
177
178 if ($this->installedplugins) {
179 return;
180 }
181
182 if (empty($CFG->version)) {
183 // Nothing installed yet.
184 $this->installedplugins = array();
185 return;
186 }
187
188 $cache = cache::make('core', 'plugin_manager');
189 $installed = $cache->get('installed');
190
191 if (is_array($installed)) {
192 $this->installedplugins = $installed;
193 return;
194 }
195
196 $this->installedplugins = array();
197
e87214bd
PS
198 $versions = $DB->get_records('config_plugins', array('name'=>'version'));
199 foreach ($versions as $version) {
200 $parts = explode('_', $version->plugin, 2);
201 if (!isset($parts[1])) {
202 // Invalid component, there must be at least one "_".
203 continue;
204 }
205 // Do not verify here if plugin type and name are valid.
206 $this->installedplugins[$parts[0]][$parts[1]] = $version->value;
207 }
208
209 foreach ($this->installedplugins as $key => $value) {
210 ksort($this->installedplugins[$key]);
211 }
212
213 $cache->set('installed', $this->installedplugins);
214 }
215
216 /**
217 * Return list of installed plugins of given type.
218 * @param string $type
219 * @return array $name=>$version
220 */
221 public function get_installed_plugins($type) {
222 $this->load_installed_plugins();
223 if (isset($this->installedplugins[$type])) {
224 return $this->installedplugins[$type];
225 }
226 return array();
227 }
228
229 /**
230 * Load list of all enabled plugins,
231 * call before using $this->enabledplugins.
232 *
233 * This method is caching results from individual plugin info classes.
234 */
235 protected function load_enabled_plugins() {
236 global $CFG;
237
238 if ($this->enabledplugins) {
239 return;
240 }
241
242 if (empty($CFG->version)) {
243 $this->enabledplugins = array();
244 return;
245 }
246
247 $cache = cache::make('core', 'plugin_manager');
248 $enabled = $cache->get('enabled');
249
250 if (is_array($enabled)) {
251 $this->enabledplugins = $enabled;
252 return;
253 }
254
255 $this->enabledplugins = array();
256
257 require_once($CFG->libdir.'/adminlib.php');
258
259 $plugintypes = core_component::get_plugin_types();
260 foreach ($plugintypes as $plugintype => $fulldir) {
361feecd 261 $plugininfoclass = static::resolve_plugininfo_class($plugintype);
e87214bd
PS
262 if (class_exists($plugininfoclass)) {
263 $enabled = $plugininfoclass::get_enabled_plugins();
264 if (!is_array($enabled)) {
265 continue;
266 }
267 $this->enabledplugins[$plugintype] = $enabled;
268 }
269 }
270
271 $cache->set('enabled', $this->enabledplugins);
272 }
273
274 /**
275 * Get list of enabled plugins of given type,
276 * the result may contain missing plugins.
277 *
278 * @param string $type
279 * @return array|null list of enabled plugins of this type, null if unknown
280 */
281 public function get_enabled_plugins($type) {
282 $this->load_enabled_plugins();
283 if (isset($this->enabledplugins[$type])) {
284 return $this->enabledplugins[$type];
285 }
286 return null;
287 }
288
289 /**
290 * Load list of all present plugins - call before using $this->presentplugins.
291 */
292 protected function load_present_plugins() {
293 if ($this->presentplugins) {
294 return;
295 }
296
297 $cache = cache::make('core', 'plugin_manager');
298 $present = $cache->get('present');
299
300 if (is_array($present)) {
301 $this->presentplugins = $present;
302 return;
303 }
304
305 $this->presentplugins = array();
306
307 $plugintypes = core_component::get_plugin_types();
308 foreach ($plugintypes as $type => $typedir) {
309 $plugs = core_component::get_plugin_list($type);
310 foreach ($plugs as $plug => $fullplug) {
01889f01 311 $module = new stdClass();
e87214bd
PS
312 $plugin = new stdClass();
313 $plugin->version = null;
0b468c59 314 include($fullplug.'/version.php');
01889f01
DM
315
316 // Check if the legacy $module syntax is still used.
17d2a336 317 if (!is_object($module) or (count((array)$module) > 0)) {
01889f01
DM
318 debugging('Unsupported $module syntax detected in version.php of the '.$type.'_'.$plug.' plugin.');
319 $skipcache = true;
320 }
321
98ea6973
DM
322 // Check if the component is properly declared.
323 if (empty($plugin->component) or ($plugin->component !== $type.'_'.$plug)) {
324 debugging('Plugin '.$type.'_'.$plug.' does not declare valid $plugin->component in its version.php.');
325 $skipcache = true;
326 }
327
e87214bd
PS
328 $this->presentplugins[$type][$plug] = $plugin;
329 }
330 }
331
01889f01
DM
332 if (empty($skipcache)) {
333 $cache->set('present', $this->presentplugins);
334 }
e87214bd
PS
335 }
336
337 /**
338 * Get list of present plugins of given type.
339 *
340 * @param string $type
341 * @return array|null list of presnet plugins $name=>$diskversion, null if unknown
342 */
343 public function get_present_plugins($type) {
344 $this->load_present_plugins();
345 if (isset($this->presentplugins[$type])) {
346 return $this->presentplugins[$type];
347 }
348 return null;
349 }
350
351 /**
352 * Returns a tree of known plugins and information about them
353 *
354 * @return array 2D array. The first keys are plugin type names (e.g. qtype);
355 * the second keys are the plugin local name (e.g. multichoice); and
356 * the values are the corresponding objects extending {@link \core\plugininfo\base}
357 */
358 public function get_plugins() {
359 $this->init_pluginsinfo_property();
360
361 // Make sure all types are initialised.
362 foreach ($this->pluginsinfo as $plugintype => $list) {
363 if ($list === null) {
364 $this->get_plugins_of_type($plugintype);
365 }
366 }
367
368 return $this->pluginsinfo;
369 }
370
371 /**
372 * Returns list of known plugins of the given type.
373 *
374 * This method returns the subset of the tree returned by {@link self::get_plugins()}.
375 * If the given type is not known, empty array is returned.
376 *
377 * @param string $type plugin type, e.g. 'mod' or 'workshopallocation'
378 * @return \core\plugininfo\base[] (string)plugin name (e.g. 'workshop') => corresponding subclass of {@link \core\plugininfo\base}
379 */
380 public function get_plugins_of_type($type) {
381 global $CFG;
382
383 $this->init_pluginsinfo_property();
384
385 if (!array_key_exists($type, $this->pluginsinfo)) {
386 return array();
387 }
388
389 if (is_array($this->pluginsinfo[$type])) {
390 return $this->pluginsinfo[$type];
391 }
392
393 $types = core_component::get_plugin_types();
394
a35fce24
PS
395 if (!isset($types[$type])) {
396 // Orphaned subplugins!
361feecd 397 $plugintypeclass = static::resolve_plugininfo_class($type);
2d488c8f 398 $this->pluginsinfo[$type] = $plugintypeclass::get_plugins($type, null, $plugintypeclass, $this);
a35fce24
PS
399 return $this->pluginsinfo[$type];
400 }
401
e87214bd 402 /** @var \core\plugininfo\base $plugintypeclass */
361feecd 403 $plugintypeclass = static::resolve_plugininfo_class($type);
2d488c8f 404 $plugins = $plugintypeclass::get_plugins($type, $types[$type], $plugintypeclass, $this);
e87214bd
PS
405 $this->pluginsinfo[$type] = $plugins;
406
e87214bd
PS
407 return $this->pluginsinfo[$type];
408 }
409
410 /**
411 * Init placeholder array for plugin infos.
412 */
413 protected function init_pluginsinfo_property() {
414 if (is_array($this->pluginsinfo)) {
415 return;
416 }
417 $this->pluginsinfo = array();
418
419 $plugintypes = $this->get_plugin_types();
420
421 foreach ($plugintypes as $plugintype => $plugintyperootdir) {
422 $this->pluginsinfo[$plugintype] = null;
423 }
a35fce24
PS
424
425 // Add orphaned subplugin types.
426 $this->load_installed_plugins();
427 foreach ($this->installedplugins as $plugintype => $unused) {
428 if (!isset($plugintypes[$plugintype])) {
429 $this->pluginsinfo[$plugintype] = null;
430 }
431 }
e87214bd
PS
432 }
433
434 /**
435 * Find the plugin info class for given type.
436 *
437 * @param string $type
438 * @return string name of pluginfo class for give plugin type
439 */
440 public static function resolve_plugininfo_class($type) {
a35fce24
PS
441 $plugintypes = core_component::get_plugin_types();
442 if (!isset($plugintypes[$type])) {
443 return '\core\plugininfo\orphaned';
444 }
445
e87214bd
PS
446 $parent = core_component::get_subtype_parent($type);
447
448 if ($parent) {
449 $class = '\\'.$parent.'\plugininfo\\' . $type;
450 if (class_exists($class)) {
451 $plugintypeclass = $class;
452 } else {
453 if ($dir = core_component::get_component_directory($parent)) {
454 // BC only - use namespace instead!
455 if (file_exists("$dir/adminlib.php")) {
456 global $CFG;
457 include_once("$dir/adminlib.php");
458 }
459 if (class_exists('plugininfo_' . $type)) {
460 $plugintypeclass = 'plugininfo_' . $type;
461 debugging('Class "'.$plugintypeclass.'" is deprecated, migrate to "'.$class.'"', DEBUG_DEVELOPER);
462 } else {
463 debugging('Subplugin type "'.$type.'" should define class "'.$class.'"', DEBUG_DEVELOPER);
464 $plugintypeclass = '\core\plugininfo\general';
465 }
466 } else {
467 $plugintypeclass = '\core\plugininfo\general';
468 }
469 }
470 } else {
471 $class = '\core\plugininfo\\' . $type;
472 if (class_exists($class)) {
473 $plugintypeclass = $class;
474 } else {
475 debugging('All standard types including "'.$type.'" should have plugininfo class!', DEBUG_DEVELOPER);
476 $plugintypeclass = '\core\plugininfo\general';
477 }
478 }
479
480 if (!in_array('core\plugininfo\base', class_parents($plugintypeclass))) {
481 throw new coding_exception('Class ' . $plugintypeclass . ' must extend \core\plugininfo\base');
482 }
483
484 return $plugintypeclass;
485 }
486
487 /**
488 * Returns list of all known subplugins of the given plugin.
489 *
490 * For plugins that do not provide subplugins (i.e. there is no support for it),
491 * empty array is returned.
492 *
493 * @param string $component full component name, e.g. 'mod_workshop'
494 * @return array (string) component name (e.g. 'workshopallocation_random') => subclass of {@link \core\plugininfo\base}
495 */
496 public function get_subplugins_of_plugin($component) {
497
498 $pluginfo = $this->get_plugin_info($component);
499
500 if (is_null($pluginfo)) {
501 return array();
502 }
503
504 $subplugins = $this->get_subplugins();
505
506 if (!isset($subplugins[$pluginfo->component])) {
507 return array();
508 }
509
510 $list = array();
511
512 foreach ($subplugins[$pluginfo->component] as $subdata) {
513 foreach ($this->get_plugins_of_type($subdata->type) as $subpluginfo) {
514 $list[$subpluginfo->component] = $subpluginfo;
515 }
516 }
517
518 return $list;
519 }
520
521 /**
522 * Returns list of plugins that define their subplugins and the information
29c442c5 523 * about them from the db/subplugins.json file.
e87214bd
PS
524 *
525 * @return array with keys like 'mod_quiz', and values the data from the
29c442c5 526 * corresponding db/subplugins.json file.
e87214bd
PS
527 */
528 public function get_subplugins() {
529
530 if (is_array($this->subpluginsinfo)) {
531 return $this->subpluginsinfo;
532 }
533
534 $plugintypes = core_component::get_plugin_types();
535
536 $this->subpluginsinfo = array();
537 foreach (core_component::get_plugin_types_with_subplugins() as $type => $ignored) {
538 foreach (core_component::get_plugin_list($type) as $plugin => $componentdir) {
539 $component = $type.'_'.$plugin;
540 $subplugins = core_component::get_subplugins($component);
541 if (!$subplugins) {
542 continue;
543 }
544 $this->subpluginsinfo[$component] = array();
545 foreach ($subplugins as $subplugintype => $ignored) {
546 $subplugin = new stdClass();
547 $subplugin->type = $subplugintype;
548 $subplugin->typerootdir = $plugintypes[$subplugintype];
549 $this->subpluginsinfo[$component][$subplugintype] = $subplugin;
550 }
551 }
552 }
553 return $this->subpluginsinfo;
554 }
555
556 /**
557 * Returns the name of the plugin that defines the given subplugin type
558 *
559 * If the given subplugin type is not actually a subplugin, returns false.
560 *
561 * @param string $subplugintype the name of subplugin type, eg. workshopform or quiz
562 * @return false|string the name of the parent plugin, eg. mod_workshop
563 */
564 public function get_parent_of_subplugin($subplugintype) {
565 $parent = core_component::get_subtype_parent($subplugintype);
566 if (!$parent) {
567 return false;
568 }
569 return $parent;
570 }
571
572 /**
573 * Returns a localized name of a given plugin
574 *
575 * @param string $component name of the plugin, eg mod_workshop or auth_ldap
576 * @return string
577 */
578 public function plugin_name($component) {
579
580 $pluginfo = $this->get_plugin_info($component);
581
582 if (is_null($pluginfo)) {
583 throw new moodle_exception('err_unknown_plugin', 'core_plugin', '', array('plugin' => $component));
584 }
585
586 return $pluginfo->displayname;
587 }
588
589 /**
590 * Returns a localized name of a plugin typed in singular form
591 *
592 * Most plugin types define their names in core_plugin lang file. In case of subplugins,
593 * we try to ask the parent plugin for the name. In the worst case, we will return
594 * the value of the passed $type parameter.
595 *
596 * @param string $type the type of the plugin, e.g. mod or workshopform
597 * @return string
598 */
599 public function plugintype_name($type) {
600
601 if (get_string_manager()->string_exists('type_' . $type, 'core_plugin')) {
602 // For most plugin types, their names are defined in core_plugin lang file.
603 return get_string('type_' . $type, 'core_plugin');
604
605 } else if ($parent = $this->get_parent_of_subplugin($type)) {
606 // If this is a subplugin, try to ask the parent plugin for the name.
607 if (get_string_manager()->string_exists('subplugintype_' . $type, $parent)) {
608 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type, $parent);
609 } else {
610 return $this->plugin_name($parent) . ' / ' . $type;
611 }
612
613 } else {
614 return $type;
615 }
616 }
617
618 /**
619 * Returns a localized name of a plugin type in plural form
620 *
621 * Most plugin types define their names in core_plugin lang file. In case of subplugins,
622 * we try to ask the parent plugin for the name. In the worst case, we will return
623 * the value of the passed $type parameter.
624 *
625 * @param string $type the type of the plugin, e.g. mod or workshopform
626 * @return string
627 */
628 public function plugintype_name_plural($type) {
629
630 if (get_string_manager()->string_exists('type_' . $type . '_plural', 'core_plugin')) {
631 // For most plugin types, their names are defined in core_plugin lang file.
632 return get_string('type_' . $type . '_plural', 'core_plugin');
633
634 } else if ($parent = $this->get_parent_of_subplugin($type)) {
635 // If this is a subplugin, try to ask the parent plugin for the name.
636 if (get_string_manager()->string_exists('subplugintype_' . $type . '_plural', $parent)) {
637 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type . '_plural', $parent);
638 } else {
639 return $this->plugin_name($parent) . ' / ' . $type;
640 }
641
642 } else {
643 return $type;
644 }
645 }
646
647 /**
648 * Returns information about the known plugin, or null
649 *
650 * @param string $component frankenstyle component name.
651 * @return \core\plugininfo\base|null the corresponding plugin information.
652 */
653 public function get_plugin_info($component) {
654 list($type, $name) = core_component::normalize_component($component);
2384d331
PS
655 $plugins = $this->get_plugins_of_type($type);
656 if (isset($plugins[$name])) {
657 return $plugins[$name];
e87214bd
PS
658 } else {
659 return null;
660 }
661 }
662
663 /**
664 * Check to see if the current version of the plugin seems to be a checkout of an external repository.
665 *
e87214bd
PS
666 * @param string $component frankenstyle component name
667 * @return false|string
668 */
669 public function plugin_external_source($component) {
670
671 $plugininfo = $this->get_plugin_info($component);
672
673 if (is_null($plugininfo)) {
674 return false;
675 }
676
677 $pluginroot = $plugininfo->rootdir;
678
679 if (is_dir($pluginroot.'/.git')) {
680 return 'git';
681 }
682
a5d08dce
DM
683 if (is_file($pluginroot.'/.git')) {
684 return 'git-submodule';
685 }
686
e87214bd
PS
687 if (is_dir($pluginroot.'/CVS')) {
688 return 'cvs';
689 }
690
691 if (is_dir($pluginroot.'/.svn')) {
692 return 'svn';
693 }
694
0b515736
OS
695 if (is_dir($pluginroot.'/.hg')) {
696 return 'mercurial';
697 }
698
e87214bd
PS
699 return false;
700 }
701
702 /**
703 * Get a list of any other plugins that require this one.
704 * @param string $component frankenstyle component name.
705 * @return array of frankensyle component names that require this one.
706 */
707 public function other_plugins_that_require($component) {
708 $others = array();
709 foreach ($this->get_plugins() as $type => $plugins) {
710 foreach ($plugins as $plugin) {
711 $required = $plugin->get_other_required_plugins();
712 if (isset($required[$component])) {
713 $others[] = $plugin->component;
714 }
715 }
716 }
717 return $others;
718 }
719
720 /**
721 * Check a dependencies list against the list of installed plugins.
722 * @param array $dependencies compenent name to required version or ANY_VERSION.
723 * @return bool true if all the dependencies are satisfied.
724 */
725 public function are_dependencies_satisfied($dependencies) {
726 foreach ($dependencies as $component => $requiredversion) {
727 $otherplugin = $this->get_plugin_info($component);
728 if (is_null($otherplugin)) {
729 return false;
730 }
731
732 if ($requiredversion != ANY_VERSION and $otherplugin->versiondisk < $requiredversion) {
733 return false;
734 }
735 }
736
737 return true;
738 }
739
740 /**
741 * Checks all dependencies for all installed plugins
742 *
743 * This is used by install and upgrade. The array passed by reference as the second
744 * argument is populated with the list of plugins that have failed dependencies (note that
745 * a single plugin can appear multiple times in the $failedplugins).
746 *
747 * @param int $moodleversion the version from version.php.
748 * @param array $failedplugins to return the list of plugins with non-satisfied dependencies
147a895a 749 * @param int $branch the current moodle branch, null if not provided
e87214bd
PS
750 * @return bool true if all the dependencies are satisfied for all plugins.
751 */
147a895a
PB
752 public function all_plugins_ok($moodleversion, &$failedplugins = array(), $branch = null) {
753 global $CFG;
754 if (empty($branch)) {
6658d9fb 755 $branch = $CFG->branch ?? '';
4fac690a
PB
756 if (empty($branch)) {
757 // During initial install there is no branch set.
758 require($CFG->dirroot . '/version.php');
759 $branch = (int)$branch;
760 // Force CFG->branch to int value during install.
761 $CFG->branch = $branch;
762 }
147a895a 763 }
e87214bd
PS
764 $return = true;
765 foreach ($this->get_plugins() as $type => $plugins) {
766 foreach ($plugins as $plugin) {
767
768 if (!$plugin->is_core_dependency_satisfied($moodleversion)) {
769 $return = false;
770 $failedplugins[] = $plugin->component;
771 }
772
773 if (!$this->are_dependencies_satisfied($plugin->get_other_required_plugins())) {
774 $return = false;
775 $failedplugins[] = $plugin->component;
776 }
147a895a
PB
777
778 if (!$plugin->is_core_compatible_satisfied($branch)) {
779 $return = false;
780 $failedplugins[] = $plugin->component;
781 }
e87214bd
PS
782 }
783 }
784
785 return $return;
786 }
787
7eb87eff
DM
788 /**
789 * Resolve requirements and dependencies of a plugin.
790 *
791 * Returns an array of objects describing the requirement/dependency,
792 * indexed by the frankenstyle name of the component. The returned array
793 * can be empty. The objects in the array have following properties:
794 *
795 * ->(numeric)hasver
796 * ->(numeric)reqver
797 * ->(string)status
5a92cd0b 798 * ->(string)availability
7eb87eff
DM
799 *
800 * @param \core\plugininfo\base $plugin the plugin we are checking
801 * @param null|string|int|double $moodleversion explicit moodle core version to check against, defaults to $CFG->version
802 * @param null|string|int $moodlebranch explicit moodle core branch to check against, defaults to $CFG->branch
803 * @return array of objects
804 */
805 public function resolve_requirements(\core\plugininfo\base $plugin, $moodleversion=null, $moodlebranch=null) {
806 global $CFG;
807
4441d5e5
DM
808 if ($plugin->versiondisk === null) {
809 // Missing from disk, we have no version.php to read from.
810 return array();
811 }
812
7eb87eff
DM
813 if ($moodleversion === null) {
814 $moodleversion = $CFG->version;
815 }
816
817 if ($moodlebranch === null) {
818 $moodlebranch = $CFG->branch;
819 }
820
821 $reqs = array();
147a895a 822 $reqcore = $this->resolve_core_requirements($plugin, $moodleversion, $moodlebranch);
7eb87eff
DM
823
824 if (!empty($reqcore)) {
825 $reqs['core'] = $reqcore;
826 }
827
828 foreach ($plugin->get_other_required_plugins() as $reqplug => $reqver) {
829 $reqs[$reqplug] = $this->resolve_dependency_requirements($plugin, $reqplug, $reqver, $moodlebranch);
830 }
831
832 return $reqs;
833 }
834
835 /**
836 * Helper method to resolve plugin's requirements on the moodle core.
837 *
838 * @param \core\plugininfo\base $plugin the plugin we are checking
839 * @param string|int|double $moodleversion moodle core branch to check against
840 * @return stdObject
841 */
147a895a 842 protected function resolve_core_requirements(\core\plugininfo\base $plugin, $moodleversion, $moodlebranch) {
7eb87eff 843
5a92cd0b
DM
844 $reqs = (object)array(
845 'hasver' => null,
846 'reqver' => null,
847 'status' => null,
848 'availability' => null,
849 );
7eb87eff
DM
850 $reqs->hasver = $moodleversion;
851
852 if (empty($plugin->versionrequires)) {
853 $reqs->reqver = ANY_VERSION;
854 } else {
855 $reqs->reqver = $plugin->versionrequires;
856 }
857
858 if ($plugin->is_core_dependency_satisfied($moodleversion)) {
859 $reqs->status = self::REQUIREMENT_STATUS_OK;
860 } else {
861 $reqs->status = self::REQUIREMENT_STATUS_OUTDATED;
862 }
863
147a895a
PB
864 // Now check if there is an explicit incompatible, supersedes requires.
865 if (isset($plugin->pluginincompatible) && $plugin->pluginincompatible != null) {
866 if (!$plugin->is_core_compatible_satisfied($moodlebranch)) {
867
868 $reqs->status = self::REQUIREMENT_STATUS_NEWER;
869 }
870 }
871
7eb87eff
DM
872 return $reqs;
873 }
874
875 /**
876 * Helper method to resolve plugin's dependecies on other plugins.
877 *
878 * @param \core\plugininfo\base $plugin the plugin we are checking
879 * @param string $otherpluginname
880 * @param string|int $requiredversion
881 * @param string|int $moodlebranch explicit moodle core branch to check against, defaults to $CFG->branch
882 * @return stdClass
883 */
884 protected function resolve_dependency_requirements(\core\plugininfo\base $plugin, $otherpluginname,
885 $requiredversion, $moodlebranch) {
886
5a92cd0b
DM
887 $reqs = (object)array(
888 'hasver' => null,
889 'reqver' => null,
890 'status' => null,
891 'availability' => null,
892 );
893
7eb87eff
DM
894 $otherplugin = $this->get_plugin_info($otherpluginname);
895
896 if ($otherplugin !== null) {
897 // The required plugin is installed.
898 $reqs->hasver = $otherplugin->versiondisk;
899 $reqs->reqver = $requiredversion;
900 // Check it has sufficient version.
901 if ($requiredversion == ANY_VERSION or $otherplugin->versiondisk >= $requiredversion) {
902 $reqs->status = self::REQUIREMENT_STATUS_OK;
903 } else {
904 $reqs->status = self::REQUIREMENT_STATUS_OUTDATED;
905 }
906
907 } else {
908 // The required plugin is not installed.
909 $reqs->hasver = null;
910 $reqs->reqver = $requiredversion;
911 $reqs->status = self::REQUIREMENT_STATUS_MISSING;
5a92cd0b
DM
912 }
913
914 if ($reqs->status !== self::REQUIREMENT_STATUS_OK) {
35f2b674 915 if ($this->is_remote_plugin_available($otherpluginname, $requiredversion, false)) {
5a92cd0b
DM
916 $reqs->availability = self::REQUIREMENT_AVAILABLE;
917 } else {
918 $reqs->availability = self::REQUIREMENT_UNAVAILABLE;
919 }
7eb87eff
DM
920 }
921
922 return $reqs;
923 }
924
147a895a
PB
925 /**
926 * Helper method to determine whether a moodle version is explicitly supported.
927 *
928 * @param \core\plugininfo\base $plugin the plugin we are checking
929 * @param int $branch the moodle branch to check support for
72aac063 930 * @return string
147a895a 931 */
72aac063 932 public function check_explicitly_supported($plugin, $branch) : string {
147a895a
PB
933 // Check for correctly formed supported.
934 if (isset($plugin->pluginsupported)) {
935 // Broken apart for readability.
936 $error = false;
937 if (!is_array($plugin->pluginsupported)) {
938 $error = true;
939 }
940 if (!is_int($plugin->pluginsupported[0]) || !is_int($plugin->pluginsupported[1])) {
941 $error = true;
942 }
943 if (count($plugin->pluginsupported) != 2) {
944 $error = true;
945 }
946 if ($error) {
947 throw new coding_exception(get_string('err_supported_syntax', 'core_plugin'));
948 }
949 }
950
951 if (isset($plugin->pluginsupported) && $plugin->pluginsupported != null) {
952 if ($plugin->pluginsupported[0] <= $branch && $branch <= $plugin->pluginsupported[1]) {
953 return self::VERSION_SUPPORTED;
954 } else {
955 return self::VERSION_NOT_SUPPORTED;
956 }
957 } else {
958 // If supports aren't specified, but incompatible is, return not supported if not incompatible.
959 if (!isset($plugin->pluginsupported) && isset($plugin->pluginincompatible) && !empty($plugin->pluginincompatible)) {
960 if (!$plugin->is_core_compatible_satisfied($branch)) {
961 return self::VERSION_NOT_SUPPORTED;
962 }
963 }
964 return self::VERSION_NO_SUPPORTS;
965 }
966 }
967
5a92cd0b
DM
968 /**
969 * Is the given plugin version available in the plugins directory?
970 *
35f2b674
DM
971 * See {@link self::get_remote_plugin_info()} for the full explanation of how the $version
972 * parameter is interpretted.
973 *
974 * @param string $component plugin frankenstyle name
975 * @param string|int $version ANY_VERSION or the version number
976 * @param bool $exactmatch false if "given version or higher" is requested
5a92cd0b
DM
977 * @return boolean
978 */
35f2b674 979 public function is_remote_plugin_available($component, $version, $exactmatch) {
5a92cd0b 980
35f2b674 981 $info = $this->get_remote_plugin_info($component, $version, $exactmatch);
5a92cd0b
DM
982
983 if (empty($info)) {
984 // There is no available plugin of that name.
985 return false;
986 }
987
988 if (empty($info->version)) {
989 // Plugin is known, but no suitable version was found.
990 return false;
991 }
992
993 return true;
994 }
995
9137a89a 996 /**
35f2b674 997 * Can the given plugin version be installed via the admin UI?
9137a89a 998 *
36977a6d
DM
999 * This check should be used whenever attempting to install a plugin from
1000 * the plugins directory (new install, available update, missing dependency).
1001 *
9137a89a 1002 * @param string $component
35f2b674 1003 * @param int $version version number
30c26421 1004 * @param string $reason returned code of the reason why it is not
9137a89a
DM
1005 * @return boolean
1006 */
36977a6d 1007 public function is_remote_plugin_installable($component, $version, &$reason=null) {
9137a89a
DM
1008 global $CFG;
1009
1010 // Make sure the feature is not disabled.
b0fc7898 1011 if (!empty($CFG->disableupdateautodeploy)) {
36977a6d 1012 $reason = 'disabled';
9137a89a
DM
1013 return false;
1014 }
1015
36977a6d 1016 // Make sure the version is available.
35f2b674 1017 if (!$this->is_remote_plugin_available($component, $version, true)) {
36977a6d 1018 $reason = 'remoteunavailable';
9137a89a
DM
1019 return false;
1020 }
1021
1022 // Make sure the plugin type root directory is writable.
1023 list($plugintype, $pluginname) = core_component::normalize_component($component);
1024 if (!$this->is_plugintype_writable($plugintype)) {
36977a6d 1025 $reason = 'notwritableplugintype';
9137a89a
DM
1026 return false;
1027 }
1028
35f2b674 1029 $remoteinfo = $this->get_remote_plugin_info($component, $version, true);
9137a89a
DM
1030 $localinfo = $this->get_plugin_info($component);
1031
1032 if ($localinfo) {
1033 // If the plugin is already present, prevent downgrade.
36977a6d
DM
1034 if ($localinfo->versiondb > $remoteinfo->version->version) {
1035 $reason = 'cannotdowngrade';
9137a89a
DM
1036 return false;
1037 }
1038
1039 // Make sure we have write access to all the existing code.
36977a6d
DM
1040 if (is_dir($localinfo->rootdir)) {
1041 if (!$this->is_plugin_folder_removable($component)) {
1042 $reason = 'notwritableplugin';
1043 return false;
1044 }
9137a89a
DM
1045 }
1046 }
1047
1048 // Looks like it could work.
1049 return true;
1050 }
1051
c948b813
DM
1052 /**
1053 * Given the list of remote plugin infos, return just those installable.
1054 *
1055 * This is typically used on lists returned by
1056 * {@link self::available_updates()} or {@link self::missing_dependencies()}
1057 * to perform bulk installation of remote plugins.
1058 *
1059 * @param array $remoteinfos list of {@link \core\update\remote_info}
1060 * @return array
1061 */
1062 public function filter_installable($remoteinfos) {
b0fc7898 1063 global $CFG;
c948b813 1064
b0fc7898
DM
1065 if (!empty($CFG->disableupdateautodeploy)) {
1066 return array();
1067 }
c948b813
DM
1068 if (empty($remoteinfos)) {
1069 return array();
1070 }
1071 $installable = array();
1072 foreach ($remoteinfos as $index => $remoteinfo) {
1073 if ($this->is_remote_plugin_installable($remoteinfo->component, $remoteinfo->version->version)) {
1074 $installable[$index] = $remoteinfo;
1075 }
1076 }
1077 return $installable;
1078 }
1079
5a92cd0b
DM
1080 /**
1081 * Returns information about a plugin in the plugins directory.
1082 *
35f2b674
DM
1083 * This is typically used when checking for available dependencies (in
1084 * which case the $version represents minimal version we need), or
1085 * when installing an available update or a new plugin from the plugins
1086 * directory (in which case the $version is exact version we are
1087 * interested in). The interpretation of the $version is controlled
1088 * by the $exactmatch argument.
5a92cd0b 1089 *
35f2b674
DM
1090 * If a plugin with the given component name is found, data about the
1091 * plugin are returned as an object. The ->version property of the object
1092 * contains the information about the particular plugin version that
1093 * matches best the given critera. The ->version property is false if no
1094 * suitable version of the plugin was found (yet the plugin itself is
1095 * known).
1096 *
1097 * See {@link \core\update\api::validate_pluginfo_format()} for the
1098 * returned data structure.
1099 *
1100 * @param string $component plugin frankenstyle name
1101 * @param string|int $version ANY_VERSION or the version number
1102 * @param bool $exactmatch false if "given version or higher" is requested
4f18a4e6 1103 * @return \core\update\remote_info|bool
5a92cd0b 1104 */
35f2b674 1105 public function get_remote_plugin_info($component, $version, $exactmatch) {
5a92cd0b 1106
35f2b674
DM
1107 if ($exactmatch and $version == ANY_VERSION) {
1108 throw new coding_exception('Invalid request for exactly any version, it does not make sense.');
5a92cd0b
DM
1109 }
1110
35f2b674
DM
1111 $client = $this->get_update_api_client();
1112
1113 if ($exactmatch) {
1114 // Use client's get_plugin_info() method.
1115 if (!isset($this->remotepluginsinfoexact[$component][$version])) {
1116 $this->remotepluginsinfoexact[$component][$version] = $client->get_plugin_info($component, $version);
1117 }
1118 return $this->remotepluginsinfoexact[$component][$version];
1119
1120 } else {
1121 // Use client's find_plugin() method.
1122 if (!isset($this->remotepluginsinfoatleast[$component][$version])) {
1123 $this->remotepluginsinfoatleast[$component][$version] = $client->find_plugin($component, $version);
1124 }
1125 return $this->remotepluginsinfoatleast[$component][$version];
1126 }
5a92cd0b
DM
1127 }
1128
1129 /**
0e442ee7
DM
1130 * Obtain the plugin ZIP file from the given URL
1131 *
1132 * The caller is supposed to know both downloads URL and the MD5 hash of
1133 * the ZIP contents in advance, typically by using the API requests against
1134 * the plugins directory.
1135 *
1136 * @param string $url
1137 * @param string $md5
1138 * @return string|bool full path to the file, false on error
1139 */
1140 public function get_remote_plugin_zip($url, $md5) {
b0fc7898
DM
1141 global $CFG;
1142
1143 if (!empty($CFG->disableupdateautodeploy)) {
1144 return false;
1145 }
0e442ee7
DM
1146 return $this->get_code_manager()->get_remote_plugin_zip($url, $md5);
1147 }
1148
1149 /**
1150 * Extracts the saved plugin ZIP file.
1151 *
1152 * Returns the list of files found in the ZIP. The format of that list is
1153 * array of (string)filerelpath => (bool|string) where the array value is
1154 * either true or a string describing the problematic file.
1155 *
1156 * @see zip_packer::extract_to_pathname()
1157 * @param string $zipfilepath full path to the saved ZIP file
1158 * @param string $targetdir full path to the directory to extract the ZIP file to
1159 * @param string $rootdir explicitly rename the root directory of the ZIP into this non-empty value
30c26421 1160 * @return array list of extracted files as returned by {@link zip_packer::extract_to_pathname()}
0e442ee7
DM
1161 */
1162 public function unzip_plugin_file($zipfilepath, $targetdir, $rootdir = '') {
1163 return $this->get_code_manager()->unzip_plugin_file($zipfilepath, $targetdir, $rootdir);
1164 }
1165
2d00be61
DM
1166 /**
1167 * Detects the plugin's name from its ZIP file.
1168 *
1169 * Plugin ZIP packages are expected to contain a single directory and the
1170 * directory name would become the plugin name once extracted to the Moodle
1171 * dirroot.
1172 *
1173 * @param string $zipfilepath full path to the ZIP files
1174 * @return string|bool false on error
1175 */
1176 public function get_plugin_zip_root_dir($zipfilepath) {
1177 return $this->get_code_manager()->get_plugin_zip_root_dir($zipfilepath);
1178 }
1179
0e442ee7
DM
1180 /**
1181 * Return a list of missing dependencies.
5a92cd0b
DM
1182 *
1183 * This should provide the full list of plugins that should be installed to
1184 * fulfill the requirements of all plugins, if possible.
1185 *
0e442ee7 1186 * @param bool $availableonly return only available missing dependencies
4f18a4e6 1187 * @return array of \core\update\remote_info|bool indexed by the component name
5a92cd0b 1188 */
0e442ee7 1189 public function missing_dependencies($availableonly=false) {
5a92cd0b
DM
1190
1191 $dependencies = array();
1192
1193 foreach ($this->get_plugins() as $plugintype => $pluginfos) {
1194 foreach ($pluginfos as $pluginname => $pluginfo) {
1195 foreach ($this->resolve_requirements($pluginfo) as $reqname => $reqinfo) {
1196 if ($reqname === 'core') {
1197 continue;
1198 }
1199 if ($reqinfo->status != self::REQUIREMENT_STATUS_OK) {
1200 if ($reqinfo->availability == self::REQUIREMENT_AVAILABLE) {
35f2b674 1201 $remoteinfo = $this->get_remote_plugin_info($reqname, $reqinfo->reqver, false);
5a92cd0b
DM
1202
1203 if (empty($dependencies[$reqname])) {
1204 $dependencies[$reqname] = $remoteinfo;
1205 } else {
35f2b674
DM
1206 // If resolving requirements has led to two different versions of the same
1207 // remote plugin, pick the higher version. This can happen in cases like one
1208 // plugin requiring ANY_VERSION and another plugin requiring specific higher
1209 // version with lower maturity of a remote plugin.
5a92cd0b
DM
1210 if ($remoteinfo->version->version > $dependencies[$reqname]->version->version) {
1211 $dependencies[$reqname] = $remoteinfo;
1212 }
1213 }
1214
1215 } else {
1216 if (!isset($dependencies[$reqname])) {
1217 // Unable to find a plugin fulfilling the requirements.
1218 $dependencies[$reqname] = false;
1219 }
1220 }
1221 }
1222 }
1223 }
1224 }
1225
0e442ee7
DM
1226 if ($availableonly) {
1227 foreach ($dependencies as $component => $info) {
1228 if (empty($info) or empty($info->version)) {
1229 unset($dependencies[$component]);
1230 }
1231 }
1232 }
1233
5a92cd0b
DM
1234 return $dependencies;
1235 }
1236
e87214bd
PS
1237 /**
1238 * Is it possible to uninstall the given plugin?
1239 *
1240 * False is returned if the plugininfo subclass declares the uninstall should
1241 * not be allowed via {@link \core\plugininfo\base::is_uninstall_allowed()} or if the
1242 * core vetoes it (e.g. becase the plugin or some of its subplugins is required
1243 * by some other installed plugin).
1244 *
1245 * @param string $component full frankenstyle name, e.g. mod_foobar
1246 * @return bool
1247 */
1248 public function can_uninstall_plugin($component) {
1249
1250 $pluginfo = $this->get_plugin_info($component);
1251
1252 if (is_null($pluginfo)) {
1253 return false;
1254 }
1255
1256 if (!$this->common_uninstall_check($pluginfo)) {
1257 return false;
1258 }
1259
1260 // Verify only if something else requires the subplugins, do not verify their common_uninstall_check()!
1261 $subplugins = $this->get_subplugins_of_plugin($pluginfo->component);
1262 foreach ($subplugins as $subpluginfo) {
1263 // Check if there are some other plugins requiring this subplugin
1264 // (but the parent and siblings).
1265 foreach ($this->other_plugins_that_require($subpluginfo->component) as $requiresme) {
1266 $ismyparent = ($pluginfo->component === $requiresme);
1267 $ismysibling = in_array($requiresme, array_keys($subplugins));
1268 if (!$ismyparent and !$ismysibling) {
1269 return false;
1270 }
1271 }
1272 }
1273
1274 // Check if there are some other plugins requiring this plugin
1275 // (but its subplugins).
1276 foreach ($this->other_plugins_that_require($pluginfo->component) as $requiresme) {
1277 $ismysubplugin = in_array($requiresme, array_keys($subplugins));
1278 if (!$ismysubplugin) {
1279 return false;
1280 }
1281 }
1282
1283 return true;
1284 }
1285
c948b813 1286 /**
2d00be61
DM
1287 * Perform the installation of plugins.
1288 *
1289 * If used for installation of remote plugins from the Moodle Plugins
1290 * directory, the $plugins must be list of {@link \core\update\remote_info}
1291 * object that represent installable remote plugins. The caller can use
1292 * {@link self::filter_installable()} to prepare the list.
c948b813 1293 *
2d00be61
DM
1294 * If used for installation of plugins from locally available ZIP files,
1295 * the $plugins should be list of objects with properties ->component and
1296 * ->zipfilepath.
c948b813 1297 *
2d00be61
DM
1298 * The method uses {@link mtrace()} to produce direct output and can be
1299 * used in both web and cli interfaces.
1300 *
1301 * @param array $plugins list of plugins
c948b813
DM
1302 * @param bool $confirmed should the files be really deployed into the dirroot?
1303 * @param bool $silent perform without output
1304 * @return bool true on success
1305 */
2d00be61 1306 public function install_plugins(array $plugins, $confirmed, $silent) {
c948b813
DM
1307 global $CFG, $OUTPUT;
1308
b0fc7898
DM
1309 if (!empty($CFG->disableupdateautodeploy)) {
1310 return false;
1311 }
1312
c948b813
DM
1313 if (empty($plugins)) {
1314 return false;
1315 }
1316
1317 $ok = get_string('ok', 'core');
1318
1319 // Let admins know they can expect more verbose output.
1320 $silent or $this->mtrace(get_string('packagesdebug', 'core_plugin'), PHP_EOL, DEBUG_NORMAL);
1321
1322 // Download all ZIP packages if we do not have them yet.
c948b813
DM
1323 $zips = array();
1324 foreach ($plugins as $plugin) {
2d00be61
DM
1325 if ($plugin instanceof \core\update\remote_info) {
1326 $zips[$plugin->component] = $this->get_remote_plugin_zip($plugin->version->downloadurl,
1327 $plugin->version->downloadmd5);
1328 $silent or $this->mtrace(get_string('packagesdownloading', 'core_plugin', $plugin->component), ' ... ');
1329 $silent or $this->mtrace(PHP_EOL.' <- '.$plugin->version->downloadurl, '', DEBUG_DEVELOPER);
1330 $silent or $this->mtrace(PHP_EOL.' -> '.$zips[$plugin->component], ' ... ', DEBUG_DEVELOPER);
1331 if (!$zips[$plugin->component]) {
1332 $silent or $this->mtrace(get_string('error'));
1333 return false;
1334 }
1335 $silent or $this->mtrace($ok);
1336 } else {
1337 if (empty($plugin->zipfilepath)) {
1338 throw new coding_exception('Unexpected data structure provided');
1339 }
1340 $zips[$plugin->component] = $plugin->zipfilepath;
1341 $silent or $this->mtrace('ZIP '.$plugin->zipfilepath, PHP_EOL, DEBUG_DEVELOPER);
c948b813
DM
1342 }
1343 }
c948b813
DM
1344
1345 // Validate all downloaded packages.
c948b813
DM
1346 foreach ($plugins as $plugin) {
1347 $zipfile = $zips[$plugin->component];
2d00be61 1348 $silent or $this->mtrace(get_string('packagesvalidating', 'core_plugin', $plugin->component), ' ... ');
c948b813
DM
1349 list($plugintype, $pluginname) = core_component::normalize_component($plugin->component);
1350 $tmp = make_request_directory();
1351 $zipcontents = $this->unzip_plugin_file($zipfile, $tmp, $pluginname);
1352 if (empty($zipcontents)) {
1353 $silent or $this->mtrace(get_string('error'));
1354 $silent or $this->mtrace('Unable to unzip '.$zipfile, PHP_EOL, DEBUG_DEVELOPER);
1355 return false;
1356 }
1357
1358 $validator = \core\update\validator::instance($tmp, $zipcontents);
1359 $validator->assert_plugin_type($plugintype);
1360 $validator->assert_moodle_version($CFG->version);
1361 // TODO Check for missing dependencies during validation.
1362 $result = $validator->execute();
1363 if (!$silent) {
1364 $result ? $this->mtrace($ok) : $this->mtrace(get_string('error'));
1365 foreach ($validator->get_messages() as $message) {
1366 if ($message->level === $validator::INFO) {
1367 // Display [OK] validation messages only if debugging mode is DEBUG_NORMAL.
1368 $level = DEBUG_NORMAL;
1369 } else if ($message->level === $validator::DEBUG) {
1370 // Display [Debug] validation messages only if debugging mode is DEBUG_ALL.
1371 $level = DEBUG_ALL;
1372 } else {
1373 // Display [Warning] and [Error] always.
1374 $level = null;
1375 }
1376 if ($message->level === $validator::WARNING and !CLI_SCRIPT) {
1377 $this->mtrace(' <strong>['.$validator->message_level_name($message->level).']</strong>', ' ', $level);
1378 } else {
1379 $this->mtrace(' ['.$validator->message_level_name($message->level).']', ' ', $level);
1380 }
1381 $this->mtrace($validator->message_code_name($message->msgcode), ' ', $level);
1382 $info = $validator->message_code_info($message->msgcode, $message->addinfo);
1383 if ($info) {
1384 $this->mtrace('['.s($info).']', ' ', $level);
1385 } else if (is_string($message->addinfo)) {
1386 $this->mtrace('['.s($message->addinfo, true).']', ' ', $level);
1387 } else {
1388 $this->mtrace('['.s(json_encode($message->addinfo, true)).']', ' ', $level);
1389 }
1390 if ($icon = $validator->message_help_icon($message->msgcode)) {
1391 if (CLI_SCRIPT) {
1392 $this->mtrace(PHP_EOL.' ^^^ '.get_string('help').': '.
1393 get_string($icon->identifier.'_help', $icon->component), '', $level);
1394 } else {
1395 $this->mtrace($OUTPUT->render($icon), ' ', $level);
1396 }
1397 }
1398 $this->mtrace(PHP_EOL, '', $level);
1399 }
1400 }
1401 if (!$result) {
1402 $silent or $this->mtrace(get_string('packagesvalidatingfailed', 'core_plugin'));
1403 return false;
1404 }
1405 }
1406 $silent or $this->mtrace(PHP_EOL.get_string('packagesvalidatingok', 'core_plugin'));
1407
1408 if (!$confirmed) {
1409 return true;
1410 }
1411
1412 // Extract all ZIP packs do the dirroot.
c948b813 1413 foreach ($plugins as $plugin) {
2d00be61 1414 $silent or $this->mtrace(get_string('packagesextracting', 'core_plugin', $plugin->component), ' ... ');
c948b813
DM
1415 $zipfile = $zips[$plugin->component];
1416 list($plugintype, $pluginname) = core_component::normalize_component($plugin->component);
1417 $target = $this->get_plugintype_root($plugintype);
1418 if (file_exists($target.'/'.$pluginname)) {
a2e1e0d0 1419 $this->remove_plugin_folder($this->get_plugin_info($plugin->component));
c948b813
DM
1420 }
1421 if (!$this->unzip_plugin_file($zipfile, $target, $pluginname)) {
1422 $silent or $this->mtrace(get_string('error'));
1423 $silent or $this->mtrace('Unable to unzip '.$zipfile, PHP_EOL, DEBUG_DEVELOPER);
da54cf11
DM
1424 if (function_exists('opcache_reset')) {
1425 opcache_reset();
1426 }
c948b813
DM
1427 return false;
1428 }
1429 $silent or $this->mtrace($ok);
1430 }
da54cf11
DM
1431 if (function_exists('opcache_reset')) {
1432 opcache_reset();
1433 }
c948b813
DM
1434
1435 return true;
1436 }
1437
1438 /**
1439 * Outputs the given message via {@link mtrace()}.
1440 *
1441 * If $debug is provided, then the message is displayed only at the given
1442 * debugging level (e.g. DEBUG_DEVELOPER to display the message only if the
1443 * site has developer debugging level selected).
1444 *
1445 * @param string $msg message
1446 * @param string $eol end of line
1447 * @param null|int $debug null to display always, int only on given debug level
1448 */
1449 protected function mtrace($msg, $eol=PHP_EOL, $debug=null) {
1450 global $CFG;
1451
1452 if ($debug !== null and !debugging(null, $debug)) {
1453 return;
1454 }
1455
1456 mtrace($msg, $eol);
1457 }
1458
e87214bd
PS
1459 /**
1460 * Returns uninstall URL if exists.
1461 *
1462 * @param string $component
1463 * @param string $return either 'overview' or 'manage'
1464 * @return moodle_url uninstall URL, null if uninstall not supported
1465 */
1466 public function get_uninstall_url($component, $return = 'overview') {
1467 if (!$this->can_uninstall_plugin($component)) {
1468 return null;
1469 }
1470
1471 $pluginfo = $this->get_plugin_info($component);
1472
1473 if (is_null($pluginfo)) {
1474 return null;
1475 }
1476
1477 if (method_exists($pluginfo, 'get_uninstall_url')) {
1478 debugging('plugininfo method get_uninstall_url() is deprecated, all plugins should be uninstalled via standard URL only.');
1479 return $pluginfo->get_uninstall_url($return);
1480 }
1481
1482 return $pluginfo->get_default_uninstall_url($return);
1483 }
1484
1485 /**
1486 * Uninstall the given plugin.
1487 *
1488 * Automatically cleans-up all remaining configuration data, log records, events,
1489 * files from the file pool etc.
1490 *
1491 * In the future, the functionality of {@link uninstall_plugin()} function may be moved
1492 * into this method and all the code should be refactored to use it. At the moment, we
1493 * mimic this future behaviour by wrapping that function call.
1494 *
1495 * @param string $component
1496 * @param progress_trace $progress traces the process
1497 * @return bool true on success, false on errors/problems
1498 */
1499 public function uninstall_plugin($component, progress_trace $progress) {
1500
1501 $pluginfo = $this->get_plugin_info($component);
1502
1503 if (is_null($pluginfo)) {
1504 return false;
1505 }
1506
1507 // Give the pluginfo class a chance to execute some steps.
1508 $result = $pluginfo->uninstall($progress);
1509 if (!$result) {
1510 return false;
1511 }
1512
1513 // Call the legacy core function to uninstall the plugin.
1514 ob_start();
1515 uninstall_plugin($pluginfo->type, $pluginfo->name);
1516 $progress->output(ob_get_clean());
1517
1518 return true;
1519 }
1520
1521 /**
1522 * Checks if there are some plugins with a known available update
1523 *
1524 * @return bool true if there is at least one available update
1525 */
1526 public function some_plugins_updatable() {
1527 foreach ($this->get_plugins() as $type => $plugins) {
1528 foreach ($plugins as $plugin) {
1529 if ($plugin->available_updates()) {
1530 return true;
1531 }
1532 }
1533 }
1534
1535 return false;
1536 }
1537
c44bbe35
DM
1538 /**
1539 * Returns list of available updates for the given component.
1540 *
1541 * This method should be considered as internal API and is supposed to be
1542 * called by {@link \core\plugininfo\base::available_updates()} only
1543 * to lazy load the data once they are first requested.
1544 *
1545 * @param string $component frankenstyle name of the plugin
1546 * @return null|array array of \core\update\info objects or null
1547 */
1548 public function load_available_updates_for_plugin($component) {
1549 global $CFG;
1550
1551 $provider = \core\update\checker::instance();
1552
1553 if (!$provider->enabled() or during_initial_install()) {
1554 return null;
1555 }
1556
1557 if (isset($CFG->updateminmaturity)) {
1558 $minmaturity = $CFG->updateminmaturity;
1559 } else {
1560 // This can happen during the very first upgrade to 2.3.
1561 $minmaturity = MATURITY_STABLE;
1562 }
1563
1564 return $provider->get_update_info($component, array('minmaturity' => $minmaturity));
1565 }
1566
cc5bc55e
DM
1567 /**
1568 * Returns a list of all available updates to be installed.
1569 *
1570 * This is used when "update all plugins" action is performed at the
1571 * administration UI screen.
1572 *
1573 * Returns array of remote info objects indexed by the plugin
1574 * component. If there are multiple updates available (typically a mix of
1575 * stable and non-stable ones), we pick the most mature most recent one.
1576 *
1577 * Plugins without explicit maturity are considered more mature than
1578 * release candidates but less mature than explicit stable (this should be
1579 * pretty rare case).
1580 *
4f18a4e6 1581 * @return array (string)component => (\core\update\remote_info)remoteinfo
cc5bc55e
DM
1582 */
1583 public function available_updates() {
1584
1585 $updates = array();
1586
1587 foreach ($this->get_plugins() as $type => $plugins) {
1588 foreach ($plugins as $plugin) {
1589 $availableupdates = $plugin->available_updates();
1590 if (empty($availableupdates)) {
1591 continue;
1592 }
1593 foreach ($availableupdates as $update) {
1594 if (empty($updates[$plugin->component])) {
1595 $updates[$plugin->component] = $update;
1596 continue;
1597 }
1598 $maturitycurrent = $updates[$plugin->component]->maturity;
1599 if (empty($maturitycurrent)) {
1600 $maturitycurrent = MATURITY_STABLE - 25;
1601 }
1602 $maturityremote = $update->maturity;
1603 if (empty($maturityremote)) {
1604 $maturityremote = MATURITY_STABLE - 25;
1605 }
1606 if ($maturityremote < $maturitycurrent) {
1607 continue;
1608 }
1609 if ($maturityremote > $maturitycurrent) {
1610 $updates[$plugin->component] = $update;
1611 continue;
1612 }
1613 if ($update->version > $updates[$plugin->component]->version) {
1614 $updates[$plugin->component] = $update;
1615 continue;
1616 }
1617 }
1618 }
1619 }
1620
1621 foreach ($updates as $component => $update) {
1622 $remoteinfo = $this->get_remote_plugin_info($component, $update->version, true);
1623 if (empty($remoteinfo) or empty($remoteinfo->version)) {
1624 unset($updates[$component]);
1625 } else {
1626 $updates[$component] = $remoteinfo;
1627 }
1628 }
1629
1630 return $updates;
1631 }
1632
e87214bd
PS
1633 /**
1634 * Check to see if the given plugin folder can be removed by the web server process.
1635 *
1636 * @param string $component full frankenstyle component
1637 * @return bool
1638 */
1639 public function is_plugin_folder_removable($component) {
1640
1641 $pluginfo = $this->get_plugin_info($component);
1642
1643 if (is_null($pluginfo)) {
1644 return false;
1645 }
1646
1647 // To be able to remove the plugin folder, its parent must be writable, too.
1648 if (!is_writable(dirname($pluginfo->rootdir))) {
1649 return false;
1650 }
1651
1652 // Check that the folder and all its content is writable (thence removable).
1653 return $this->is_directory_removable($pluginfo->rootdir);
1654 }
1655
0e442ee7
DM
1656 /**
1657 * Is it possible to create a new plugin directory for the given plugin type?
1658 *
1659 * @throws coding_exception for invalid plugin types or non-existing plugin type locations
1660 * @param string $plugintype
1661 * @return boolean
1662 */
1663 public function is_plugintype_writable($plugintype) {
1664
1665 $plugintypepath = $this->get_plugintype_root($plugintype);
1666
1667 if (is_null($plugintypepath)) {
1668 throw new coding_exception('Unknown plugin type: '.$plugintype);
1669 }
1670
1671 if ($plugintypepath === false) {
1672 throw new coding_exception('Plugin type location does not exist: '.$plugintype);
1673 }
1674
1675 return is_writable($plugintypepath);
1676 }
1677
1678 /**
1679 * Returns the full path of the root of the given plugin type
1680 *
1681 * Null is returned if the plugin type is not known. False is returned if
1682 * the plugin type root is expected but not found. Otherwise, string is
1683 * returned.
1684 *
1685 * @param string $plugintype
1686 * @return string|bool|null
1687 */
1688 public function get_plugintype_root($plugintype) {
1689
1690 $plugintypepath = null;
1691 foreach (core_component::get_plugin_types() as $type => $fullpath) {
1692 if ($type === $plugintype) {
1693 $plugintypepath = $fullpath;
1694 break;
1695 }
1696 }
1697 if (is_null($plugintypepath)) {
1698 return null;
1699 }
1700 if (!is_dir($plugintypepath)) {
1701 return false;
1702 }
1703
1704 return $plugintypepath;
1705 }
1706
e87214bd
PS
1707 /**
1708 * Defines a list of all plugins that were originally shipped in the standard Moodle distribution,
1709 * but are not anymore and are deleted during upgrades.
1710 *
1711 * The main purpose of this list is to hide missing plugins during upgrade.
1712 *
1713 * @param string $type plugin type
1714 * @param string $name plugin name
1715 * @return bool
1716 */
1717 public static function is_deleted_standard_plugin($type, $name) {
1718 // Do not include plugins that were removed during upgrades to versions that are
1719 // not supported as source versions for upgrade any more. For example, at MOODLE_23_STABLE
1720 // branch, listed should be no plugins that were removed at 1.9.x - 2.1.x versions as
1721 // Moodle 2.3 supports upgrades from 2.2.x only.
1722 $plugins = array(
a75fa4c0 1723 'qformat' => array('blackboard', 'learnwise'),
abc25c01 1724 'auth' => array('radius', 'fc', 'nntp', 'pam', 'pop3', 'imap'),
7ce941a4 1725 'block' => array('course_overview', 'messages', 'community', 'participants'),
66619627 1726 'cachestore' => array('memcache'),
e87214bd 1727 'enrol' => array('authorize'),
6414edf1 1728 'quizaccess' => array('safebrowser'),
6a4c2146 1729 'report' => array('search'),
f1f94da4 1730 'repository' => array('alfresco'),
1170df12 1731 'tinymce' => array('dragmath'),
3412b12c 1732 'tool' => array('bloglevelupgrade', 'qeupgradehelper', 'timezoneimport', 'assignmentupgrade'),
e00f1c66
MM
1733 'theme' => array('bootstrapbase', 'clean', 'more', 'afterburner', 'anomaly', 'arialist', 'base',
1734 'binarius', 'boxxie', 'brick', 'canvas', 'formal_white', 'formfactor', 'fusion', 'leatherbound',
1735 'magazine', 'mymobile', 'nimble', 'nonzero', 'overlay', 'serenity', 'sky_high', 'splash',
1736 'standard', 'standardold'),
13ae7db2 1737 'webservice' => array('amf'),
e87214bd
PS
1738 );
1739
1740 if (!isset($plugins[$type])) {
1741 return false;
1742 }
1743 return in_array($name, $plugins[$type]);
1744 }
1745
1746 /**
1747 * Defines a white list of all plugins shipped in the standard Moodle distribution
1748 *
1749 * @param string $type
1750 * @return false|array array of standard plugins or false if the type is unknown
1751 */
1752 public static function standard_plugins_list($type) {
1753
1754 $standard_plugins = array(
1755
d2de503c
MG
1756 'antivirus' => array(
1757 'clamav'
1758 ),
1759
adca7326 1760 'atto' => array(
205c6db5
MG
1761 'accessibilitychecker', 'accessibilityhelper', 'align',
1762 'backcolor', 'bold', 'charmap', 'clear', 'collapse', 'emoticon',
1763 'equation', 'fontcolor', 'html', 'image', 'indent', 'italic',
1764 'link', 'managefiles', 'media', 'noautolink', 'orderedlist',
2ce42911 1765 'recordrtc', 'rtl', 'strike', 'subscript', 'superscript', 'table',
afaf64b3 1766 'title', 'underline', 'undo', 'unorderedlist', 'h5p', 'emojipicker',
adca7326
DW
1767 ),
1768
e87214bd
PS
1769 'assignment' => array(
1770 'offline', 'online', 'upload', 'uploadsingle'
1771 ),
1772
1773 'assignsubmission' => array(
1774 'comments', 'file', 'onlinetext'
1775 ),
1776
1777 'assignfeedback' => array(
1778 'comments', 'file', 'offline', 'editpdf'
1779 ),
1780
e87214bd 1781 'auth' => array(
abc25c01 1782 'cas', 'db', 'email', 'ldap', 'lti', 'manual', 'mnet',
36eaa963 1783 'nologin', 'none', 'oauth2', 'shibboleth', 'webservice'
e87214bd
PS
1784 ),
1785
d3db4b03 1786 'availability' => array(
1787 'completion', 'date', 'grade', 'group', 'grouping', 'profile'
1788 ),
1789
e87214bd 1790 'block' => array(
d6383f6a
SB
1791 'activity_modules', 'activity_results', 'admin_bookmarks', 'badges',
1792 'blog_menu', 'blog_recent', 'blog_tags', 'calendar_month',
1d6f041a 1793 'calendar_upcoming', 'comments',
4c5cde31
MN
1794 'completionstatus', 'course_list', 'course_summary',
1795 'feedback', 'globalsearch', 'glossary_random', 'html',
f3a3e234 1796 'login', 'lp', 'mentees', 'mnet_hosts', 'myoverview', 'myprofile',
7ce941a4 1797 'navigation', 'news_items', 'online_users',
78e07cbc 1798 'private_files', 'quiz_results', 'recent_activity', 'recentlyaccesseditems',
41f61293 1799 'recentlyaccessedcourses', 'rss_client', 'search_forums', 'section_links',
e87214bd 1800 'selfcompletion', 'settings', 'site_main_menu',
115ce6ba 1801 'social_activities', 'starredcourses', 'tag_flickr', 'tag_youtube', 'tags', 'timeline'
e87214bd
PS
1802 ),
1803
1804 'booktool' => array(
1805 'exportimscp', 'importhtml', 'print'
1806 ),
1807
1808 'cachelock' => array(
1809 'file'
1810 ),
1811
1812 'cachestore' => array(
66619627 1813 'file', 'memcached', 'mongodb', 'session', 'static', 'apcu', 'redis'
e87214bd
PS
1814 ),
1815
1816 'calendartype' => array(
1817 'gregorian'
1818 ),
1819
33b8ca26
AA
1820 'contenttype' => array(
1821 'h5p'
1822 ),
1823
8a8221f5
SV
1824 'customfield' => array(
1825 'checkbox', 'date', 'select', 'text', 'textarea'
1826 ),
1827
e87214bd
PS
1828 'coursereport' => array(
1829 // Deprecated!
1830 ),
1831
1832 'datafield' => array(
1833 'checkbox', 'date', 'file', 'latlong', 'menu', 'multimenu',
1834 'number', 'picture', 'radiobutton', 'text', 'textarea', 'url'
1835 ),
1836
bff1edbe 1837 'dataformat' => array(
35514f57 1838 'html', 'csv', 'json', 'excel', 'ods', 'pdf',
bff1edbe
BH
1839 ),
1840
e87214bd
PS
1841 'datapreset' => array(
1842 'imagegallery'
1843 ),
1844
34df779a 1845 'fileconverter' => array(
9fe33633 1846 'unoconv', 'googledrive'
34df779a
AN
1847 ),
1848
e87214bd 1849 'editor' => array(
205c6db5 1850 'atto', 'textarea', 'tinymce'
e87214bd
PS
1851 ),
1852
1853 'enrol' => array(
1854 'category', 'cohort', 'database', 'flatfile',
01bb32a2 1855 'guest', 'imsenterprise', 'ldap', 'lti', 'manual', 'meta', 'mnet',
e87214bd
PS
1856 'paypal', 'self'
1857 ),
1858
1859 'filter' => array(
1860 'activitynames', 'algebra', 'censor', 'emailprotect',
4019ae73 1861 'emoticon', 'displayh5p', 'mathjaxloader', 'mediaplugin', 'multilang', 'tex', 'tidy',
e87214bd
PS
1862 'urltolink', 'data', 'glossary'
1863 ),
1864
1865 'format' => array(
1866 'singleactivity', 'social', 'topics', 'weeks'
1867 ),
1868
afaf64b3
PH
1869 'forumreport' => array(
1870 'summary',
1871 ),
1872
e87214bd
PS
1873 'gradeexport' => array(
1874 'ods', 'txt', 'xls', 'xml'
1875 ),
1876
1877 'gradeimport' => array(
aa60bda9 1878 'csv', 'direct', 'xml'
e87214bd
PS
1879 ),
1880
1881 'gradereport' => array(
8ec7b088 1882 'grader', 'history', 'outcomes', 'overview', 'user', 'singleview'
e87214bd
PS
1883 ),
1884
1885 'gradingform' => array(
1886 'rubric', 'guide'
1887 ),
1888
08fda3e0
SA
1889 'h5plib' => array(
1890 'v124'
1891 ),
1892
e87214bd
PS
1893 'local' => array(
1894 ),
1895
7eaca5a8
1896 'logstore' => array(
1897 'database', 'legacy', 'standard',
1898 ),
1899
e3f69b58 1900 'ltiservice' => array(
27cbb596 1901 'gradebookservices', 'memberships', 'profile', 'toolproxy', 'toolsettings', 'basicoutcomes'
e3f69b58 1902 ),
1903
40fcb365
DM
1904 'mlbackend' => array(
1905 'php', 'python'
1906 ),
1907
fab11235
MG
1908 'media' => array(
1909 'html5audio', 'html5video', 'swf', 'videojs', 'vimeo', 'youtube'
1910 ),
1911
e87214bd 1912 'message' => array(
324facf4 1913 'airnotifier', 'email', 'jabber', 'popup'
e87214bd
PS
1914 ),
1915
1916 'mnetservice' => array(
1917 'enrol'
1918 ),
1919
1920 'mod' => array(
1921 'assign', 'assignment', 'book', 'chat', 'choice', 'data', 'feedback', 'folder',
252e1123 1922 'forum', 'glossary', 'h5pactivity', 'imscp', 'label', 'lesson', 'lti', 'page',
e87214bd
PS
1923 'quiz', 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop'
1924 ),
1925
83171347
SR
1926 'pg' => [
1927 ],
1928
e87214bd
PS
1929 'plagiarism' => array(
1930 ),
1931
1932 'portfolio' => array(
1933 'boxnet', 'download', 'flickr', 'googledocs', 'mahara', 'picasa'
1934 ),
1935
1936 'profilefield' => array(
1937 'checkbox', 'datetime', 'menu', 'text', 'textarea'
1938 ),
1939
1940 'qbehaviour' => array(
1941 'adaptive', 'adaptivenopenalty', 'deferredcbm',
1942 'deferredfeedback', 'immediatecbm', 'immediatefeedback',
1943 'informationitem', 'interactive', 'interactivecountback',
1944 'manualgraded', 'missing'
1945 ),
1946
1947 'qformat' => array(
1948 'aiken', 'blackboard_six', 'examview', 'gift',
a75fa4c0 1949 'missingword', 'multianswer', 'webct',
e87214bd
PS
1950 'xhtml', 'xml'
1951 ),
1952
1953 'qtype' => array(
1954 'calculated', 'calculatedmulti', 'calculatedsimple',
6e28e150
TH
1955 'ddimageortext', 'ddmarker', 'ddwtos', 'description',
1956 'essay', 'gapselect', 'match', 'missingtype', 'multianswer',
e87214bd
PS
1957 'multichoice', 'numerical', 'random', 'randomsamatch',
1958 'shortanswer', 'truefalse'
1959 ),
1960
1961 'quiz' => array(
1962 'grading', 'overview', 'responses', 'statistics'
1963 ),
1964
1965 'quizaccess' => array(
5635ead8 1966 'delaybetweenattempts', 'ipaddress', 'numattempts', 'offlineattempts', 'openclosedate',
78adf6f6 1967 'password', 'seb', 'securewindow', 'timelimit'
e87214bd
PS
1968 ),
1969
1970 'report' => array(
d8370908 1971 'backups', 'competency', 'completion', 'configlog', 'courseoverview', 'eventlist',
8e0e99e4
PB
1972 'infectedfiles', 'insights', 'log', 'loglive', 'outline', 'participation', 'progress',
1973 'questioninstances', 'security', 'stats', 'status', 'performance', 'usersessions'
e87214bd
PS
1974 ),
1975
1976 'repository' => array(
7cec78f7 1977 'areafiles', 'boxnet', 'contentbank', 'coursefiles', 'dropbox', 'equella', 'filesystem',
f6df2a88 1978 'flickr', 'flickr_public', 'googledocs', 'local', 'merlot', 'nextcloud',
d37945c3 1979 'onedrive', 'picasa', 'recent', 'skydrive', 's3', 'upload', 'url', 'user', 'webdav',
e87214bd
PS
1980 'wikimedia', 'youtube'
1981 ),
1982
db48207e 1983 'search' => array(
2ee2f530 1984 'simpledb', 'solr'
db48207e
DM
1985 ),
1986
e87214bd
PS
1987 'scormreport' => array(
1988 'basic',
1989 'interactions',
1990 'graphs',
1991 'objectives'
1992 ),
1993
1994 'tinymce' => array(
1170df12 1995 'ctrlhelp', 'managefiles', 'moodleemoticon', 'moodleimage',
e87214bd
PS
1996 'moodlemedia', 'moodlenolink', 'pdw', 'spellchecker', 'wrap'
1997 ),
1998
1999 'theme' => array(
e00f1c66 2000 'boost', 'classic'
e87214bd
PS
2001 ),
2002
2003 'tool' => array(
3412b12c 2004 'analytics', 'availabilityconditions', 'behat', 'capability', 'cohortroles', 'customlang',
4399e475
TD
2005 'dataprivacy', 'dbtransfer', 'filetypes', 'generator', 'health', 'httpsreplace', 'innodb',
2006 'installaddon', 'langimport', 'licensemanager', 'log', 'lp', 'lpimportcsv', 'lpmigrate', 'messageinbound',
fb6b7e8a 2007 'mobile', 'moodlenet', 'multilangupgrade', 'monitor', 'oauth2', 'phpunit', 'policy', 'profiling', 'recyclebin',
4399e475
TD
2008 'replace', 'spamcleaner', 'task', 'templatelibrary', 'uploadcourse', 'uploaduser', 'unsuproles',
2009 'usertours', 'xmldb'
e87214bd
PS
2010 ),
2011
2012 'webservice' => array(
13ae7db2 2013 'rest', 'soap', 'xmlrpc'
e87214bd
PS
2014 ),
2015
2016 'workshopallocation' => array(
2017 'manual', 'random', 'scheduled'
2018 ),
2019
2020 'workshopeval' => array(
2021 'best'
2022 ),
2023
2024 'workshopform' => array(
2025 'accumulative', 'comments', 'numerrors', 'rubric'
2026 )
2027 );
2028
2029 if (isset($standard_plugins[$type])) {
2030 return $standard_plugins[$type];
2031 } else {
2032 return false;
2033 }
2034 }
2035
a2e1e0d0
DM
2036 /**
2037 * Remove the current plugin code from the dirroot.
2038 *
2039 * If removing the currently installed version (which happens during
2040 * updates), we archive the code so that the upgrade can be cancelled.
2041 *
2042 * To prevent accidental data-loss, we also archive the existing plugin
2043 * code if cancelling installation of it, so that the developer does not
2044 * loose the only version of their work-in-progress.
2045 *
2046 * @param \core\plugininfo\base $plugin
2047 */
2048 public function remove_plugin_folder(\core\plugininfo\base $plugin) {
2049
2050 if (!$this->is_plugin_folder_removable($plugin->component)) {
2051 throw new moodle_exception('err_removing_unremovable_folder', 'core_plugin', '',
0355e76f 2052 array('plugin' => $plugin->component, 'rootdir' => $plugin->rootdir),
a2e1e0d0
DM
2053 'plugin root folder is not removable as expected');
2054 }
2055
2056 if ($plugin->get_status() === self::PLUGIN_STATUS_UPTODATE or $plugin->get_status() === self::PLUGIN_STATUS_NEW) {
2057 $this->archive_plugin_version($plugin);
2058 }
2059
2060 remove_dir($plugin->rootdir);
2061 clearstatcache();
2062 if (function_exists('opcache_reset')) {
2063 opcache_reset();
2064 }
2065 }
2066
4d7528f9
DM
2067 /**
2068 * Can the installation of the new plugin be cancelled?
2069 *
c20e9ae8
DM
2070 * Subplugins can be cancelled only via their parent plugin, not separately
2071 * (they are considered as implicit requirements if distributed together
2072 * with the main package).
2073 *
4d7528f9
DM
2074 * @param \core\plugininfo\base $plugin
2075 * @return bool
2076 */
2077 public function can_cancel_plugin_installation(\core\plugininfo\base $plugin) {
b0fc7898
DM
2078 global $CFG;
2079
2080 if (!empty($CFG->disableupdateautodeploy)) {
2081 return false;
2082 }
4d7528f9 2083
c20e9ae8
DM
2084 if (empty($plugin) or $plugin->is_standard() or $plugin->is_subplugin()
2085 or !$this->is_plugin_folder_removable($plugin->component)) {
4d7528f9
DM
2086 return false;
2087 }
2088
2089 if ($plugin->get_status() === self::PLUGIN_STATUS_NEW) {
2090 return true;
2091 }
2092
2093 return false;
2094 }
2095
c20e9ae8
DM
2096 /**
2097 * Can the upgrade of the existing plugin be cancelled?
2098 *
2099 * Subplugins can be cancelled only via their parent plugin, not separately
2100 * (they are considered as implicit requirements if distributed together
2101 * with the main package).
2102 *
2103 * @param \core\plugininfo\base $plugin
2104 * @return bool
2105 */
2106 public function can_cancel_plugin_upgrade(\core\plugininfo\base $plugin) {
b0fc7898
DM
2107 global $CFG;
2108
2109 if (!empty($CFG->disableupdateautodeploy)) {
2110 // Cancelling the plugin upgrade is actually installation of the
2111 // previously archived version.
2112 return false;
2113 }
c20e9ae8
DM
2114
2115 if (empty($plugin) or $plugin->is_standard() or $plugin->is_subplugin()
2116 or !$this->is_plugin_folder_removable($plugin->component)) {
2117 return false;
2118 }
2119
2120 if ($plugin->get_status() === self::PLUGIN_STATUS_UPGRADE) {
2121 if ($this->get_code_manager()->get_archived_plugin_version($plugin->component, $plugin->versiondb)) {
2122 return true;
2123 }
2124 }
2125
2126 return false;
2127 }
2128
2f29cf6e
DM
2129 /**
2130 * Removes the plugin code directory if it is not installed yet.
2131 *
2132 * This is intended for the plugins check screen to give the admin a chance
2133 * to cancel the installation of just unzipped plugin before the database
2134 * upgrade happens.
2135 *
2136 * @param string $component
2f29cf6e
DM
2137 */
2138 public function cancel_plugin_installation($component) {
b0fc7898
DM
2139 global $CFG;
2140
2141 if (!empty($CFG->disableupdateautodeploy)) {
2142 return false;
2143 }
2f29cf6e
DM
2144
2145 $plugin = $this->get_plugin_info($component);
2146
4d7528f9 2147 if ($this->can_cancel_plugin_installation($plugin)) {
a2e1e0d0 2148 $this->remove_plugin_folder($plugin);
2f29cf6e
DM
2149 }
2150
4d7528f9 2151 return false;
2f29cf6e
DM
2152 }
2153
2154 /**
c20e9ae8
DM
2155 * Returns plugins, the installation of which can be cancelled.
2156 *
2157 * @return array [(string)component] => (\core\plugininfo\base)plugin
2f29cf6e 2158 */
c20e9ae8 2159 public function list_cancellable_installations() {
b0fc7898
DM
2160 global $CFG;
2161
2162 if (!empty($CFG->disableupdateautodeploy)) {
2163 return array();
2164 }
2f29cf6e 2165
c20e9ae8 2166 $cancellable = array();
2f29cf6e
DM
2167 foreach ($this->get_plugins() as $type => $plugins) {
2168 foreach ($plugins as $plugin) {
4d7528f9 2169 if ($this->can_cancel_plugin_installation($plugin)) {
c20e9ae8 2170 $cancellable[$plugin->component] = $plugin;
2f29cf6e
DM
2171 }
2172 }
2173 }
c20e9ae8
DM
2174
2175 return $cancellable;
2f29cf6e
DM
2176 }
2177
4d7528f9
DM
2178 /**
2179 * Archive the current on-disk plugin code.
2180 *
2181 * @param \core\plugiinfo\base $plugin
2182 * @return bool
2183 */
2184 public function archive_plugin_version(\core\plugininfo\base $plugin) {
a2e1e0d0 2185 return $this->get_code_manager()->archive_plugin_version($plugin->rootdir, $plugin->component, $plugin->versiondisk);
4d7528f9
DM
2186 }
2187
c20e9ae8
DM
2188 /**
2189 * Returns list of all archives that can be installed to cancel the plugin upgrade.
2190 *
2191 * @return array [(string)component] => {(string)->component, (string)->zipfilepath}
2192 */
2193 public function list_restorable_archives() {
b0fc7898
DM
2194 global $CFG;
2195
2196 if (!empty($CFG->disableupdateautodeploy)) {
2197 return false;
2198 }
c20e9ae8
DM
2199
2200 $codeman = $this->get_code_manager();
2201 $restorable = array();
2202 foreach ($this->get_plugins() as $type => $plugins) {
2203 foreach ($plugins as $plugin) {
2204 if ($this->can_cancel_plugin_upgrade($plugin)) {
2205 $restorable[$plugin->component] = (object)array(
2206 'component' => $plugin->component,
2207 'zipfilepath' => $codeman->get_archived_plugin_version($plugin->component, $plugin->versiondb)
2208 );
2209 }
2210 }
2211 }
2212
2213 return $restorable;
2214 }
2215
e87214bd
PS
2216 /**
2217 * Reorders plugin types into a sequence to be displayed
2218 *
2219 * For technical reasons, plugin types returned by {@link core_component::get_plugin_types()} are
2220 * in a certain order that does not need to fit the expected order for the display.
2221 * Particularly, activity modules should be displayed first as they represent the
2222 * real heart of Moodle. They should be followed by other plugin types that are
2223 * used to build the courses (as that is what one expects from LMS). After that,
2224 * other supportive plugin types follow.
2225 *
2226 * @param array $types associative array
2227 * @return array same array with altered order of items
2228 */
2229 protected function reorder_plugin_types(array $types) {
2230 $fix = array('mod' => $types['mod']);
2231 foreach (core_component::get_plugin_list('mod') as $plugin => $fulldir) {
2232 if (!$subtypes = core_component::get_subplugins('mod_'.$plugin)) {
2233 continue;
2234 }
2235 foreach ($subtypes as $subtype => $ignored) {
2236 $fix[$subtype] = $types[$subtype];
2237 }
2238 }
2239
2240 $fix['mod'] = $types['mod'];
2241 $fix['block'] = $types['block'];
2242 $fix['qtype'] = $types['qtype'];
2243 $fix['qbehaviour'] = $types['qbehaviour'];
2244 $fix['qformat'] = $types['qformat'];
2245 $fix['filter'] = $types['filter'];
2246
2247 $fix['editor'] = $types['editor'];
2248 foreach (core_component::get_plugin_list('editor') as $plugin => $fulldir) {
2249 if (!$subtypes = core_component::get_subplugins('editor_'.$plugin)) {
2250 continue;
2251 }
2252 foreach ($subtypes as $subtype => $ignored) {
2253 $fix[$subtype] = $types[$subtype];
2254 }
2255 }
2256
2257 $fix['enrol'] = $types['enrol'];
2258 $fix['auth'] = $types['auth'];
2259 $fix['tool'] = $types['tool'];
2260 foreach (core_component::get_plugin_list('tool') as $plugin => $fulldir) {
2261 if (!$subtypes = core_component::get_subplugins('tool_'.$plugin)) {
2262 continue;
2263 }
2264 foreach ($subtypes as $subtype => $ignored) {
2265 $fix[$subtype] = $types[$subtype];
2266 }
2267 }
2268
2269 foreach ($types as $type => $path) {
2270 if (!isset($fix[$type])) {
2271 $fix[$type] = $path;
2272 }
2273 }
2274 return $fix;
2275 }
2276
2277 /**
2278 * Check if the given directory can be removed by the web server process.
2279 *
2280 * This recursively checks that the given directory and all its contents
2281 * it writable.
2282 *
2283 * @param string $fullpath
2284 * @return boolean
2285 */
8acee4b5 2286 public function is_directory_removable($fullpath) {
e87214bd
PS
2287
2288 if (!is_writable($fullpath)) {
2289 return false;
2290 }
2291
2292 if (is_dir($fullpath)) {
2293 $handle = opendir($fullpath);
2294 } else {
2295 return false;
2296 }
2297
2298 $result = true;
2299
2300 while ($filename = readdir($handle)) {
2301
2302 if ($filename === '.' or $filename === '..') {
2303 continue;
2304 }
2305
2306 $subfilepath = $fullpath.'/'.$filename;
2307
2308 if (is_dir($subfilepath)) {
2309 $result = $result && $this->is_directory_removable($subfilepath);
2310
2311 } else {
2312 $result = $result && is_writable($subfilepath);
2313 }
2314 }
2315
2316 closedir($handle);
2317
2318 return $result;
2319 }
2320
2321 /**
2322 * Helper method that implements common uninstall prerequisites
2323 *
2324 * @param \core\plugininfo\base $pluginfo
2325 * @return bool
2326 */
2327 protected function common_uninstall_check(\core\plugininfo\base $pluginfo) {
442eeac3
PB
2328 global $CFG;
2329 // Check if uninstall is allowed from the GUI.
2330 if (!empty($CFG->uninstallclionly) && (!CLI_SCRIPT)) {
2331 return false;
2332 }
e87214bd
PS
2333
2334 if (!$pluginfo->is_uninstall_allowed()) {
2335 // The plugin's plugininfo class declares it should not be uninstalled.
2336 return false;
2337 }
2338
361feecd 2339 if ($pluginfo->get_status() === static::PLUGIN_STATUS_NEW) {
e87214bd
PS
2340 // The plugin is not installed. It should be either installed or removed from the disk.
2341 // Relying on this temporary state may be tricky.
2342 return false;
2343 }
2344
2345 if (method_exists($pluginfo, 'get_uninstall_url') and is_null($pluginfo->get_uninstall_url())) {
2346 // Backwards compatibility.
2347 debugging('\core\plugininfo\base subclasses should use is_uninstall_allowed() instead of returning null in get_uninstall_url()',
2348 DEBUG_DEVELOPER);
2349 return false;
2350 }
2351
2352 return true;
2353 }
0e442ee7
DM
2354
2355 /**
2356 * Returns a code_manager instance to be used for the plugins code operations.
2357 *
2358 * @return \core\update\code_manager
2359 */
2360 protected function get_code_manager() {
2361
2362 if ($this->codemanager === null) {
2363 $this->codemanager = new \core\update\code_manager();
2364 }
2365
2366 return $this->codemanager;
2367 }
35f2b674
DM
2368
2369 /**
2370 * Returns a client for https://download.moodle.org/api/
2371 *
2372 * @return \core\update\api
2373 */
2374 protected function get_update_api_client() {
2375
2376 if ($this->updateapiclient === null) {
2377 $this->updateapiclient = \core\update\api::client();
2378 }
2379
2380 return $this->updateapiclient;
2381 }
e87214bd 2382}