MDL-49329 admin: Clean up code manager methods
[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)) {
149 throw coding_exception('core_plugin_manager->get_plugin_types() does not support relative paths.');
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
994e5662 189 // TODO: Delete this block once Moodle 2.6 or later becomes minimum required version to upgrade.
e87214bd
PS
190 if ($CFG->version < 2013092001.02) {
191 // We did not upgrade the database yet.
192 $modules = $DB->get_records('modules', array(), 'name ASC', 'id, name, version');
193 foreach ($modules as $module) {
194 $this->installedplugins['mod'][$module->name] = $module->version;
195 }
196 $blocks = $DB->get_records('block', array(), 'name ASC', 'id, name, version');
197 foreach ($blocks as $block) {
198 $this->installedplugins['block'][$block->name] = $block->version;
199 }
200 }
201
202 $versions = $DB->get_records('config_plugins', array('name'=>'version'));
203 foreach ($versions as $version) {
204 $parts = explode('_', $version->plugin, 2);
205 if (!isset($parts[1])) {
206 // Invalid component, there must be at least one "_".
207 continue;
208 }
209 // Do not verify here if plugin type and name are valid.
210 $this->installedplugins[$parts[0]][$parts[1]] = $version->value;
211 }
212
213 foreach ($this->installedplugins as $key => $value) {
214 ksort($this->installedplugins[$key]);
215 }
216
217 $cache->set('installed', $this->installedplugins);
218 }
219
220 /**
221 * Return list of installed plugins of given type.
222 * @param string $type
223 * @return array $name=>$version
224 */
225 public function get_installed_plugins($type) {
226 $this->load_installed_plugins();
227 if (isset($this->installedplugins[$type])) {
228 return $this->installedplugins[$type];
229 }
230 return array();
231 }
232
233 /**
234 * Load list of all enabled plugins,
235 * call before using $this->enabledplugins.
236 *
237 * This method is caching results from individual plugin info classes.
238 */
239 protected function load_enabled_plugins() {
240 global $CFG;
241
242 if ($this->enabledplugins) {
243 return;
244 }
245
246 if (empty($CFG->version)) {
247 $this->enabledplugins = array();
248 return;
249 }
250
251 $cache = cache::make('core', 'plugin_manager');
252 $enabled = $cache->get('enabled');
253
254 if (is_array($enabled)) {
255 $this->enabledplugins = $enabled;
256 return;
257 }
258
259 $this->enabledplugins = array();
260
261 require_once($CFG->libdir.'/adminlib.php');
262
263 $plugintypes = core_component::get_plugin_types();
264 foreach ($plugintypes as $plugintype => $fulldir) {
361feecd 265 $plugininfoclass = static::resolve_plugininfo_class($plugintype);
e87214bd
PS
266 if (class_exists($plugininfoclass)) {
267 $enabled = $plugininfoclass::get_enabled_plugins();
268 if (!is_array($enabled)) {
269 continue;
270 }
271 $this->enabledplugins[$plugintype] = $enabled;
272 }
273 }
274
275 $cache->set('enabled', $this->enabledplugins);
276 }
277
278 /**
279 * Get list of enabled plugins of given type,
280 * the result may contain missing plugins.
281 *
282 * @param string $type
283 * @return array|null list of enabled plugins of this type, null if unknown
284 */
285 public function get_enabled_plugins($type) {
286 $this->load_enabled_plugins();
287 if (isset($this->enabledplugins[$type])) {
288 return $this->enabledplugins[$type];
289 }
290 return null;
291 }
292
293 /**
294 * Load list of all present plugins - call before using $this->presentplugins.
295 */
296 protected function load_present_plugins() {
297 if ($this->presentplugins) {
298 return;
299 }
300
301 $cache = cache::make('core', 'plugin_manager');
302 $present = $cache->get('present');
303
304 if (is_array($present)) {
305 $this->presentplugins = $present;
306 return;
307 }
308
309 $this->presentplugins = array();
310
311 $plugintypes = core_component::get_plugin_types();
312 foreach ($plugintypes as $type => $typedir) {
313 $plugs = core_component::get_plugin_list($type);
314 foreach ($plugs as $plug => $fullplug) {
01889f01 315 $module = new stdClass();
e87214bd
PS
316 $plugin = new stdClass();
317 $plugin->version = null;
0b468c59 318 include($fullplug.'/version.php');
01889f01
DM
319
320 // Check if the legacy $module syntax is still used.
17d2a336 321 if (!is_object($module) or (count((array)$module) > 0)) {
01889f01
DM
322 debugging('Unsupported $module syntax detected in version.php of the '.$type.'_'.$plug.' plugin.');
323 $skipcache = true;
324 }
325
98ea6973
DM
326 // Check if the component is properly declared.
327 if (empty($plugin->component) or ($plugin->component !== $type.'_'.$plug)) {
328 debugging('Plugin '.$type.'_'.$plug.' does not declare valid $plugin->component in its version.php.');
329 $skipcache = true;
330 }
331
e87214bd
PS
332 $this->presentplugins[$type][$plug] = $plugin;
333 }
334 }
335
01889f01
DM
336 if (empty($skipcache)) {
337 $cache->set('present', $this->presentplugins);
338 }
e87214bd
PS
339 }
340
341 /**
342 * Get list of present plugins of given type.
343 *
344 * @param string $type
345 * @return array|null list of presnet plugins $name=>$diskversion, null if unknown
346 */
347 public function get_present_plugins($type) {
348 $this->load_present_plugins();
349 if (isset($this->presentplugins[$type])) {
350 return $this->presentplugins[$type];
351 }
352 return null;
353 }
354
355 /**
356 * Returns a tree of known plugins and information about them
357 *
358 * @return array 2D array. The first keys are plugin type names (e.g. qtype);
359 * the second keys are the plugin local name (e.g. multichoice); and
360 * the values are the corresponding objects extending {@link \core\plugininfo\base}
361 */
362 public function get_plugins() {
363 $this->init_pluginsinfo_property();
364
365 // Make sure all types are initialised.
366 foreach ($this->pluginsinfo as $plugintype => $list) {
367 if ($list === null) {
368 $this->get_plugins_of_type($plugintype);
369 }
370 }
371
372 return $this->pluginsinfo;
373 }
374
375 /**
376 * Returns list of known plugins of the given type.
377 *
378 * This method returns the subset of the tree returned by {@link self::get_plugins()}.
379 * If the given type is not known, empty array is returned.
380 *
381 * @param string $type plugin type, e.g. 'mod' or 'workshopallocation'
382 * @return \core\plugininfo\base[] (string)plugin name (e.g. 'workshop') => corresponding subclass of {@link \core\plugininfo\base}
383 */
384 public function get_plugins_of_type($type) {
385 global $CFG;
386
387 $this->init_pluginsinfo_property();
388
389 if (!array_key_exists($type, $this->pluginsinfo)) {
390 return array();
391 }
392
393 if (is_array($this->pluginsinfo[$type])) {
394 return $this->pluginsinfo[$type];
395 }
396
397 $types = core_component::get_plugin_types();
398
a35fce24
PS
399 if (!isset($types[$type])) {
400 // Orphaned subplugins!
361feecd 401 $plugintypeclass = static::resolve_plugininfo_class($type);
2d488c8f 402 $this->pluginsinfo[$type] = $plugintypeclass::get_plugins($type, null, $plugintypeclass, $this);
a35fce24
PS
403 return $this->pluginsinfo[$type];
404 }
405
e87214bd 406 /** @var \core\plugininfo\base $plugintypeclass */
361feecd 407 $plugintypeclass = static::resolve_plugininfo_class($type);
2d488c8f 408 $plugins = $plugintypeclass::get_plugins($type, $types[$type], $plugintypeclass, $this);
e87214bd
PS
409 $this->pluginsinfo[$type] = $plugins;
410
e87214bd
PS
411 return $this->pluginsinfo[$type];
412 }
413
414 /**
415 * Init placeholder array for plugin infos.
416 */
417 protected function init_pluginsinfo_property() {
418 if (is_array($this->pluginsinfo)) {
419 return;
420 }
421 $this->pluginsinfo = array();
422
423 $plugintypes = $this->get_plugin_types();
424
425 foreach ($plugintypes as $plugintype => $plugintyperootdir) {
426 $this->pluginsinfo[$plugintype] = null;
427 }
a35fce24
PS
428
429 // Add orphaned subplugin types.
430 $this->load_installed_plugins();
431 foreach ($this->installedplugins as $plugintype => $unused) {
432 if (!isset($plugintypes[$plugintype])) {
433 $this->pluginsinfo[$plugintype] = null;
434 }
435 }
e87214bd
PS
436 }
437
438 /**
439 * Find the plugin info class for given type.
440 *
441 * @param string $type
442 * @return string name of pluginfo class for give plugin type
443 */
444 public static function resolve_plugininfo_class($type) {
a35fce24
PS
445 $plugintypes = core_component::get_plugin_types();
446 if (!isset($plugintypes[$type])) {
447 return '\core\plugininfo\orphaned';
448 }
449
e87214bd
PS
450 $parent = core_component::get_subtype_parent($type);
451
452 if ($parent) {
453 $class = '\\'.$parent.'\plugininfo\\' . $type;
454 if (class_exists($class)) {
455 $plugintypeclass = $class;
456 } else {
457 if ($dir = core_component::get_component_directory($parent)) {
458 // BC only - use namespace instead!
459 if (file_exists("$dir/adminlib.php")) {
460 global $CFG;
461 include_once("$dir/adminlib.php");
462 }
463 if (class_exists('plugininfo_' . $type)) {
464 $plugintypeclass = 'plugininfo_' . $type;
465 debugging('Class "'.$plugintypeclass.'" is deprecated, migrate to "'.$class.'"', DEBUG_DEVELOPER);
466 } else {
467 debugging('Subplugin type "'.$type.'" should define class "'.$class.'"', DEBUG_DEVELOPER);
468 $plugintypeclass = '\core\plugininfo\general';
469 }
470 } else {
471 $plugintypeclass = '\core\plugininfo\general';
472 }
473 }
474 } else {
475 $class = '\core\plugininfo\\' . $type;
476 if (class_exists($class)) {
477 $plugintypeclass = $class;
478 } else {
479 debugging('All standard types including "'.$type.'" should have plugininfo class!', DEBUG_DEVELOPER);
480 $plugintypeclass = '\core\plugininfo\general';
481 }
482 }
483
484 if (!in_array('core\plugininfo\base', class_parents($plugintypeclass))) {
485 throw new coding_exception('Class ' . $plugintypeclass . ' must extend \core\plugininfo\base');
486 }
487
488 return $plugintypeclass;
489 }
490
491 /**
492 * Returns list of all known subplugins of the given plugin.
493 *
494 * For plugins that do not provide subplugins (i.e. there is no support for it),
495 * empty array is returned.
496 *
497 * @param string $component full component name, e.g. 'mod_workshop'
498 * @return array (string) component name (e.g. 'workshopallocation_random') => subclass of {@link \core\plugininfo\base}
499 */
500 public function get_subplugins_of_plugin($component) {
501
502 $pluginfo = $this->get_plugin_info($component);
503
504 if (is_null($pluginfo)) {
505 return array();
506 }
507
508 $subplugins = $this->get_subplugins();
509
510 if (!isset($subplugins[$pluginfo->component])) {
511 return array();
512 }
513
514 $list = array();
515
516 foreach ($subplugins[$pluginfo->component] as $subdata) {
517 foreach ($this->get_plugins_of_type($subdata->type) as $subpluginfo) {
518 $list[$subpluginfo->component] = $subpluginfo;
519 }
520 }
521
522 return $list;
523 }
524
525 /**
526 * Returns list of plugins that define their subplugins and the information
527 * about them from the db/subplugins.php file.
528 *
529 * @return array with keys like 'mod_quiz', and values the data from the
530 * corresponding db/subplugins.php file.
531 */
532 public function get_subplugins() {
533
534 if (is_array($this->subpluginsinfo)) {
535 return $this->subpluginsinfo;
536 }
537
538 $plugintypes = core_component::get_plugin_types();
539
540 $this->subpluginsinfo = array();
541 foreach (core_component::get_plugin_types_with_subplugins() as $type => $ignored) {
542 foreach (core_component::get_plugin_list($type) as $plugin => $componentdir) {
543 $component = $type.'_'.$plugin;
544 $subplugins = core_component::get_subplugins($component);
545 if (!$subplugins) {
546 continue;
547 }
548 $this->subpluginsinfo[$component] = array();
549 foreach ($subplugins as $subplugintype => $ignored) {
550 $subplugin = new stdClass();
551 $subplugin->type = $subplugintype;
552 $subplugin->typerootdir = $plugintypes[$subplugintype];
553 $this->subpluginsinfo[$component][$subplugintype] = $subplugin;
554 }
555 }
556 }
557 return $this->subpluginsinfo;
558 }
559
560 /**
561 * Returns the name of the plugin that defines the given subplugin type
562 *
563 * If the given subplugin type is not actually a subplugin, returns false.
564 *
565 * @param string $subplugintype the name of subplugin type, eg. workshopform or quiz
566 * @return false|string the name of the parent plugin, eg. mod_workshop
567 */
568 public function get_parent_of_subplugin($subplugintype) {
569 $parent = core_component::get_subtype_parent($subplugintype);
570 if (!$parent) {
571 return false;
572 }
573 return $parent;
574 }
575
576 /**
577 * Returns a localized name of a given plugin
578 *
579 * @param string $component name of the plugin, eg mod_workshop or auth_ldap
580 * @return string
581 */
582 public function plugin_name($component) {
583
584 $pluginfo = $this->get_plugin_info($component);
585
586 if (is_null($pluginfo)) {
587 throw new moodle_exception('err_unknown_plugin', 'core_plugin', '', array('plugin' => $component));
588 }
589
590 return $pluginfo->displayname;
591 }
592
593 /**
594 * Returns a localized name of a plugin typed in singular form
595 *
596 * Most plugin types define their names in core_plugin lang file. In case of subplugins,
597 * we try to ask the parent plugin for the name. In the worst case, we will return
598 * the value of the passed $type parameter.
599 *
600 * @param string $type the type of the plugin, e.g. mod or workshopform
601 * @return string
602 */
603 public function plugintype_name($type) {
604
605 if (get_string_manager()->string_exists('type_' . $type, 'core_plugin')) {
606 // For most plugin types, their names are defined in core_plugin lang file.
607 return get_string('type_' . $type, 'core_plugin');
608
609 } else if ($parent = $this->get_parent_of_subplugin($type)) {
610 // If this is a subplugin, try to ask the parent plugin for the name.
611 if (get_string_manager()->string_exists('subplugintype_' . $type, $parent)) {
612 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type, $parent);
613 } else {
614 return $this->plugin_name($parent) . ' / ' . $type;
615 }
616
617 } else {
618 return $type;
619 }
620 }
621
622 /**
623 * Returns a localized name of a plugin type in plural form
624 *
625 * Most plugin types define their names in core_plugin lang file. In case of subplugins,
626 * we try to ask the parent plugin for the name. In the worst case, we will return
627 * the value of the passed $type parameter.
628 *
629 * @param string $type the type of the plugin, e.g. mod or workshopform
630 * @return string
631 */
632 public function plugintype_name_plural($type) {
633
634 if (get_string_manager()->string_exists('type_' . $type . '_plural', 'core_plugin')) {
635 // For most plugin types, their names are defined in core_plugin lang file.
636 return get_string('type_' . $type . '_plural', 'core_plugin');
637
638 } else if ($parent = $this->get_parent_of_subplugin($type)) {
639 // If this is a subplugin, try to ask the parent plugin for the name.
640 if (get_string_manager()->string_exists('subplugintype_' . $type . '_plural', $parent)) {
641 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type . '_plural', $parent);
642 } else {
643 return $this->plugin_name($parent) . ' / ' . $type;
644 }
645
646 } else {
647 return $type;
648 }
649 }
650
651 /**
652 * Returns information about the known plugin, or null
653 *
654 * @param string $component frankenstyle component name.
655 * @return \core\plugininfo\base|null the corresponding plugin information.
656 */
657 public function get_plugin_info($component) {
658 list($type, $name) = core_component::normalize_component($component);
2384d331
PS
659 $plugins = $this->get_plugins_of_type($type);
660 if (isset($plugins[$name])) {
661 return $plugins[$name];
e87214bd
PS
662 } else {
663 return null;
664 }
665 }
666
667 /**
668 * Check to see if the current version of the plugin seems to be a checkout of an external repository.
669 *
e87214bd
PS
670 * @param string $component frankenstyle component name
671 * @return false|string
672 */
673 public function plugin_external_source($component) {
674
675 $plugininfo = $this->get_plugin_info($component);
676
677 if (is_null($plugininfo)) {
678 return false;
679 }
680
681 $pluginroot = $plugininfo->rootdir;
682
683 if (is_dir($pluginroot.'/.git')) {
684 return 'git';
685 }
686
a5d08dce
DM
687 if (is_file($pluginroot.'/.git')) {
688 return 'git-submodule';
689 }
690
e87214bd
PS
691 if (is_dir($pluginroot.'/CVS')) {
692 return 'cvs';
693 }
694
695 if (is_dir($pluginroot.'/.svn')) {
696 return 'svn';
697 }
698
0b515736
OS
699 if (is_dir($pluginroot.'/.hg')) {
700 return 'mercurial';
701 }
702
e87214bd
PS
703 return false;
704 }
705
706 /**
707 * Get a list of any other plugins that require this one.
708 * @param string $component frankenstyle component name.
709 * @return array of frankensyle component names that require this one.
710 */
711 public function other_plugins_that_require($component) {
712 $others = array();
713 foreach ($this->get_plugins() as $type => $plugins) {
714 foreach ($plugins as $plugin) {
715 $required = $plugin->get_other_required_plugins();
716 if (isset($required[$component])) {
717 $others[] = $plugin->component;
718 }
719 }
720 }
721 return $others;
722 }
723
724 /**
725 * Check a dependencies list against the list of installed plugins.
726 * @param array $dependencies compenent name to required version or ANY_VERSION.
727 * @return bool true if all the dependencies are satisfied.
728 */
729 public function are_dependencies_satisfied($dependencies) {
730 foreach ($dependencies as $component => $requiredversion) {
731 $otherplugin = $this->get_plugin_info($component);
732 if (is_null($otherplugin)) {
733 return false;
734 }
735
736 if ($requiredversion != ANY_VERSION and $otherplugin->versiondisk < $requiredversion) {
737 return false;
738 }
739 }
740
741 return true;
742 }
743
744 /**
745 * Checks all dependencies for all installed plugins
746 *
747 * This is used by install and upgrade. The array passed by reference as the second
748 * argument is populated with the list of plugins that have failed dependencies (note that
749 * a single plugin can appear multiple times in the $failedplugins).
750 *
751 * @param int $moodleversion the version from version.php.
752 * @param array $failedplugins to return the list of plugins with non-satisfied dependencies
753 * @return bool true if all the dependencies are satisfied for all plugins.
754 */
755 public function all_plugins_ok($moodleversion, &$failedplugins = array()) {
756
757 $return = true;
758 foreach ($this->get_plugins() as $type => $plugins) {
759 foreach ($plugins as $plugin) {
760
761 if (!$plugin->is_core_dependency_satisfied($moodleversion)) {
762 $return = false;
763 $failedplugins[] = $plugin->component;
764 }
765
766 if (!$this->are_dependencies_satisfied($plugin->get_other_required_plugins())) {
767 $return = false;
768 $failedplugins[] = $plugin->component;
769 }
770 }
771 }
772
773 return $return;
774 }
775
7eb87eff
DM
776 /**
777 * Resolve requirements and dependencies of a plugin.
778 *
779 * Returns an array of objects describing the requirement/dependency,
780 * indexed by the frankenstyle name of the component. The returned array
781 * can be empty. The objects in the array have following properties:
782 *
783 * ->(numeric)hasver
784 * ->(numeric)reqver
785 * ->(string)status
5a92cd0b 786 * ->(string)availability
7eb87eff
DM
787 *
788 * @param \core\plugininfo\base $plugin the plugin we are checking
789 * @param null|string|int|double $moodleversion explicit moodle core version to check against, defaults to $CFG->version
790 * @param null|string|int $moodlebranch explicit moodle core branch to check against, defaults to $CFG->branch
791 * @return array of objects
792 */
793 public function resolve_requirements(\core\plugininfo\base $plugin, $moodleversion=null, $moodlebranch=null) {
794 global $CFG;
795
4441d5e5
DM
796 if ($plugin->versiondisk === null) {
797 // Missing from disk, we have no version.php to read from.
798 return array();
799 }
800
7eb87eff
DM
801 if ($moodleversion === null) {
802 $moodleversion = $CFG->version;
803 }
804
805 if ($moodlebranch === null) {
806 $moodlebranch = $CFG->branch;
807 }
808
809 $reqs = array();
810 $reqcore = $this->resolve_core_requirements($plugin, $moodleversion);
811
812 if (!empty($reqcore)) {
813 $reqs['core'] = $reqcore;
814 }
815
816 foreach ($plugin->get_other_required_plugins() as $reqplug => $reqver) {
817 $reqs[$reqplug] = $this->resolve_dependency_requirements($plugin, $reqplug, $reqver, $moodlebranch);
818 }
819
820 return $reqs;
821 }
822
823 /**
824 * Helper method to resolve plugin's requirements on the moodle core.
825 *
826 * @param \core\plugininfo\base $plugin the plugin we are checking
827 * @param string|int|double $moodleversion moodle core branch to check against
828 * @return stdObject
829 */
830 protected function resolve_core_requirements(\core\plugininfo\base $plugin, $moodleversion) {
831
5a92cd0b
DM
832 $reqs = (object)array(
833 'hasver' => null,
834 'reqver' => null,
835 'status' => null,
836 'availability' => null,
837 );
7eb87eff
DM
838
839 $reqs->hasver = $moodleversion;
840
841 if (empty($plugin->versionrequires)) {
842 $reqs->reqver = ANY_VERSION;
843 } else {
844 $reqs->reqver = $plugin->versionrequires;
845 }
846
847 if ($plugin->is_core_dependency_satisfied($moodleversion)) {
848 $reqs->status = self::REQUIREMENT_STATUS_OK;
849 } else {
850 $reqs->status = self::REQUIREMENT_STATUS_OUTDATED;
851 }
852
853 return $reqs;
854 }
855
856 /**
857 * Helper method to resolve plugin's dependecies on other plugins.
858 *
859 * @param \core\plugininfo\base $plugin the plugin we are checking
860 * @param string $otherpluginname
861 * @param string|int $requiredversion
862 * @param string|int $moodlebranch explicit moodle core branch to check against, defaults to $CFG->branch
863 * @return stdClass
864 */
865 protected function resolve_dependency_requirements(\core\plugininfo\base $plugin, $otherpluginname,
866 $requiredversion, $moodlebranch) {
867
5a92cd0b
DM
868 $reqs = (object)array(
869 'hasver' => null,
870 'reqver' => null,
871 'status' => null,
872 'availability' => null,
873 );
874
7eb87eff
DM
875 $otherplugin = $this->get_plugin_info($otherpluginname);
876
877 if ($otherplugin !== null) {
878 // The required plugin is installed.
879 $reqs->hasver = $otherplugin->versiondisk;
880 $reqs->reqver = $requiredversion;
881 // Check it has sufficient version.
882 if ($requiredversion == ANY_VERSION or $otherplugin->versiondisk >= $requiredversion) {
883 $reqs->status = self::REQUIREMENT_STATUS_OK;
884 } else {
885 $reqs->status = self::REQUIREMENT_STATUS_OUTDATED;
886 }
887
888 } else {
889 // The required plugin is not installed.
890 $reqs->hasver = null;
891 $reqs->reqver = $requiredversion;
892 $reqs->status = self::REQUIREMENT_STATUS_MISSING;
5a92cd0b
DM
893 }
894
895 if ($reqs->status !== self::REQUIREMENT_STATUS_OK) {
35f2b674 896 if ($this->is_remote_plugin_available($otherpluginname, $requiredversion, false)) {
5a92cd0b
DM
897 $reqs->availability = self::REQUIREMENT_AVAILABLE;
898 } else {
899 $reqs->availability = self::REQUIREMENT_UNAVAILABLE;
900 }
7eb87eff
DM
901 }
902
903 return $reqs;
904 }
905
5a92cd0b
DM
906 /**
907 * Is the given plugin version available in the plugins directory?
908 *
35f2b674
DM
909 * See {@link self::get_remote_plugin_info()} for the full explanation of how the $version
910 * parameter is interpretted.
911 *
912 * @param string $component plugin frankenstyle name
913 * @param string|int $version ANY_VERSION or the version number
914 * @param bool $exactmatch false if "given version or higher" is requested
5a92cd0b
DM
915 * @return boolean
916 */
35f2b674 917 public function is_remote_plugin_available($component, $version, $exactmatch) {
5a92cd0b 918
35f2b674 919 $info = $this->get_remote_plugin_info($component, $version, $exactmatch);
5a92cd0b
DM
920
921 if (empty($info)) {
922 // There is no available plugin of that name.
923 return false;
924 }
925
926 if (empty($info->version)) {
927 // Plugin is known, but no suitable version was found.
928 return false;
929 }
930
931 return true;
932 }
933
9137a89a 934 /**
35f2b674 935 * Can the given plugin version be installed via the admin UI?
9137a89a 936 *
36977a6d
DM
937 * This check should be used whenever attempting to install a plugin from
938 * the plugins directory (new install, available update, missing dependency).
939 *
9137a89a 940 * @param string $component
35f2b674 941 * @param int $version version number
36977a6d 942 * $param string $reason returned code of the reason why it is not
9137a89a
DM
943 * @return boolean
944 */
36977a6d 945 public function is_remote_plugin_installable($component, $version, &$reason=null) {
9137a89a
DM
946 global $CFG;
947
948 // Make sure the feature is not disabled.
949 if (!empty($CFG->disableonclickaddoninstall)) {
36977a6d 950 $reason = 'disabled';
9137a89a
DM
951 return false;
952 }
953
36977a6d 954 // Make sure the version is available.
35f2b674 955 if (!$this->is_remote_plugin_available($component, $version, true)) {
36977a6d 956 $reason = 'remoteunavailable';
9137a89a
DM
957 return false;
958 }
959
960 // Make sure the plugin type root directory is writable.
961 list($plugintype, $pluginname) = core_component::normalize_component($component);
962 if (!$this->is_plugintype_writable($plugintype)) {
36977a6d 963 $reason = 'notwritableplugintype';
9137a89a
DM
964 return false;
965 }
966
35f2b674 967 $remoteinfo = $this->get_remote_plugin_info($component, $version, true);
9137a89a
DM
968 $localinfo = $this->get_plugin_info($component);
969
970 if ($localinfo) {
971 // If the plugin is already present, prevent downgrade.
36977a6d
DM
972 if ($localinfo->versiondb > $remoteinfo->version->version) {
973 $reason = 'cannotdowngrade';
9137a89a
DM
974 return false;
975 }
976
977 // Make sure we have write access to all the existing code.
36977a6d
DM
978 if (is_dir($localinfo->rootdir)) {
979 if (!$this->is_plugin_folder_removable($component)) {
980 $reason = 'notwritableplugin';
981 return false;
982 }
9137a89a
DM
983 }
984 }
985
986 // Looks like it could work.
987 return true;
988 }
989
c948b813
DM
990 /**
991 * Given the list of remote plugin infos, return just those installable.
992 *
993 * This is typically used on lists returned by
994 * {@link self::available_updates()} or {@link self::missing_dependencies()}
995 * to perform bulk installation of remote plugins.
996 *
997 * @param array $remoteinfos list of {@link \core\update\remote_info}
998 * @return array
999 */
1000 public function filter_installable($remoteinfos) {
1001
1002 if (empty($remoteinfos)) {
1003 return array();
1004 }
1005 $installable = array();
1006 foreach ($remoteinfos as $index => $remoteinfo) {
1007 if ($this->is_remote_plugin_installable($remoteinfo->component, $remoteinfo->version->version)) {
1008 $installable[$index] = $remoteinfo;
1009 }
1010 }
1011 return $installable;
1012 }
1013
5a92cd0b
DM
1014 /**
1015 * Returns information about a plugin in the plugins directory.
1016 *
35f2b674
DM
1017 * This is typically used when checking for available dependencies (in
1018 * which case the $version represents minimal version we need), or
1019 * when installing an available update or a new plugin from the plugins
1020 * directory (in which case the $version is exact version we are
1021 * interested in). The interpretation of the $version is controlled
1022 * by the $exactmatch argument.
5a92cd0b 1023 *
35f2b674
DM
1024 * If a plugin with the given component name is found, data about the
1025 * plugin are returned as an object. The ->version property of the object
1026 * contains the information about the particular plugin version that
1027 * matches best the given critera. The ->version property is false if no
1028 * suitable version of the plugin was found (yet the plugin itself is
1029 * known).
1030 *
1031 * See {@link \core\update\api::validate_pluginfo_format()} for the
1032 * returned data structure.
1033 *
1034 * @param string $component plugin frankenstyle name
1035 * @param string|int $version ANY_VERSION or the version number
1036 * @param bool $exactmatch false if "given version or higher" is requested
4f18a4e6 1037 * @return \core\update\remote_info|bool
5a92cd0b 1038 */
35f2b674 1039 public function get_remote_plugin_info($component, $version, $exactmatch) {
5a92cd0b 1040
35f2b674
DM
1041 if ($exactmatch and $version == ANY_VERSION) {
1042 throw new coding_exception('Invalid request for exactly any version, it does not make sense.');
5a92cd0b
DM
1043 }
1044
35f2b674
DM
1045 $client = $this->get_update_api_client();
1046
1047 if ($exactmatch) {
1048 // Use client's get_plugin_info() method.
1049 if (!isset($this->remotepluginsinfoexact[$component][$version])) {
1050 $this->remotepluginsinfoexact[$component][$version] = $client->get_plugin_info($component, $version);
1051 }
1052 return $this->remotepluginsinfoexact[$component][$version];
1053
1054 } else {
1055 // Use client's find_plugin() method.
1056 if (!isset($this->remotepluginsinfoatleast[$component][$version])) {
1057 $this->remotepluginsinfoatleast[$component][$version] = $client->find_plugin($component, $version);
1058 }
1059 return $this->remotepluginsinfoatleast[$component][$version];
1060 }
5a92cd0b
DM
1061 }
1062
1063 /**
0e442ee7
DM
1064 * Obtain the plugin ZIP file from the given URL
1065 *
1066 * The caller is supposed to know both downloads URL and the MD5 hash of
1067 * the ZIP contents in advance, typically by using the API requests against
1068 * the plugins directory.
1069 *
1070 * @param string $url
1071 * @param string $md5
1072 * @return string|bool full path to the file, false on error
1073 */
1074 public function get_remote_plugin_zip($url, $md5) {
1075 return $this->get_code_manager()->get_remote_plugin_zip($url, $md5);
1076 }
1077
1078 /**
1079 * Extracts the saved plugin ZIP file.
1080 *
1081 * Returns the list of files found in the ZIP. The format of that list is
1082 * array of (string)filerelpath => (bool|string) where the array value is
1083 * either true or a string describing the problematic file.
1084 *
1085 * @see zip_packer::extract_to_pathname()
1086 * @param string $zipfilepath full path to the saved ZIP file
1087 * @param string $targetdir full path to the directory to extract the ZIP file to
1088 * @param string $rootdir explicitly rename the root directory of the ZIP into this non-empty value
1089 * @param array list of extracted files as returned by {@link zip_packer::extract_to_pathname()}
1090 */
1091 public function unzip_plugin_file($zipfilepath, $targetdir, $rootdir = '') {
1092 return $this->get_code_manager()->unzip_plugin_file($zipfilepath, $targetdir, $rootdir);
1093 }
1094
2d00be61
DM
1095 /**
1096 * Detects the plugin's name from its ZIP file.
1097 *
1098 * Plugin ZIP packages are expected to contain a single directory and the
1099 * directory name would become the plugin name once extracted to the Moodle
1100 * dirroot.
1101 *
1102 * @param string $zipfilepath full path to the ZIP files
1103 * @return string|bool false on error
1104 */
1105 public function get_plugin_zip_root_dir($zipfilepath) {
1106 return $this->get_code_manager()->get_plugin_zip_root_dir($zipfilepath);
1107 }
1108
0e442ee7
DM
1109 /**
1110 * Return a list of missing dependencies.
5a92cd0b
DM
1111 *
1112 * This should provide the full list of plugins that should be installed to
1113 * fulfill the requirements of all plugins, if possible.
1114 *
0e442ee7 1115 * @param bool $availableonly return only available missing dependencies
4f18a4e6 1116 * @return array of \core\update\remote_info|bool indexed by the component name
5a92cd0b 1117 */
0e442ee7 1118 public function missing_dependencies($availableonly=false) {
5a92cd0b
DM
1119
1120 $dependencies = array();
1121
1122 foreach ($this->get_plugins() as $plugintype => $pluginfos) {
1123 foreach ($pluginfos as $pluginname => $pluginfo) {
1124 foreach ($this->resolve_requirements($pluginfo) as $reqname => $reqinfo) {
1125 if ($reqname === 'core') {
1126 continue;
1127 }
1128 if ($reqinfo->status != self::REQUIREMENT_STATUS_OK) {
1129 if ($reqinfo->availability == self::REQUIREMENT_AVAILABLE) {
35f2b674 1130 $remoteinfo = $this->get_remote_plugin_info($reqname, $reqinfo->reqver, false);
5a92cd0b
DM
1131
1132 if (empty($dependencies[$reqname])) {
1133 $dependencies[$reqname] = $remoteinfo;
1134 } else {
35f2b674
DM
1135 // If resolving requirements has led to two different versions of the same
1136 // remote plugin, pick the higher version. This can happen in cases like one
1137 // plugin requiring ANY_VERSION and another plugin requiring specific higher
1138 // version with lower maturity of a remote plugin.
5a92cd0b
DM
1139 if ($remoteinfo->version->version > $dependencies[$reqname]->version->version) {
1140 $dependencies[$reqname] = $remoteinfo;
1141 }
1142 }
1143
1144 } else {
1145 if (!isset($dependencies[$reqname])) {
1146 // Unable to find a plugin fulfilling the requirements.
1147 $dependencies[$reqname] = false;
1148 }
1149 }
1150 }
1151 }
1152 }
1153 }
1154
0e442ee7
DM
1155 if ($availableonly) {
1156 foreach ($dependencies as $component => $info) {
1157 if (empty($info) or empty($info->version)) {
1158 unset($dependencies[$component]);
1159 }
1160 }
1161 }
1162
5a92cd0b
DM
1163 return $dependencies;
1164 }
1165
e87214bd
PS
1166 /**
1167 * Is it possible to uninstall the given plugin?
1168 *
1169 * False is returned if the plugininfo subclass declares the uninstall should
1170 * not be allowed via {@link \core\plugininfo\base::is_uninstall_allowed()} or if the
1171 * core vetoes it (e.g. becase the plugin or some of its subplugins is required
1172 * by some other installed plugin).
1173 *
1174 * @param string $component full frankenstyle name, e.g. mod_foobar
1175 * @return bool
1176 */
1177 public function can_uninstall_plugin($component) {
1178
1179 $pluginfo = $this->get_plugin_info($component);
1180
1181 if (is_null($pluginfo)) {
1182 return false;
1183 }
1184
1185 if (!$this->common_uninstall_check($pluginfo)) {
1186 return false;
1187 }
1188
1189 // Verify only if something else requires the subplugins, do not verify their common_uninstall_check()!
1190 $subplugins = $this->get_subplugins_of_plugin($pluginfo->component);
1191 foreach ($subplugins as $subpluginfo) {
1192 // Check if there are some other plugins requiring this subplugin
1193 // (but the parent and siblings).
1194 foreach ($this->other_plugins_that_require($subpluginfo->component) as $requiresme) {
1195 $ismyparent = ($pluginfo->component === $requiresme);
1196 $ismysibling = in_array($requiresme, array_keys($subplugins));
1197 if (!$ismyparent and !$ismysibling) {
1198 return false;
1199 }
1200 }
1201 }
1202
1203 // Check if there are some other plugins requiring this plugin
1204 // (but its subplugins).
1205 foreach ($this->other_plugins_that_require($pluginfo->component) as $requiresme) {
1206 $ismysubplugin = in_array($requiresme, array_keys($subplugins));
1207 if (!$ismysubplugin) {
1208 return false;
1209 }
1210 }
1211
1212 return true;
1213 }
1214
c948b813 1215 /**
2d00be61
DM
1216 * Perform the installation of plugins.
1217 *
1218 * If used for installation of remote plugins from the Moodle Plugins
1219 * directory, the $plugins must be list of {@link \core\update\remote_info}
1220 * object that represent installable remote plugins. The caller can use
1221 * {@link self::filter_installable()} to prepare the list.
c948b813 1222 *
2d00be61
DM
1223 * If used for installation of plugins from locally available ZIP files,
1224 * the $plugins should be list of objects with properties ->component and
1225 * ->zipfilepath.
c948b813 1226 *
2d00be61
DM
1227 * The method uses {@link mtrace()} to produce direct output and can be
1228 * used in both web and cli interfaces.
1229 *
1230 * @param array $plugins list of plugins
c948b813
DM
1231 * @param bool $confirmed should the files be really deployed into the dirroot?
1232 * @param bool $silent perform without output
1233 * @return bool true on success
1234 */
2d00be61 1235 public function install_plugins(array $plugins, $confirmed, $silent) {
c948b813
DM
1236 global $CFG, $OUTPUT;
1237
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'),
e87214bd 1649 'enrol' => array('authorize'),
1170df12 1650 'tinymce' => array('dragmath'),
d6e7a63d 1651 'tool' => array('bloglevelupgrade', 'qeupgradehelper', 'timezoneimport'),
7a2dabcb
FM
1652 'theme' => array('mymobile', 'afterburner', 'anomaly', 'arialist', 'binarius', 'boxxie', 'brick', 'formal_white',
1653 'formfactor', 'fusion', 'leatherbound', 'magazine', 'nimble', 'nonzero', 'overlay', 'serenity', 'sky_high',
1654 'splash', 'standard', 'standardold'),
e87214bd
PS
1655 );
1656
1657 if (!isset($plugins[$type])) {
1658 return false;
1659 }
1660 return in_array($name, $plugins[$type]);
1661 }
1662
1663 /**
1664 * Defines a white list of all plugins shipped in the standard Moodle distribution
1665 *
1666 * @param string $type
1667 * @return false|array array of standard plugins or false if the type is unknown
1668 */
1669 public static function standard_plugins_list($type) {
1670
1671 $standard_plugins = array(
1672
adca7326 1673 'atto' => array(
205c6db5
MG
1674 'accessibilitychecker', 'accessibilityhelper', 'align',
1675 'backcolor', 'bold', 'charmap', 'clear', 'collapse', 'emoticon',
1676 'equation', 'fontcolor', 'html', 'image', 'indent', 'italic',
1677 'link', 'managefiles', 'media', 'noautolink', 'orderedlist',
1678 'rtl', 'strike', 'subscript', 'superscript', 'table', 'title',
49a510ef 1679 'underline', 'undo', 'unorderedlist'
adca7326
DW
1680 ),
1681
e87214bd
PS
1682 'assignment' => array(
1683 'offline', 'online', 'upload', 'uploadsingle'
1684 ),
1685
1686 'assignsubmission' => array(
1687 'comments', 'file', 'onlinetext'
1688 ),
1689
1690 'assignfeedback' => array(
1691 'comments', 'file', 'offline', 'editpdf'
1692 ),
1693
e87214bd
PS
1694 'auth' => array(
1695 'cas', 'db', 'email', 'fc', 'imap', 'ldap', 'manual', 'mnet',
1696 'nntp', 'nologin', 'none', 'pam', 'pop3', 'radius',
1697 'shibboleth', 'webservice'
1698 ),
1699
d3db4b03 1700 'availability' => array(
1701 'completion', 'date', 'grade', 'group', 'grouping', 'profile'
1702 ),
1703
e87214bd 1704 'block' => array(
d6383f6a
SB
1705 'activity_modules', 'activity_results', 'admin_bookmarks', 'badges',
1706 'blog_menu', 'blog_recent', 'blog_tags', 'calendar_month',
e87214bd
PS
1707 'calendar_upcoming', 'comments', 'community',
1708 'completionstatus', 'course_list', 'course_overview',
1709 'course_summary', 'feedback', 'glossary_random', 'html',
1710 'login', 'mentees', 'messages', 'mnet_hosts', 'myprofile',
1711 'navigation', 'news_items', 'online_users', 'participants',
1712 'private_files', 'quiz_results', 'recent_activity',
1713 'rss_client', 'search_forums', 'section_links',
1714 'selfcompletion', 'settings', 'site_main_menu',
1715 'social_activities', 'tag_flickr', 'tag_youtube', 'tags'
1716 ),
1717
1718 'booktool' => array(
1719 'exportimscp', 'importhtml', 'print'
1720 ),
1721
1722 'cachelock' => array(
1723 'file'
1724 ),
1725
1726 'cachestore' => array(
1727 'file', 'memcache', 'memcached', 'mongodb', 'session', 'static'
1728 ),
1729
1730 'calendartype' => array(
1731 'gregorian'
1732 ),
1733
1734 'coursereport' => array(
1735 // Deprecated!
1736 ),
1737
1738 'datafield' => array(
1739 'checkbox', 'date', 'file', 'latlong', 'menu', 'multimenu',
1740 'number', 'picture', 'radiobutton', 'text', 'textarea', 'url'
1741 ),
1742
1743 'datapreset' => array(
1744 'imagegallery'
1745 ),
1746
1747 'editor' => array(
205c6db5 1748 'atto', 'textarea', 'tinymce'
e87214bd
PS
1749 ),
1750
1751 'enrol' => array(
1752 'category', 'cohort', 'database', 'flatfile',
1753 'guest', 'imsenterprise', 'ldap', 'manual', 'meta', 'mnet',
1754 'paypal', 'self'
1755 ),
1756
1757 'filter' => array(
1758 'activitynames', 'algebra', 'censor', 'emailprotect',
289ed254 1759 'emoticon', 'mathjaxloader', 'mediaplugin', 'multilang', 'tex', 'tidy',
e87214bd
PS
1760 'urltolink', 'data', 'glossary'
1761 ),
1762
1763 'format' => array(
1764 'singleactivity', 'social', 'topics', 'weeks'
1765 ),
1766
1767 'gradeexport' => array(
1768 'ods', 'txt', 'xls', 'xml'
1769 ),
1770
1771 'gradeimport' => array(
aa60bda9 1772 'csv', 'direct', 'xml'
e87214bd
PS
1773 ),
1774
1775 'gradereport' => array(
8ec7b088 1776 'grader', 'history', 'outcomes', 'overview', 'user', 'singleview'
e87214bd
PS
1777 ),
1778
1779 'gradingform' => array(
1780 'rubric', 'guide'
1781 ),
1782
1783 'local' => array(
1784 ),
1785
7eaca5a8
1786 'logstore' => array(
1787 'database', 'legacy', 'standard',
1788 ),
1789
e3f69b58 1790 'ltiservice' => array(
3562c426 1791 'memberships', 'profile', 'toolproxy', 'toolsettings'
e3f69b58 1792 ),
1793
e87214bd 1794 'message' => array(
324facf4 1795 'airnotifier', 'email', 'jabber', 'popup'
e87214bd
PS
1796 ),
1797
1798 'mnetservice' => array(
1799 'enrol'
1800 ),
1801
1802 'mod' => array(
1803 'assign', 'assignment', 'book', 'chat', 'choice', 'data', 'feedback', 'folder',
1804 'forum', 'glossary', 'imscp', 'label', 'lesson', 'lti', 'page',
1805 'quiz', 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop'
1806 ),
1807
1808 'plagiarism' => array(
1809 ),
1810
1811 'portfolio' => array(
1812 'boxnet', 'download', 'flickr', 'googledocs', 'mahara', 'picasa'
1813 ),
1814
1815 'profilefield' => array(
1816 'checkbox', 'datetime', 'menu', 'text', 'textarea'
1817 ),
1818
1819 'qbehaviour' => array(
1820 'adaptive', 'adaptivenopenalty', 'deferredcbm',
1821 'deferredfeedback', 'immediatecbm', 'immediatefeedback',
1822 'informationitem', 'interactive', 'interactivecountback',
1823 'manualgraded', 'missing'
1824 ),
1825
1826 'qformat' => array(
1827 'aiken', 'blackboard_six', 'examview', 'gift',
a75fa4c0 1828 'missingword', 'multianswer', 'webct',
e87214bd
PS
1829 'xhtml', 'xml'
1830 ),
1831
1832 'qtype' => array(
1833 'calculated', 'calculatedmulti', 'calculatedsimple',
6e28e150
TH
1834 'ddimageortext', 'ddmarker', 'ddwtos', 'description',
1835 'essay', 'gapselect', 'match', 'missingtype', 'multianswer',
e87214bd
PS
1836 'multichoice', 'numerical', 'random', 'randomsamatch',
1837 'shortanswer', 'truefalse'
1838 ),
1839
1840 'quiz' => array(
1841 'grading', 'overview', 'responses', 'statistics'
1842 ),
1843
1844 'quizaccess' => array(
1845 'delaybetweenattempts', 'ipaddress', 'numattempts', 'openclosedate',
1846 'password', 'safebrowser', 'securewindow', 'timelimit'
1847 ),
1848
1849 'report' => array(
4f078f38 1850 'backups', 'completion', 'configlog', 'courseoverview', 'eventlist',
8064168e
PS
1851 'log', 'loglive', 'outline', 'participation', 'progress', 'questioninstances', 'security', 'stats', 'performance',
1852 'usersessions',
e87214bd
PS
1853 ),
1854
1855 'repository' => array(
1856 'alfresco', 'areafiles', 'boxnet', 'coursefiles', 'dropbox', 'equella', 'filesystem',
1857 'flickr', 'flickr_public', 'googledocs', 'local', 'merlot',
1858 'picasa', 'recent', 'skydrive', 's3', 'upload', 'url', 'user', 'webdav',
1859 'wikimedia', 'youtube'
1860 ),
1861
1862 'scormreport' => array(
1863 'basic',
1864 'interactions',
1865 'graphs',
1866 'objectives'
1867 ),
1868
1869 'tinymce' => array(
1170df12 1870 'ctrlhelp', 'managefiles', 'moodleemoticon', 'moodleimage',
e87214bd
PS
1871 'moodlemedia', 'moodlenolink', 'pdw', 'spellchecker', 'wrap'
1872 ),
1873
1874 'theme' => array(
bfb6e97e 1875 'base', 'bootstrapbase', 'canvas', 'clean', 'more'
e87214bd
PS
1876 ),
1877
1878 'tool' => array(
d3db4b03 1879 'assignmentupgrade', 'availabilityconditions', 'behat', 'capability', 'customlang',
ae46ca5f 1880 'dbtransfer', 'filetypes', 'generator', 'health', 'innodb', 'installaddon',
92b40de9 1881 'langimport', 'log', 'messageinbound', 'multilangupgrade', 'monitor', 'phpunit', 'profiling',
274d79c9 1882 'replace', 'spamcleaner', 'task', 'templatelibrary',
e87214bd
PS
1883 'unittest', 'uploadcourse', 'uploaduser', 'unsuproles', 'xmldb'
1884 ),
1885
1886 'webservice' => array(
1887 'amf', 'rest', 'soap', 'xmlrpc'
1888 ),
1889
1890 'workshopallocation' => array(
1891 'manual', 'random', 'scheduled'
1892 ),
1893
1894 'workshopeval' => array(
1895 'best'
1896 ),
1897
1898 'workshopform' => array(
1899 'accumulative', 'comments', 'numerrors', 'rubric'
1900 )
1901 );
1902
1903 if (isset($standard_plugins[$type])) {
1904 return $standard_plugins[$type];
1905 } else {
1906 return false;
1907 }
1908 }
1909
a2e1e0d0
DM
1910 /**
1911 * Remove the current plugin code from the dirroot.
1912 *
1913 * If removing the currently installed version (which happens during
1914 * updates), we archive the code so that the upgrade can be cancelled.
1915 *
1916 * To prevent accidental data-loss, we also archive the existing plugin
1917 * code if cancelling installation of it, so that the developer does not
1918 * loose the only version of their work-in-progress.
1919 *
1920 * @param \core\plugininfo\base $plugin
1921 */
1922 public function remove_plugin_folder(\core\plugininfo\base $plugin) {
1923
1924 if (!$this->is_plugin_folder_removable($plugin->component)) {
1925 throw new moodle_exception('err_removing_unremovable_folder', 'core_plugin', '',
1926 array('plugin' => $pluginfo->component, 'rootdir' => $pluginfo->rootdir),
1927 'plugin root folder is not removable as expected');
1928 }
1929
1930 if ($plugin->get_status() === self::PLUGIN_STATUS_UPTODATE or $plugin->get_status() === self::PLUGIN_STATUS_NEW) {
1931 $this->archive_plugin_version($plugin);
1932 }
1933
1934 remove_dir($plugin->rootdir);
1935 clearstatcache();
1936 if (function_exists('opcache_reset')) {
1937 opcache_reset();
1938 }
1939 }
1940
4d7528f9
DM
1941 /**
1942 * Can the installation of the new plugin be cancelled?
1943 *
1944 * @param \core\plugininfo\base $plugin
1945 * @return bool
1946 */
1947 public function can_cancel_plugin_installation(\core\plugininfo\base $plugin) {
1948
1949 if (empty($plugin) or $plugin->is_standard() or !$this->is_plugin_folder_removable($plugin->component)) {
1950 return false;
1951 }
1952
1953 if ($plugin->get_status() === self::PLUGIN_STATUS_NEW) {
1954 return true;
1955 }
1956
1957 return false;
1958 }
1959
2f29cf6e
DM
1960 /**
1961 * Removes the plugin code directory if it is not installed yet.
1962 *
1963 * This is intended for the plugins check screen to give the admin a chance
1964 * to cancel the installation of just unzipped plugin before the database
1965 * upgrade happens.
1966 *
1967 * @param string $component
2f29cf6e
DM
1968 */
1969 public function cancel_plugin_installation($component) {
1970
1971 $plugin = $this->get_plugin_info($component);
1972
4d7528f9 1973 if ($this->can_cancel_plugin_installation($plugin)) {
a2e1e0d0 1974 $this->remove_plugin_folder($plugin);
2f29cf6e
DM
1975 }
1976
4d7528f9 1977 return false;
2f29cf6e
DM
1978 }
1979
1980 /**
1981 * Cancels installation of all new additional plugins.
1982 */
1983 public function cancel_all_plugin_installations() {
1984
1985 foreach ($this->get_plugins() as $type => $plugins) {
1986 foreach ($plugins as $plugin) {
4d7528f9 1987 if ($this->can_cancel_plugin_installation($plugin)) {
2f29cf6e
DM
1988 $this->cancel_plugin_installation($plugin->component);
1989 }
1990 }
1991 }
1992 }
1993
4d7528f9
DM
1994 /**
1995 * Archive the current on-disk plugin code.
1996 *
1997 * @param \core\plugiinfo\base $plugin
1998 * @return bool
1999 */
2000 public function archive_plugin_version(\core\plugininfo\base $plugin) {
a2e1e0d0 2001 return $this->get_code_manager()->archive_plugin_version($plugin->rootdir, $plugin->component, $plugin->versiondisk);
4d7528f9
DM
2002 }
2003
e87214bd
PS
2004 /**
2005 * Reorders plugin types into a sequence to be displayed
2006 *
2007 * For technical reasons, plugin types returned by {@link core_component::get_plugin_types()} are
2008 * in a certain order that does not need to fit the expected order for the display.
2009 * Particularly, activity modules should be displayed first as they represent the
2010 * real heart of Moodle. They should be followed by other plugin types that are
2011 * used to build the courses (as that is what one expects from LMS). After that,
2012 * other supportive plugin types follow.
2013 *
2014 * @param array $types associative array
2015 * @return array same array with altered order of items
2016 */
2017 protected function reorder_plugin_types(array $types) {
2018 $fix = array('mod' => $types['mod']);
2019 foreach (core_component::get_plugin_list('mod') as $plugin => $fulldir) {
2020 if (!$subtypes = core_component::get_subplugins('mod_'.$plugin)) {
2021 continue;
2022 }
2023 foreach ($subtypes as $subtype => $ignored) {
2024 $fix[$subtype] = $types[$subtype];
2025 }
2026 }
2027
2028 $fix['mod'] = $types['mod'];
2029 $fix['block'] = $types['block'];
2030 $fix['qtype'] = $types['qtype'];
2031 $fix['qbehaviour'] = $types['qbehaviour'];
2032 $fix['qformat'] = $types['qformat'];
2033 $fix['filter'] = $types['filter'];
2034
2035 $fix['editor'] = $types['editor'];
2036 foreach (core_component::get_plugin_list('editor') as $plugin => $fulldir) {
2037 if (!$subtypes = core_component::get_subplugins('editor_'.$plugin)) {
2038 continue;
2039 }
2040 foreach ($subtypes as $subtype => $ignored) {
2041 $fix[$subtype] = $types[$subtype];
2042 }
2043 }
2044
2045 $fix['enrol'] = $types['enrol'];
2046 $fix['auth'] = $types['auth'];
2047 $fix['tool'] = $types['tool'];
2048 foreach (core_component::get_plugin_list('tool') as $plugin => $fulldir) {
2049 if (!$subtypes = core_component::get_subplugins('tool_'.$plugin)) {
2050 continue;
2051 }
2052 foreach ($subtypes as $subtype => $ignored) {
2053 $fix[$subtype] = $types[$subtype];
2054 }
2055 }
2056
2057 foreach ($types as $type => $path) {
2058 if (!isset($fix[$type])) {
2059 $fix[$type] = $path;
2060 }
2061 }
2062 return $fix;
2063 }
2064
2065 /**
2066 * Check if the given directory can be removed by the web server process.
2067 *
2068 * This recursively checks that the given directory and all its contents
2069 * it writable.
2070 *
2071 * @param string $fullpath
2072 * @return boolean
2073 */
8acee4b5 2074 public function is_directory_removable($fullpath) {
e87214bd
PS
2075
2076 if (!is_writable($fullpath)) {
2077 return false;
2078 }
2079
2080 if (is_dir($fullpath)) {
2081 $handle = opendir($fullpath);
2082 } else {
2083 return false;
2084 }
2085
2086 $result = true;
2087
2088 while ($filename = readdir($handle)) {
2089
2090 if ($filename === '.' or $filename === '..') {
2091 continue;
2092 }
2093
2094 $subfilepath = $fullpath.'/'.$filename;
2095
2096 if (is_dir($subfilepath)) {
2097 $result = $result && $this->is_directory_removable($subfilepath);
2098
2099 } else {
2100 $result = $result && is_writable($subfilepath);
2101 }
2102 }
2103
2104 closedir($handle);
2105
2106 return $result;
2107 }
2108
2109 /**
2110 * Helper method that implements common uninstall prerequisites
2111 *
2112 * @param \core\plugininfo\base $pluginfo
2113 * @return bool
2114 */
2115 protected function common_uninstall_check(\core\plugininfo\base $pluginfo) {
2116
2117 if (!$pluginfo->is_uninstall_allowed()) {
2118 // The plugin's plugininfo class declares it should not be uninstalled.
2119 return false;
2120 }
2121
361feecd 2122 if ($pluginfo->get_status() === static::PLUGIN_STATUS_NEW) {
e87214bd
PS
2123 // The plugin is not installed. It should be either installed or removed from the disk.
2124 // Relying on this temporary state may be tricky.
2125 return false;
2126 }
2127
2128 if (method_exists($pluginfo, 'get_uninstall_url') and is_null($pluginfo->get_uninstall_url())) {
2129 // Backwards compatibility.
2130 debugging('\core\plugininfo\base subclasses should use is_uninstall_allowed() instead of returning null in get_uninstall_url()',
2131 DEBUG_DEVELOPER);
2132 return false;
2133 }
2134
2135 return true;
2136 }
0e442ee7
DM
2137
2138 /**
2139 * Returns a code_manager instance to be used for the plugins code operations.
2140 *
2141 * @return \core\update\code_manager
2142 */
2143 protected function get_code_manager() {
2144
2145 if ($this->codemanager === null) {
2146 $this->codemanager = new \core\update\code_manager();
2147 }
2148
2149 return $this->codemanager;
2150 }
35f2b674
DM
2151
2152 /**
2153 * Returns a client for https://download.moodle.org/api/
2154 *
2155 * @return \core\update\api
2156 */
2157 protected function get_update_api_client() {
2158
2159 if ($this->updateapiclient === null) {
2160 $this->updateapiclient = \core\update\api::client();
2161 }
2162
2163 return $this->updateapiclient;
2164 }
e87214bd 2165}