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