MDL-41437 fix and cleanup filter uninstall
[moodle.git] / lib / pluginlib.php
CommitLineData
b9934a17 1<?php
b6ad8594 2
b9934a17
DM
3// This file is part of Moodle - http://moodle.org/
4//
5// Moodle is free software: you can redistribute it and/or modify
6// it under the terms of the GNU General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// Moodle is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU General Public License for more details.
14//
15// You should have received a copy of the GNU General Public License
16// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17
18/**
19 * Defines classes used for plugins management
20 *
21 * This library provides a unified interface to various plugin types in
22 * Moodle. It is mainly used by the plugins management admin page and the
23 * plugins check page during the upgrade.
24 *
25 * @package core
26 * @subpackage admin
27 * @copyright 2011 David Mudrak <david@moodle.com>
28 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
29 */
30
31defined('MOODLE_INTERNAL') || die();
32
33/**
34 * Singleton class providing general plugins management functionality
35 */
36class plugin_manager {
37
38 /** the plugin is shipped with standard Moodle distribution */
39 const PLUGIN_SOURCE_STANDARD = 'std';
40 /** the plugin is added extension */
41 const PLUGIN_SOURCE_EXTENSION = 'ext';
42
43 /** the plugin uses neither database nor capabilities, no versions */
44 const PLUGIN_STATUS_NODB = 'nodb';
45 /** the plugin is up-to-date */
46 const PLUGIN_STATUS_UPTODATE = 'uptodate';
47 /** the plugin is about to be installed */
48 const PLUGIN_STATUS_NEW = 'new';
49 /** the plugin is about to be upgraded */
50 const PLUGIN_STATUS_UPGRADE = 'upgrade';
ec8935f5
PS
51 /** the standard plugin is about to be deleted */
52 const PLUGIN_STATUS_DELETE = 'delete';
b9934a17
DM
53 /** the version at the disk is lower than the one already installed */
54 const PLUGIN_STATUS_DOWNGRADE = 'downgrade';
55 /** the plugin is installed but missing from disk */
56 const PLUGIN_STATUS_MISSING = 'missing';
57
58 /** @var plugin_manager holds the singleton instance */
59 protected static $singletoninstance;
60 /** @var array of raw plugins information */
61 protected $pluginsinfo = null;
62 /** @var array of raw subplugins information */
63 protected $subpluginsinfo = null;
bde002b8
PS
64 /** @var array list of installed plugins $name=>$version */
65 protected $installedplugins = null;
66 /** @var array list of all enabled plugins $name=>$name */
67 protected $enabledplugins = null;
68 /** @var array list of all enabled plugins $name=>$diskversion */
69 protected $presentplugins = null;
b9934a17
DM
70
71 /**
72 * Direct initiation not allowed, use the factory method {@link self::instance()}
b9934a17
DM
73 */
74 protected function __construct() {
b9934a17
DM
75 }
76
77 /**
78 * Sorry, this is singleton
79 */
80 protected function __clone() {
81 }
82
83 /**
84 * Factory method for this class
85 *
86 * @return plugin_manager the singleton instance
87 */
88 public static function instance() {
b9934a17
DM
89 if (is_null(self::$singletoninstance)) {
90 self::$singletoninstance = new self();
91 }
92 return self::$singletoninstance;
93 }
94
98547432 95 /**
bde002b8 96 * Reset all caches.
98547432
97 * @param bool $phpunitreset
98 */
99 public static function reset_caches($phpunitreset = false) {
100 if ($phpunitreset) {
101 self::$singletoninstance = null;
bde002b8
PS
102 } else {
103 if (self::$singletoninstance) {
104 self::$singletoninstance->pluginsinfo = null;
105 self::$singletoninstance->subpluginsinfo = null;
106 self::$singletoninstance->installedplugins = null;
107 self::$singletoninstance->enabledplugins = null;
108 self::$singletoninstance->presentplugins = null;
109 }
98547432 110 }
bde002b8
PS
111 $cache = cache::make('core', 'plugin_manager');
112 $cache->purge();
98547432
113 }
114
ce1a0d3c 115 /**
46f6f7f2 116 * Returns the result of {@link core_component::get_plugin_types()} ordered for humans
ce1a0d3c
DM
117 *
118 * @see self::reorder_plugin_types()
ce1a0d3c
DM
119 * @return array (string)name => (string)location
120 */
bde002b8
PS
121 public function get_plugin_types() {
122 if (func_num_args() > 0) {
123 if (!func_get_arg(0)) {
124 throw coding_exception('plugin_manager->get_plugin_types() does not support relative paths.');
125 }
126 }
127 return $this->reorder_plugin_types(core_component::get_plugin_types());
ce1a0d3c
DM
128 }
129
d7d48b40 130 /**
bde002b8
PS
131 * Load list of installed plugins,
132 * always call before using $this->installedplugins.
d7d48b40 133 *
bde002b8
PS
134 * This method is caching results for all plugins.
135 */
136 protected function load_installed_plugins() {
137 global $DB, $CFG;
138
139 if ($this->installedplugins) {
140 return;
141 }
142
143 if (empty($CFG->version)) {
144 // Nothing installed yet.
145 $this->installedplugins = array();
146 return;
147 }
148
149 $cache = cache::make('core', 'plugin_manager');
150 $installed = $cache->get('installed');
151
152 if (is_array($installed)) {
153 $this->installedplugins = $installed;
154 return;
155 }
156
157 $this->installedplugins = array();
158
159 if ($CFG->version < 2013092001.02) {
160 // We did not upgrade the database yet.
161 $modules = $DB->get_records('modules', array(), 'name ASC', 'id, name, version');
162 foreach ($modules as $module) {
163 $this->installedplugins['mod'][$module->name] = $module->version;
164 }
165 $blocks = $DB->get_records('block', array(), 'name ASC', 'id, name, version');
166 foreach ($blocks as $block) {
167 $this->installedplugins['block'][$block->name] = $block->version;
168 }
169 }
170
171 $versions = $DB->get_records('config_plugins', array('name'=>'version'));
172 foreach ($versions as $version) {
173 $parts = explode('_', $version->plugin, 2);
174 if (!isset($parts[1])) {
175 // Invalid component, there must be at least one "_".
176 continue;
177 }
178 // Do not verify here if plugin type and name are valid.
179 $this->installedplugins[$parts[0]][$parts[1]] = $version->value;
180 }
181
182 foreach ($this->installedplugins as $key => $value) {
183 ksort($this->installedplugins[$key]);
184 }
185
186 $cache->set('installed', $this->installedplugins);
187 }
188
189 /**
190 * Return list of installed plugins of given type.
191 * @param string $type
192 * @return array $name=>$version
193 */
194 public function get_installed_plugins($type) {
195 $this->load_installed_plugins();
196 if (isset($this->installedplugins[$type])) {
197 return $this->installedplugins[$type];
198 }
199 return array();
200 }
201
202 /**
203 * Load list of all enabled plugins,
204 * call before using $this->enabledplugins.
d7d48b40 205 *
bde002b8 206 * This method is caching results from individual plugin info classes.
d7d48b40 207 */
bde002b8
PS
208 protected function load_enabled_plugins() {
209 global $CFG;
d7d48b40 210
bde002b8
PS
211 if ($this->enabledplugins) {
212 return;
213 }
d7d48b40 214
bde002b8
PS
215 if (empty($CFG->version)) {
216 $this->enabledplugins = array();
217 return;
d7d48b40
DM
218 }
219
bde002b8
PS
220 $cache = cache::make('core', 'plugin_manager');
221 $enabled = $cache->get('enabled');
222
223 if (is_array($enabled)) {
224 $this->enabledplugins = $enabled;
225 return;
226 }
227
228 $this->enabledplugins = array();
229
230 require_once($CFG->libdir.'/adminlib.php');
231
232 $plugintypes = core_component::get_plugin_types();
233 foreach ($plugintypes as $plugintype => $fulldir) {
234 // Hack: include mod and editor subplugin management classes first,
235 // the adminlib.php is supposed to contain extra admin settings too.
236 $plugininfoclass = 'plugininfo_' . $plugintype;
237 if (!class_exists($plugininfoclass) and file_exists("$fulldir/adminlib.php")) {
238 include_once("$fulldir/adminlib.php");
239 }
240 if (class_exists($plugininfoclass)) {
241 $enabled = $plugininfoclass::get_enabled_plugins();
242 if (!is_array($enabled)) {
243 continue;
244 }
245 $this->enabledplugins[$plugintype] = $enabled;
246 }
247 }
248
249 $cache->set('enabled', $this->enabledplugins);
250 }
251
252 /**
253 * Get list of enabled plugins of given type,
254 * the result may contain missing plugins.
255 *
256 * @param string $type
257 * @return array|null list of enabled plugins of this type, null if unknown
258 */
259 public function get_enabled_plugins($type) {
260 $this->load_enabled_plugins();
261 if (isset($this->enabledplugins[$type])) {
262 return $this->enabledplugins[$type];
263 }
264 return null;
265 }
266
267 /**
268 * Load list of all present plugins - call before using $this->presentplugins.
269 */
270 protected function load_present_plugins() {
271 if ($this->presentplugins) {
272 return;
273 }
274
275 $cache = cache::make('core', 'plugin_manager');
276 $present = $cache->get('present');
277
278 if (is_array($present)) {
279 $this->presentplugins = $present;
280 return;
281 }
282
283 $this->presentplugins = array();
284
285 $plugintypes = core_component::get_plugin_types();
286 foreach ($plugintypes as $type => $typedir) {
287 $plugs = core_component::get_plugin_list($type);
288 foreach ($plugs as $plug => $fullplug) {
289 $plugin = new stdClass();
290 $plugin->version = null;
291 $module = $plugin;
292 @include($fullplug.'/version.php');
293 $this->presentplugins[$type][$plug] = $plugin;
294 }
295 }
296
297 $cache->set('present', $this->presentplugins);
298 }
299
300 /**
301 * Get list of present plugins of given type.
302 *
303 * @param string $type
304 * @return array|null list of presnet plugins $name=>$diskversion, null if unknown
305 */
306 public function get_present_plugins($type) {
307 $this->load_present_plugins();
308 if (isset($this->presentplugins[$type])) {
309 return $this->presentplugins[$type];
310 }
311 return null;
d7d48b40
DM
312 }
313
b9934a17
DM
314 /**
315 * Returns a tree of known plugins and information about them
316 *
e61aaece
TH
317 * @return array 2D array. The first keys are plugin type names (e.g. qtype);
318 * the second keys are the plugin local name (e.g. multichoice); and
b6ad8594 319 * the values are the corresponding objects extending {@link plugininfo_base}
b9934a17 320 */
bde002b8 321 public function get_plugins() {
7716057f 322 global $CFG;
b9934a17 323
bde002b8
PS
324 if (is_array($this->pluginsinfo)) {
325 return $this->pluginsinfo;
326 }
327
328 $this->pluginsinfo = array();
329
330 // Hack: include mod and editor subplugin management classes first,
331 // the adminlib.php is supposed to contain extra admin settings too.
332 require_once($CFG->libdir.'/adminlib.php');
333 foreach (core_component::get_plugin_types_with_subplugins() as $type => $ignored) {
334 foreach (core_component::get_plugin_list($type) as $dir) {
335 if (file_exists("$dir/adminlib.php")) {
336 include_once("$dir/adminlib.php");
7d59d8da
PS
337 }
338 }
bde002b8
PS
339 }
340 $plugintypes = $this->get_plugin_types();
341 foreach ($plugintypes as $plugintype => $plugintyperootdir) {
342 if (in_array($plugintype, array('base', 'general'))) {
343 throw new coding_exception('Illegal usage of reserved word for plugin type');
344 }
345 if (class_exists('plugininfo_' . $plugintype)) {
346 $plugintypeclass = 'plugininfo_' . $plugintype;
347 } else {
348 $plugintypeclass = 'plugininfo_general';
349 }
350 if (!in_array('plugininfo_base', class_parents($plugintypeclass))) {
351 throw new coding_exception('Class ' . $plugintypeclass . ' must extend plugininfo_base');
b9934a17 352 }
bde002b8
PS
353 $plugins = $plugintypeclass::get_plugins($plugintype, $plugintyperootdir, $plugintypeclass);
354 $this->pluginsinfo[$plugintype] = $plugins;
355 }
dd119e21 356
bde002b8
PS
357 if (empty($CFG->disableupdatenotifications) and !during_initial_install()) {
358 // append the information about available updates provided by {@link available_update_checker()}
359 $provider = available_update_checker::instance();
360 foreach ($this->pluginsinfo as $plugintype => $plugins) {
361 foreach ($plugins as $plugininfoholder) {
362 $plugininfoholder->check_available_updates($provider);
dd119e21
DM
363 }
364 }
b9934a17
DM
365 }
366
367 return $this->pluginsinfo;
368 }
369
d7d48b40 370 /**
bde002b8
PS
371 * Returns list of known plugins of the given type.
372 *
373 * This method returns the subset of the tree returned by {@link self::get_plugins()}.
374 * If the given type is not known, empty array is returned.
375 *
376 * @param string $type plugin type, e.g. 'mod' or 'workshopallocation'
377 * @return array (string)plugin name (e.g. 'workshop') => corresponding subclass of {@link plugininfo_base}
378 */
379 public function get_plugins_of_type($type) {
380
381 $plugins = $this->get_plugins();
382
383 if (!isset($plugins[$type])) {
384 return array();
385 }
386
387 return $plugins[$type];
388 }
389
390 /**
391 * Returns list of all known subplugins of the given plugin.
d7d48b40
DM
392 *
393 * For plugins that do not provide subplugins (i.e. there is no support for it),
394 * empty array is returned.
395 *
396 * @param string $component full component name, e.g. 'mod_workshop'
d7d48b40
DM
397 * @return array (string) component name (e.g. 'workshopallocation_random') => subclass of {@link plugininfo_base}
398 */
bde002b8 399 public function get_subplugins_of_plugin($component) {
d7d48b40 400
bde002b8 401 $pluginfo = $this->get_plugin_info($component);
d7d48b40
DM
402
403 if (is_null($pluginfo)) {
404 return array();
405 }
406
bde002b8 407 $subplugins = $this->get_subplugins();
d7d48b40
DM
408
409 if (!isset($subplugins[$pluginfo->component])) {
410 return array();
411 }
412
413 $list = array();
414
415 foreach ($subplugins[$pluginfo->component] as $subdata) {
416 foreach ($this->get_plugins_of_type($subdata->type) as $subpluginfo) {
417 $list[$subpluginfo->component] = $subpluginfo;
418 }
419 }
420
421 return $list;
422 }
423
b9934a17 424 /**
0242bdc7
TH
425 * Returns list of plugins that define their subplugins and the information
426 * about them from the db/subplugins.php file.
b9934a17 427 *
0242bdc7
TH
428 * @return array with keys like 'mod_quiz', and values the data from the
429 * corresponding db/subplugins.php file.
b9934a17 430 */
bde002b8
PS
431 public function get_subplugins() {
432
433 if (is_array($this->subpluginsinfo)) {
434 return $this->subpluginsinfo;
435 }
436
437 $this->subpluginsinfo = array();
438 foreach (core_component::get_plugin_types_with_subplugins() as $type => $ignored) {
439 foreach (core_component::get_plugin_list($type) as $component => $ownerdir) {
440 $componentsubplugins = array();
441 if (file_exists($ownerdir . '/db/subplugins.php')) {
442 $subplugins = array();
443 include($ownerdir . '/db/subplugins.php');
444 foreach ($subplugins as $subplugintype => $subplugintyperootdir) {
445 $subplugin = new stdClass();
446 $subplugin->type = $subplugintype;
447 $subplugin->typerootdir = $subplugintyperootdir;
448 $componentsubplugins[$subplugintype] = $subplugin;
b9934a17 449 }
bde002b8 450 $this->subpluginsinfo[$type . '_' . $component] = $componentsubplugins;
b9934a17
DM
451 }
452 }
453 }
454
455 return $this->subpluginsinfo;
456 }
457
458 /**
459 * Returns the name of the plugin that defines the given subplugin type
460 *
461 * If the given subplugin type is not actually a subplugin, returns false.
462 *
463 * @param string $subplugintype the name of subplugin type, eg. workshopform or quiz
464 * @return false|string the name of the parent plugin, eg. mod_workshop
465 */
466 public function get_parent_of_subplugin($subplugintype) {
467
468 $parent = false;
469 foreach ($this->get_subplugins() as $pluginname => $subplugintypes) {
470 if (isset($subplugintypes[$subplugintype])) {
471 $parent = $pluginname;
472 break;
473 }
474 }
475
476 return $parent;
477 }
478
479 /**
480 * Returns a localized name of a given plugin
481 *
7a46a55d 482 * @param string $component name of the plugin, eg mod_workshop or auth_ldap
b9934a17
DM
483 * @return string
484 */
7a46a55d
DM
485 public function plugin_name($component) {
486
487 $pluginfo = $this->get_plugin_info($component);
488
489 if (is_null($pluginfo)) {
490 throw new moodle_exception('err_unknown_plugin', 'core_plugin', '', array('plugin' => $component));
491 }
492
493 return $pluginfo->displayname;
b9934a17
DM
494 }
495
b8efcb92
DM
496 /**
497 * Returns a localized name of a plugin typed in singular form
498 *
499 * Most plugin types define their names in core_plugin lang file. In case of subplugins,
500 * we try to ask the parent plugin for the name. In the worst case, we will return
501 * the value of the passed $type parameter.
502 *
503 * @param string $type the type of the plugin, e.g. mod or workshopform
504 * @return string
505 */
506 public function plugintype_name($type) {
507
508 if (get_string_manager()->string_exists('type_' . $type, 'core_plugin')) {
509 // for most plugin types, their names are defined in core_plugin lang file
510 return get_string('type_' . $type, 'core_plugin');
511
512 } else if ($parent = $this->get_parent_of_subplugin($type)) {
513 // if this is a subplugin, try to ask the parent plugin for the name
514 if (get_string_manager()->string_exists('subplugintype_' . $type, $parent)) {
515 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type, $parent);
516 } else {
517 return $this->plugin_name($parent) . ' / ' . $type;
518 }
519
520 } else {
521 return $type;
522 }
523 }
524
b9934a17
DM
525 /**
526 * Returns a localized name of a plugin type in plural form
527 *
528 * Most plugin types define their names in core_plugin lang file. In case of subplugins,
529 * we try to ask the parent plugin for the name. In the worst case, we will return
530 * the value of the passed $type parameter.
531 *
532 * @param string $type the type of the plugin, e.g. mod or workshopform
533 * @return string
534 */
535 public function plugintype_name_plural($type) {
536
537 if (get_string_manager()->string_exists('type_' . $type . '_plural', 'core_plugin')) {
538 // for most plugin types, their names are defined in core_plugin lang file
539 return get_string('type_' . $type . '_plural', 'core_plugin');
540
541 } else if ($parent = $this->get_parent_of_subplugin($type)) {
542 // if this is a subplugin, try to ask the parent plugin for the name
543 if (get_string_manager()->string_exists('subplugintype_' . $type . '_plural', $parent)) {
544 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type . '_plural', $parent);
545 } else {
546 return $this->plugin_name($parent) . ' / ' . $type;
547 }
548
549 } else {
550 return $type;
551 }
552 }
553
e61aaece 554 /**
d7d48b40
DM
555 * Returns information about the known plugin, or null
556 *
e61aaece 557 * @param string $component frankenstyle component name.
b6ad8594 558 * @return plugininfo_base|null the corresponding plugin information.
e61aaece 559 */
bde002b8
PS
560 public function get_plugin_info($component) {
561 list($type, $name) = core_component::normalize_component($component);
562 $plugins = $this->get_plugins();
e61aaece
TH
563 if (isset($plugins[$type][$name])) {
564 return $plugins[$type][$name];
565 } else {
566 return null;
567 }
568 }
569
436d9447
DM
570 /**
571 * Check to see if the current version of the plugin seems to be a checkout of an external repository.
572 *
573 * @see available_update_deployer::plugin_external_source()
574 * @param string $component frankenstyle component name
575 * @return false|string
576 */
577 public function plugin_external_source($component) {
578
579 $plugininfo = $this->get_plugin_info($component);
580
581 if (is_null($plugininfo)) {
582 return false;
583 }
584
585 $pluginroot = $plugininfo->rootdir;
586
587 if (is_dir($pluginroot.'/.git')) {
588 return 'git';
589 }
590
591 if (is_dir($pluginroot.'/CVS')) {
592 return 'cvs';
593 }
594
595 if (is_dir($pluginroot.'/.svn')) {
596 return 'svn';
597 }
598
599 return false;
600 }
601
828788f0 602 /**
b6ad8594 603 * Get a list of any other plugins that require this one.
828788f0
TH
604 * @param string $component frankenstyle component name.
605 * @return array of frankensyle component names that require this one.
606 */
607 public function other_plugins_that_require($component) {
608 $others = array();
609 foreach ($this->get_plugins() as $type => $plugins) {
610 foreach ($plugins as $plugin) {
611 $required = $plugin->get_other_required_plugins();
612 if (isset($required[$component])) {
613 $others[] = $plugin->component;
614 }
615 }
616 }
617 return $others;
618 }
619
e61aaece 620 /**
777781d1
TH
621 * Check a dependencies list against the list of installed plugins.
622 * @param array $dependencies compenent name to required version or ANY_VERSION.
623 * @return bool true if all the dependencies are satisfied.
e61aaece 624 */
777781d1
TH
625 public function are_dependencies_satisfied($dependencies) {
626 foreach ($dependencies as $component => $requiredversion) {
e61aaece
TH
627 $otherplugin = $this->get_plugin_info($component);
628 if (is_null($otherplugin)) {
0242bdc7
TH
629 return false;
630 }
631
3f123d92 632 if ($requiredversion != ANY_VERSION and $otherplugin->versiondisk < $requiredversion) {
0242bdc7
TH
633 return false;
634 }
635 }
636
637 return true;
638 }
639
faadd326 640 /**
927cb511
DM
641 * Checks all dependencies for all installed plugins
642 *
643 * This is used by install and upgrade. The array passed by reference as the second
644 * argument is populated with the list of plugins that have failed dependencies (note that
645 * a single plugin can appear multiple times in the $failedplugins).
646 *
faadd326 647 * @param int $moodleversion the version from version.php.
927cb511 648 * @param array $failedplugins to return the list of plugins with non-satisfied dependencies
777781d1 649 * @return bool true if all the dependencies are satisfied for all plugins.
faadd326 650 */
927cb511
DM
651 public function all_plugins_ok($moodleversion, &$failedplugins = array()) {
652
653 $return = true;
faadd326
TH
654 foreach ($this->get_plugins() as $type => $plugins) {
655 foreach ($plugins as $plugin) {
656
3a2300f5 657 if (!$plugin->is_core_dependency_satisfied($moodleversion)) {
927cb511
DM
658 $return = false;
659 $failedplugins[] = $plugin->component;
faadd326
TH
660 }
661
777781d1 662 if (!$this->are_dependencies_satisfied($plugin->get_other_required_plugins())) {
927cb511
DM
663 $return = false;
664 $failedplugins[] = $plugin->component;
faadd326
TH
665 }
666 }
667 }
668
927cb511 669 return $return;
faadd326
TH
670 }
671
73658371
DM
672 /**
673 * Is it possible to uninstall the given plugin?
674 *
675 * False is returned if the plugininfo subclass declares the uninstall should
676 * not be allowed via {@link plugininfo_base::is_uninstall_allowed()} or if the
677 * core vetoes it (e.g. becase the plugin or some of its subplugins is required
678 * by some other installed plugin).
679 *
680 * @param string $component full frankenstyle name, e.g. mod_foobar
681 * @return bool
682 */
683 public function can_uninstall_plugin($component) {
684
685 $pluginfo = $this->get_plugin_info($component);
686
687 if (is_null($pluginfo)) {
688 return false;
689 }
690
ccc6c15f 691 if (!$this->common_uninstall_check($pluginfo)) {
73658371
DM
692 return false;
693 }
694
ccc6c15f
DM
695 // If it has subplugins, check they can be uninstalled too.
696 $subplugins = $this->get_subplugins_of_plugin($pluginfo->component);
697 foreach ($subplugins as $subpluginfo) {
698 if (!$this->common_uninstall_check($subpluginfo)) {
73658371
DM
699 return false;
700 }
ccc6c15f
DM
701 // Check if there are some other plugins requiring this subplugin
702 // (but the parent and siblings).
703 foreach ($this->other_plugins_that_require($subpluginfo->component) as $requiresme) {
704 $ismyparent = ($pluginfo->component === $requiresme);
705 $ismysibling = in_array($requiresme, array_keys($subplugins));
706 if (!$ismyparent and !$ismysibling) {
707 return false;
708 }
709 }
73658371
DM
710 }
711
ccc6c15f
DM
712 // Check if there are some other plugins requiring this plugin
713 // (but its subplugins).
73658371 714 foreach ($this->other_plugins_that_require($pluginfo->component) as $requiresme) {
ccc6c15f
DM
715 $ismysubplugin = in_array($requiresme, array_keys($subplugins));
716 if (!$ismysubplugin) {
73658371
DM
717 return false;
718 }
719 }
720
73658371
DM
721 return true;
722 }
723
2f87bb03
PS
724 /**
725 * Returns uninstall URL if exists.
726 *
727 * @param string $component
728 * @return moodle_url uninstall URL, null if uninstall not supported
729 */
730 public function get_uninstall_url($component) {
731 if (!$this->can_uninstall_plugin($component)) {
732 return null;
733 }
734
735 $pluginfo = $this->get_plugin_info($component);
736
737 if (is_null($pluginfo)) {
738 return null;
739 }
740
741 return $pluginfo->get_uninstall_url();
742 }
743
436d9447
DM
744 /**
745 * Uninstall the given plugin.
746 *
747 * Automatically cleans-up all remaining configuration data, log records, events,
748 * files from the file pool etc.
749 *
750 * In the future, the functionality of {@link uninstall_plugin()} function may be moved
751 * into this method and all the code should be refactored to use it. At the moment, we
752 * mimic this future behaviour by wrapping that function call.
753 *
754 * @param string $component
3ca1b546 755 * @param progress_trace $progress traces the process
436d9447
DM
756 * @return bool true on success, false on errors/problems
757 */
3ca1b546 758 public function uninstall_plugin($component, progress_trace $progress) {
436d9447
DM
759
760 $pluginfo = $this->get_plugin_info($component);
761
762 if (is_null($pluginfo)) {
763 return false;
764 }
765
3ca1b546
DM
766 // Give the pluginfo class a chance to execute some steps.
767 $result = $pluginfo->uninstall($progress);
436d9447
DM
768 if (!$result) {
769 return false;
770 }
771
772 // Call the legacy core function to uninstall the plugin.
773 ob_start();
774 uninstall_plugin($pluginfo->type, $pluginfo->name);
3ca1b546 775 $progress->output(ob_get_clean());
436d9447
DM
776
777 return true;
778 }
779
5344ddd1
DM
780 /**
781 * Checks if there are some plugins with a known available update
782 *
783 * @return bool true if there is at least one available update
784 */
785 public function some_plugins_updatable() {
786 foreach ($this->get_plugins() as $type => $plugins) {
787 foreach ($plugins as $plugin) {
788 if ($plugin->available_updates()) {
789 return true;
790 }
791 }
792 }
793
794 return false;
795 }
796
436d9447
DM
797 /**
798 * Check to see if the given plugin folder can be removed by the web server process.
799 *
436d9447
DM
800 * @param string $component full frankenstyle component
801 * @return bool
802 */
803 public function is_plugin_folder_removable($component) {
804
805 $pluginfo = $this->get_plugin_info($component);
806
807 if (is_null($pluginfo)) {
808 return false;
809 }
810
436d9447
DM
811 // To be able to remove the plugin folder, its parent must be writable, too.
812 if (!is_writable(dirname($pluginfo->rootdir))) {
813 return false;
814 }
815
816 // Check that the folder and all its content is writable (thence removable).
817 return $this->is_directory_removable($pluginfo->rootdir);
818 }
819
ec8935f5
PS
820 /**
821 * Defines a list of all plugins that were originally shipped in the standard Moodle distribution,
822 * but are not anymore and are deleted during upgrades.
823 *
824 * The main purpose of this list is to hide missing plugins during upgrade.
825 *
826 * @param string $type plugin type
827 * @param string $name plugin name
828 * @return bool
829 */
830 public static function is_deleted_standard_plugin($type, $name) {
b8a6f26e
DM
831
832 // Example of the array structure:
833 // $plugins = array(
834 // 'block' => array('admin', 'admin_tree'),
835 // 'mod' => array('assignment'),
836 // );
837 // Do not include plugins that were removed during upgrades to versions that are
838 // not supported as source versions for upgrade any more. For example, at MOODLE_23_STABLE
839 // branch, listed should be no plugins that were removed at 1.9.x - 2.1.x versions as
840 // Moodle 2.3 supports upgrades from 2.2.x only.
841 $plugins = array(
4f315786 842 'qformat' => array('blackboard'),
fdb0dfe7 843 'enrol' => array('authorize'),
bde002b8 844 'tool' => array('bloglevelupgrade'),
ec8935f5
PS
845 );
846
847 if (!isset($plugins[$type])) {
848 return false;
849 }
850 return in_array($name, $plugins[$type]);
851 }
852
b9934a17
DM
853 /**
854 * Defines a white list of all plugins shipped in the standard Moodle distribution
855 *
ec8935f5 856 * @param string $type
b9934a17
DM
857 * @return false|array array of standard plugins or false if the type is unknown
858 */
859 public static function standard_plugins_list($type) {
5989677e 860
b8a6f26e 861 $standard_plugins = array(
b9934a17
DM
862
863 'assignment' => array(
864 'offline', 'online', 'upload', 'uploadsingle'
865 ),
866
1619a38b
DP
867 'assignsubmission' => array(
868 'comments', 'file', 'onlinetext'
869 ),
870
871 'assignfeedback' => array(
fcae4a0c 872 'comments', 'file', 'offline'
1619a38b
DP
873 ),
874
c90641fa
DW
875 'atto' => array(
876 'bold', 'clear', 'html', 'image', 'indent', 'italic', 'link',
877 'media', 'orderedlist', 'outdent', 'strike', 'title',
878 'underline', 'unlink', 'unorderedlist'
879 ),
880
b9934a17
DM
881 'auth' => array(
882 'cas', 'db', 'email', 'fc', 'imap', 'ldap', 'manual', 'mnet',
883 'nntp', 'nologin', 'none', 'pam', 'pop3', 'radius',
884 'shibboleth', 'webservice'
885 ),
886
887 'block' => array(
2188a697 888 'activity_modules', 'admin_bookmarks', 'badges', 'blog_menu',
b9934a17
DM
889 'blog_recent', 'blog_tags', 'calendar_month',
890 'calendar_upcoming', 'comments', 'community',
891 'completionstatus', 'course_list', 'course_overview',
892 'course_summary', 'feedback', 'glossary_random', 'html',
893 'login', 'mentees', 'messages', 'mnet_hosts', 'myprofile',
894 'navigation', 'news_items', 'online_users', 'participants',
895 'private_files', 'quiz_results', 'recent_activity',
f68cef22 896 'rss_client', 'search_forums', 'section_links',
b9934a17
DM
897 'selfcompletion', 'settings', 'site_main_menu',
898 'social_activities', 'tag_flickr', 'tag_youtube', 'tags'
899 ),
900
f7e6dd4d
EL
901 'booktool' => array(
902 'exportimscp', 'importhtml', 'print'
903 ),
904
fd59389c
SH
905 'cachelock' => array(
906 'file'
907 ),
908
909 'cachestore' => array(
910 'file', 'memcache', 'memcached', 'mongodb', 'session', 'static'
911 ),
912
5989677e
MN
913 'calendartype' => array(
914 'gregorian'
915 ),
916
b9934a17 917 'coursereport' => array(
a2a444ab 918 //deprecated!
b9934a17
DM
919 ),
920
921 'datafield' => array(
922 'checkbox', 'date', 'file', 'latlong', 'menu', 'multimenu',
923 'number', 'picture', 'radiobutton', 'text', 'textarea', 'url'
924 ),
925
926 'datapreset' => array(
927 'imagegallery'
928 ),
929
930 'editor' => array(
c90641fa 931 'textarea', 'tinymce', 'atto'
b9934a17
DM
932 ),
933
934 'enrol' => array(
317638a3 935 'category', 'cohort', 'database', 'flatfile',
b9934a17
DM
936 'guest', 'imsenterprise', 'ldap', 'manual', 'meta', 'mnet',
937 'paypal', 'self'
938 ),
939
940 'filter' => array(
941 'activitynames', 'algebra', 'censor', 'emailprotect',
942 'emoticon', 'mediaplugin', 'multilang', 'tex', 'tidy',
87783982 943 'urltolink', 'data', 'glossary'
b9934a17
DM
944 ),
945
946 'format' => array(
3d1808c6 947 'singleactivity', 'social', 'topics', 'weeks'
b9934a17
DM
948 ),
949
950 'gradeexport' => array(
951 'ods', 'txt', 'xls', 'xml'
952 ),
953
954 'gradeimport' => array(
955 'csv', 'xml'
956 ),
957
958 'gradereport' => array(
959 'grader', 'outcomes', 'overview', 'user'
960 ),
961
f59f488a 962 'gradingform' => array(
77143217 963 'rubric', 'guide'
f59f488a
DM
964 ),
965
b9934a17
DM
966 'local' => array(
967 ),
968
969 'message' => array(
970 'email', 'jabber', 'popup'
971 ),
972
973 'mnetservice' => array(
974 'enrol'
975 ),
976
977 'mod' => array(
f7e6dd4d 978 'assign', 'assignment', 'book', 'chat', 'choice', 'data', 'feedback', 'folder',
7fdee5b6 979 'forum', 'glossary', 'imscp', 'label', 'lesson', 'lti', 'page',
b9934a17
DM
980 'quiz', 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop'
981 ),
982
983 'plagiarism' => array(
984 ),
985
986 'portfolio' => array(
987 'boxnet', 'download', 'flickr', 'googledocs', 'mahara', 'picasa'
988 ),
989
990 'profilefield' => array(
991 'checkbox', 'datetime', 'menu', 'text', 'textarea'
992 ),
993
d1c77ac3
DM
994 'qbehaviour' => array(
995 'adaptive', 'adaptivenopenalty', 'deferredcbm',
996 'deferredfeedback', 'immediatecbm', 'immediatefeedback',
997 'informationitem', 'interactive', 'interactivecountback',
998 'manualgraded', 'missing'
999 ),
1000
b9934a17 1001 'qformat' => array(
4f315786 1002 'aiken', 'blackboard_six', 'examview', 'gift',
2dc54611 1003 'learnwise', 'missingword', 'multianswer', 'webct',
b9934a17
DM
1004 'xhtml', 'xml'
1005 ),
1006
1007 'qtype' => array(
1008 'calculated', 'calculatedmulti', 'calculatedsimple',
1009 'description', 'essay', 'match', 'missingtype', 'multianswer',
1010 'multichoice', 'numerical', 'random', 'randomsamatch',
1011 'shortanswer', 'truefalse'
1012 ),
1013
1014 'quiz' => array(
1015 'grading', 'overview', 'responses', 'statistics'
1016 ),
1017
c999d841
TH
1018 'quizaccess' => array(
1019 'delaybetweenattempts', 'ipaddress', 'numattempts', 'openclosedate',
1020 'password', 'safebrowser', 'securewindow', 'timelimit'
1021 ),
1022
b9934a17 1023 'report' => array(
13fdaaac 1024 'backups', 'completion', 'configlog', 'courseoverview',
5c9e8898 1025 'log', 'loglive', 'outline', 'participation', 'progress', 'questioninstances', 'security', 'stats', 'performance'
b9934a17
DM
1026 ),
1027
1028 'repository' => array(
1d945573 1029 'alfresco', 'areafiles', 'boxnet', 'coursefiles', 'dropbox', 'equella', 'filesystem',
b9934a17 1030 'flickr', 'flickr_public', 'googledocs', 'local', 'merlot',
562525dc 1031 'picasa', 'recent', 'skydrive', 's3', 'upload', 'url', 'user', 'webdav',
b9934a17
DM
1032 'wikimedia', 'youtube'
1033 ),
1034
99e86561 1035 'scormreport' => array(
8f1a0d21 1036 'basic',
e61a7137 1037 'interactions',
07e045b8
EL
1038 'graphs',
1039 'objectives'
99e86561
PS
1040 ),
1041
29e03690 1042 'tinymce' => array(
9e88661e
MG
1043 'ctrlhelp', 'dragmath', 'managefiles', 'moodleemoticon', 'moodleimage',
1044 'moodlemedia', 'moodlenolink', 'pdw', 'spellchecker', 'wrap'
29e03690
PS
1045 ),
1046
b9934a17 1047 'theme' => array(
4f100820 1048 'afterburner', 'anomaly', 'arialist', 'base', 'binarius', 'bootstrapbase',
e18597fa 1049 'boxxie', 'brick', 'canvas', 'clean', 'formal_white', 'formfactor',
98ca9e84 1050 'fusion', 'leatherbound', 'magazine', 'mymobile', 'nimble',
e18597fa 1051 'nonzero', 'overlay', 'serenity', 'sky_high', 'splash',
98ca9e84 1052 'standard', 'standardold'
b9934a17
DM
1053 ),
1054
11b24ce7 1055 'tool' => array(
2459758b
DM
1056 'assignmentupgrade', 'behat', 'capability', 'customlang',
1057 'dbtransfer', 'generator', 'health', 'innodb', 'installaddon',
1058 'langimport', 'multilangupgrade', 'phpunit', 'profiling',
1059 'qeupgradehelper', 'replace', 'spamcleaner', 'timezoneimport',
c122e784 1060 'unittest', 'uploadcourse', 'uploaduser', 'unsuproles', 'xmldb'
11b24ce7
PS
1061 ),
1062
b9934a17
DM
1063 'webservice' => array(
1064 'amf', 'rest', 'soap', 'xmlrpc'
1065 ),
1066
1067 'workshopallocation' => array(
98621280 1068 'manual', 'random', 'scheduled'
b9934a17
DM
1069 ),
1070
1071 'workshopeval' => array(
1072 'best'
1073 ),
1074
1075 'workshopform' => array(
1076 'accumulative', 'comments', 'numerrors', 'rubric'
1077 )
1078 );
1079
1080 if (isset($standard_plugins[$type])) {
1081 return $standard_plugins[$type];
b9934a17
DM
1082 } else {
1083 return false;
1084 }
1085 }
4ed26680
DM
1086
1087 /**
660c4d46 1088 * Reorders plugin types into a sequence to be displayed
4ed26680 1089 *
46f6f7f2 1090 * For technical reasons, plugin types returned by {@link core_component::get_plugin_types()} are
4ed26680
DM
1091 * in a certain order that does not need to fit the expected order for the display.
1092 * Particularly, activity modules should be displayed first as they represent the
1093 * real heart of Moodle. They should be followed by other plugin types that are
1094 * used to build the courses (as that is what one expects from LMS). After that,
1095 * other supportive plugin types follow.
1096 *
1097 * @param array $types associative array
1098 * @return array same array with altered order of items
1099 */
1100 protected function reorder_plugin_types(array $types) {
1101 $fix = array(
1102 'mod' => $types['mod'],
1103 'block' => $types['block'],
1104 'qtype' => $types['qtype'],
1105 'qbehaviour' => $types['qbehaviour'],
1106 'qformat' => $types['qformat'],
1107 'filter' => $types['filter'],
1108 'enrol' => $types['enrol'],
1109 );
1110 foreach ($types as $type => $path) {
1111 if (!isset($fix[$type])) {
1112 $fix[$type] = $path;
1113 }
1114 }
1115 return $fix;
1116 }
436d9447
DM
1117
1118 /**
1119 * Check if the given directory can be removed by the web server process.
1120 *
1121 * This recursively checks that the given directory and all its contents
1122 * it writable.
1123 *
1124 * @param string $fullpath
1125 * @return boolean
1126 */
1127 protected function is_directory_removable($fullpath) {
1128
1129 if (!is_writable($fullpath)) {
1130 return false;
1131 }
1132
1133 if (is_dir($fullpath)) {
1134 $handle = opendir($fullpath);
1135 } else {
1136 return false;
1137 }
1138
1139 $result = true;
1140
1141 while ($filename = readdir($handle)) {
1142
1143 if ($filename === '.' or $filename === '..') {
1144 continue;
1145 }
1146
1147 $subfilepath = $fullpath.'/'.$filename;
1148
1149 if (is_dir($subfilepath)) {
1150 $result = $result && $this->is_directory_removable($subfilepath);
1151
1152 } else {
1153 $result = $result && is_writable($subfilepath);
1154 }
1155 }
1156
1157 closedir($handle);
1158
1159 return $result;
1160 }
ccc6c15f
DM
1161
1162 /**
1163 * Helper method that implements common uninstall prerequisities
1164 *
1165 * @param plugininfo_base $pluginfo
1166 * @return bool
1167 */
1168 protected function common_uninstall_check(plugininfo_base $pluginfo) {
1169
1170 if (!$pluginfo->is_uninstall_allowed()) {
1171 // The plugin's plugininfo class declares it should not be uninstalled.
1172 return false;
1173 }
1174
badf4647
DM
1175 if ($pluginfo->get_status() === plugin_manager::PLUGIN_STATUS_NEW) {
1176 // The plugin is not installed. It should be either installed or removed from the disk.
1177 // Relying on this temporary state may be tricky.
1178 return false;
1179 }
1180
ccc6c15f
DM
1181 if (is_null($pluginfo->get_uninstall_url())) {
1182 // Backwards compatibility.
1183 debugging('plugininfo_base subclasses should use is_uninstall_allowed() instead of returning null in get_uninstall_url()',
1184 DEBUG_DEVELOPER);
1185 return false;
1186 }
1187
1188 return true;
1189 }
b9934a17
DM
1190}
1191
b9934a17 1192
b9934a17 1193/**
cd0bb55f 1194 * General exception thrown by the {@link available_update_checker} class
b9934a17 1195 */
cd0bb55f 1196class available_update_checker_exception extends moodle_exception {
b9934a17
DM
1197
1198 /**
cd0bb55f
DM
1199 * @param string $errorcode exception description identifier
1200 * @param mixed $debuginfo debugging data to display
1201 */
1202 public function __construct($errorcode, $debuginfo=null) {
1203 parent::__construct($errorcode, 'core_plugin', '', null, print_r($debuginfo, true));
1204 }
1205}
1206
1207
1208/**
1209 * Singleton class that handles checking for available updates
1210 */
1211class available_update_checker {
1212
1213 /** @var available_update_checker holds the singleton instance */
1214 protected static $singletoninstance;
7d8de6d8
DM
1215 /** @var null|int the timestamp of when the most recent response was fetched */
1216 protected $recentfetch = null;
1217 /** @var null|array the recent response from the update notification provider */
1218 protected $recentresponse = null;
55585f3a
DM
1219 /** @var null|string the numerical version of the local Moodle code */
1220 protected $currentversion = null;
4442cc80
DM
1221 /** @var null|string the release info of the local Moodle code */
1222 protected $currentrelease = null;
55585f3a
DM
1223 /** @var null|string branch of the local Moodle code */
1224 protected $currentbranch = null;
1225 /** @var array of (string)frankestyle => (string)version list of additional plugins deployed at this site */
1226 protected $currentplugins = array();
cd0bb55f
DM
1227
1228 /**
1229 * Direct initiation not allowed, use the factory method {@link self::instance()}
1230 */
1231 protected function __construct() {
cd0bb55f
DM
1232 }
1233
1234 /**
1235 * Sorry, this is singleton
1236 */
1237 protected function __clone() {
1238 }
1239
1240 /**
1241 * Factory method for this class
b9934a17 1242 *
cd0bb55f
DM
1243 * @return available_update_checker the singleton instance
1244 */
1245 public static function instance() {
1246 if (is_null(self::$singletoninstance)) {
1247 self::$singletoninstance = new self();
1248 }
1249 return self::$singletoninstance;
1250 }
1251
98547432
1252 /**
1253 * Reset any caches
1254 * @param bool $phpunitreset
1255 */
1256 public static function reset_caches($phpunitreset = false) {
1257 if ($phpunitreset) {
1258 self::$singletoninstance = null;
1259 }
1260 }
1261
cd0bb55f
DM
1262 /**
1263 * Returns the timestamp of the last execution of {@link fetch()}
b9934a17 1264 *
cd0bb55f 1265 * @return int|null null if it has never been executed or we don't known
b9934a17 1266 */
cd0bb55f 1267 public function get_last_timefetched() {
7d8de6d8
DM
1268
1269 $this->restore_response();
1270
1271 if (!empty($this->recentfetch)) {
1272 return $this->recentfetch;
1273
cd0bb55f 1274 } else {
7d8de6d8 1275 return null;
cd0bb55f
DM
1276 }
1277 }
b9934a17
DM
1278
1279 /**
cd0bb55f 1280 * Fetches the available update status from the remote site
b9934a17 1281 *
cd0bb55f 1282 * @throws available_update_checker_exception
b9934a17 1283 */
cd0bb55f 1284 public function fetch() {
7d8de6d8 1285 $response = $this->get_response();
cd0bb55f 1286 $this->validate_response($response);
7d8de6d8 1287 $this->store_response($response);
cd0bb55f 1288 }
b9934a17
DM
1289
1290 /**
cd0bb55f 1291 * Returns the available update information for the given component
b9934a17 1292 *
cd0bb55f 1293 * This method returns null if the most recent response does not contain any information
7d8de6d8
DM
1294 * about it. The returned structure is an array of available updates for the given
1295 * component. Each update info is an object with at least one property called
1296 * 'version'. Other possible properties are 'release', 'maturity', 'url' and 'downloadurl'.
cd0bb55f 1297 *
c6f008e7
DM
1298 * For the 'core' component, the method returns real updates only (those with higher version).
1299 * For all other components, the list of all known remote updates is returned and the caller
1300 * (usually the {@link plugin_manager}) is supposed to make the actual comparison of versions.
b9934a17 1301 *
cd0bb55f 1302 * @param string $component frankenstyle
c6f008e7
DM
1303 * @param array $options with supported keys 'minmaturity' and/or 'notifybuilds'
1304 * @return null|array null or array of available_update_info objects
b9934a17 1305 */
c6f008e7
DM
1306 public function get_update_info($component, array $options = array()) {
1307
1308 if (!isset($options['minmaturity'])) {
1309 $options['minmaturity'] = 0;
1310 }
1311
1312 if (!isset($options['notifybuilds'])) {
1313 $options['notifybuilds'] = false;
1314 }
1315
1316 if ($component == 'core') {
1317 $this->load_current_environment();
1318 }
cd0bb55f 1319
7d8de6d8 1320 $this->restore_response();
cd0bb55f 1321
c6f008e7
DM
1322 if (empty($this->recentresponse['updates'][$component])) {
1323 return null;
1324 }
1325
1326 $updates = array();
1327 foreach ($this->recentresponse['updates'][$component] as $info) {
1328 $update = new available_update_info($component, $info);
1329 if (isset($update->maturity) and ($update->maturity < $options['minmaturity'])) {
1330 continue;
7d8de6d8 1331 }
c6f008e7
DM
1332 if ($component == 'core') {
1333 if ($update->version <= $this->currentversion) {
1334 continue;
1335 }
4442cc80
DM
1336 if (empty($options['notifybuilds']) and $this->is_same_release($update->release)) {
1337 continue;
1338 }
c6f008e7
DM
1339 }
1340 $updates[] = $update;
1341 }
1342
1343 if (empty($updates)) {
cd0bb55f
DM
1344 return null;
1345 }
c6f008e7
DM
1346
1347 return $updates;
cd0bb55f 1348 }
b9934a17
DM
1349
1350 /**
be378880
DM
1351 * The method being run via cron.php
1352 */
1353 public function cron() {
1354 global $CFG;
1355
1356 if (!$this->cron_autocheck_enabled()) {
1357 $this->cron_mtrace('Automatic check for available updates not enabled, skipping.');
1358 return;
1359 }
1360
1361 $now = $this->cron_current_timestamp();
1362
1363 if ($this->cron_has_fresh_fetch($now)) {
1364 $this->cron_mtrace('Recently fetched info about available updates is still fresh enough, skipping.');
1365 return;
1366 }
1367
1368 if ($this->cron_has_outdated_fetch($now)) {
1369 $this->cron_mtrace('Outdated or missing info about available updates, forced fetching ... ', '');
1370 $this->cron_execute();
1371 return;
1372 }
1373
1374 $offset = $this->cron_execution_offset();
1375 $start = mktime(1, 0, 0, date('n', $now), date('j', $now), date('Y', $now)); // 01:00 AM today local time
1376 if ($now > $start + $offset) {
1377 $this->cron_mtrace('Regular daily check for available updates ... ', '');
1378 $this->cron_execute();
1379 return;
1380 }
1381 }
1382
1383 /// end of public API //////////////////////////////////////////////////////
1384
cd0bb55f 1385 /**
7d8de6d8 1386 * Makes cURL request to get data from the remote site
b9934a17 1387 *
7d8de6d8 1388 * @return string raw request result
cd0bb55f
DM
1389 * @throws available_update_checker_exception
1390 */
7d8de6d8 1391 protected function get_response() {
b4bfdf5a
PS
1392 global $CFG;
1393 require_once($CFG->libdir.'/filelib.php');
1394
cd0bb55f 1395 $curl = new curl(array('proxy' => true));
4785c45d
DM
1396 $response = $curl->post($this->prepare_request_url(), $this->prepare_request_params(), $this->prepare_request_options());
1397 $curlerrno = $curl->get_errno();
1398 if (!empty($curlerrno)) {
1399 throw new available_update_checker_exception('err_response_curl', 'cURL error '.$curlerrno.': '.$curl->error);
1400 }
cd0bb55f
DM
1401 $curlinfo = $curl->get_info();
1402 if ($curlinfo['http_code'] != 200) {
1403 throw new available_update_checker_exception('err_response_http_code', $curlinfo['http_code']);
1404 }
cd0bb55f
DM
1405 return $response;
1406 }
1407
1408 /**
1409 * Makes sure the response is valid, has correct API format etc.
1410 *
7d8de6d8 1411 * @param string $response raw response as returned by the {@link self::get_response()}
cd0bb55f
DM
1412 * @throws available_update_checker_exception
1413 */
7d8de6d8
DM
1414 protected function validate_response($response) {
1415
1416 $response = $this->decode_response($response);
cd0bb55f
DM
1417
1418 if (empty($response)) {
1419 throw new available_update_checker_exception('err_response_empty');
1420 }
1421
7d8de6d8
DM
1422 if (empty($response['status']) or $response['status'] !== 'OK') {
1423 throw new available_update_checker_exception('err_response_status', $response['status']);
1424 }
1425
803738ea 1426 if (empty($response['apiver']) or $response['apiver'] !== '1.2') {
7d8de6d8 1427 throw new available_update_checker_exception('err_response_format_version', $response['apiver']);
cd0bb55f
DM
1428 }
1429
7d8de6d8 1430 if (empty($response['forbranch']) or $response['forbranch'] !== moodle_major_version(true)) {
d5d2e353 1431 throw new available_update_checker_exception('err_response_target_version', $response['forbranch']);
cd0bb55f
DM
1432 }
1433 }
1434
1435 /**
7d8de6d8 1436 * Decodes the raw string response from the update notifications provider
b9934a17 1437 *
7d8de6d8
DM
1438 * @param string $response as returned by {@link self::get_response()}
1439 * @return array decoded response structure
b9934a17 1440 */
7d8de6d8
DM
1441 protected function decode_response($response) {
1442 return json_decode($response, true);
cd0bb55f 1443 }
b9934a17
DM
1444
1445 /**
7d8de6d8
DM
1446 * Stores the valid fetched response for later usage
1447 *
1448 * This implementation uses the config_plugins table as the permanent storage.
b9934a17 1449 *
7d8de6d8 1450 * @param string $response raw valid data returned by {@link self::get_response()}
b9934a17 1451 */
7d8de6d8
DM
1452 protected function store_response($response) {
1453
1454 set_config('recentfetch', time(), 'core_plugin');
1455 set_config('recentresponse', $response, 'core_plugin');
1456
1457 $this->restore_response(true);
cd0bb55f 1458 }
b9934a17
DM
1459
1460 /**
7d8de6d8 1461 * Loads the most recent raw response record we have fetched
b9934a17 1462 *
c62580b9
DM
1463 * After this method is called, $this->recentresponse is set to an array. If the
1464 * array is empty, then either no data have been fetched yet or the fetched data
1465 * do not have expected format (and thence they are ignored and a debugging
1466 * message is displayed).
1467 *
7d8de6d8 1468 * This implementation uses the config_plugins table as the permanent storage.
b9934a17 1469 *
7d8de6d8 1470 * @param bool $forcereload reload even if it was already loaded
b9934a17 1471 */
7d8de6d8
DM
1472 protected function restore_response($forcereload = false) {
1473
1474 if (!$forcereload and !is_null($this->recentresponse)) {
1475 // we already have it, nothing to do
1476 return;
cd0bb55f
DM
1477 }
1478
7d8de6d8
DM
1479 $config = get_config('core_plugin');
1480
1481 if (!empty($config->recentresponse) and !empty($config->recentfetch)) {
1482 try {
1483 $this->validate_response($config->recentresponse);
1484 $this->recentfetch = $config->recentfetch;
1485 $this->recentresponse = $this->decode_response($config->recentresponse);
660c4d46 1486 } catch (available_update_checker_exception $e) {
a22de4ce
DM
1487 // The server response is not valid. Behave as if no data were fetched yet.
1488 // This may happen when the most recent update info (cached locally) has been
1489 // fetched with the previous branch of Moodle (like during an upgrade from 2.x
1490 // to 2.y) or when the API of the response has changed.
c62580b9 1491 $this->recentresponse = array();
7d8de6d8
DM
1492 }
1493
cd0bb55f 1494 } else {
7d8de6d8 1495 $this->recentresponse = array();
cd0bb55f
DM
1496 }
1497 }
1498
7b35553b
DM
1499 /**
1500 * Compares two raw {@link $recentresponse} records and returns the list of changed updates
1501 *
1502 * This method is used to populate potential update info to be sent to site admins.
1503 *
19d11b3b
DM
1504 * @param array $old
1505 * @param array $new
7b35553b
DM
1506 * @throws available_update_checker_exception
1507 * @return array parts of $new['updates'] that have changed
1508 */
19d11b3b 1509 protected function compare_responses(array $old, array $new) {
7b35553b 1510
19d11b3b 1511 if (empty($new)) {
7b35553b
DM
1512 return array();
1513 }
1514
1515 if (!array_key_exists('updates', $new)) {
1516 throw new available_update_checker_exception('err_response_format');
1517 }
1518
19d11b3b 1519 if (empty($old)) {
7b35553b
DM
1520 return $new['updates'];
1521 }
1522
1523 if (!array_key_exists('updates', $old)) {
1524 throw new available_update_checker_exception('err_response_format');
1525 }
1526
1527 $changes = array();
1528
1529 foreach ($new['updates'] as $newcomponent => $newcomponentupdates) {
1530 if (empty($old['updates'][$newcomponent])) {
1531 $changes[$newcomponent] = $newcomponentupdates;
1532 continue;
1533 }
1534 foreach ($newcomponentupdates as $newcomponentupdate) {
1535 $inold = false;
1536 foreach ($old['updates'][$newcomponent] as $oldcomponentupdate) {
1537 if ($newcomponentupdate['version'] == $oldcomponentupdate['version']) {
1538 $inold = true;
1539 }
1540 }
1541 if (!$inold) {
1542 if (!isset($changes[$newcomponent])) {
1543 $changes[$newcomponent] = array();
1544 }
1545 $changes[$newcomponent][] = $newcomponentupdate;
1546 }
1547 }
1548 }
1549
1550 return $changes;
1551 }
1552
cd0bb55f
DM
1553 /**
1554 * Returns the URL to send update requests to
1555 *
1556 * During the development or testing, you can set $CFG->alternativeupdateproviderurl
1557 * to a custom URL that will be used. Otherwise the standard URL will be returned.
1558 *
1559 * @return string URL
1560 */
1561 protected function prepare_request_url() {
1562 global $CFG;
1563
56c05088
DM
1564 if (!empty($CFG->config_php_settings['alternativeupdateproviderurl'])) {
1565 return $CFG->config_php_settings['alternativeupdateproviderurl'];
cd0bb55f 1566 } else {
803738ea 1567 return 'https://download.moodle.org/api/1.2/updates.php';
cd0bb55f
DM
1568 }
1569 }
1570
55585f3a 1571 /**
4442cc80 1572 * Sets the properties currentversion, currentrelease, currentbranch and currentplugins
55585f3a
DM
1573 *
1574 * @param bool $forcereload
1575 */
1576 protected function load_current_environment($forcereload=false) {
1577 global $CFG;
1578
1579 if (!is_null($this->currentversion) and !$forcereload) {
1580 // nothing to do
1581 return;
1582 }
1583
975311d3
PS
1584 $version = null;
1585 $release = null;
1586
55585f3a
DM
1587 require($CFG->dirroot.'/version.php');
1588 $this->currentversion = $version;
4442cc80 1589 $this->currentrelease = $release;
55585f3a
DM
1590 $this->currentbranch = moodle_major_version(true);
1591
1592 $pluginman = plugin_manager::instance();
1593 foreach ($pluginman->get_plugins() as $type => $plugins) {
1594 foreach ($plugins as $plugin) {
1595 if (!$plugin->is_standard()) {
1596 $this->currentplugins[$plugin->component] = $plugin->versiondisk;
1597 }
1598 }
1599 }
1600 }
1601
cd0bb55f
DM
1602 /**
1603 * Returns the list of HTTP params to be sent to the updates provider URL
1604 *
1605 * @return array of (string)param => (string)value
1606 */
1607 protected function prepare_request_params() {
1608 global $CFG;
1609
55585f3a 1610 $this->load_current_environment();
7d8de6d8
DM
1611 $this->restore_response();
1612
cd0bb55f
DM
1613 $params = array();
1614 $params['format'] = 'json';
1615
7d8de6d8
DM
1616 if (isset($this->recentresponse['ticket'])) {
1617 $params['ticket'] = $this->recentresponse['ticket'];
cd0bb55f
DM
1618 }
1619
55585f3a
DM
1620 if (isset($this->currentversion)) {
1621 $params['version'] = $this->currentversion;
1622 } else {
1623 throw new coding_exception('Main Moodle version must be already known here');
cd0bb55f
DM
1624 }
1625
55585f3a
DM
1626 if (isset($this->currentbranch)) {
1627 $params['branch'] = $this->currentbranch;
1628 } else {
1629 throw new coding_exception('Moodle release must be already known here');
1630 }
1631
1632 $plugins = array();
1633 foreach ($this->currentplugins as $plugin => $version) {
1634 $plugins[] = $plugin.'@'.$version;
1635 }
1636 if (!empty($plugins)) {
1637 $params['plugins'] = implode(',', $plugins);
cd0bb55f
DM
1638 }
1639
cd0bb55f
DM
1640 return $params;
1641 }
be378880 1642
4785c45d
DM
1643 /**
1644 * Returns the list of cURL options to use when fetching available updates data
1645 *
1646 * @return array of (string)param => (string)value
1647 */
1648 protected function prepare_request_options() {
1649 global $CFG;
1650
1651 $options = array(
1652 'CURLOPT_SSL_VERIFYHOST' => 2, // this is the default in {@link curl} class but just in case
1653 'CURLOPT_SSL_VERIFYPEER' => true,
1654 );
1655
4785c45d
DM
1656 return $options;
1657 }
1658
be378880
DM
1659 /**
1660 * Returns the current timestamp
1661 *
1662 * @return int the timestamp
1663 */
1664 protected function cron_current_timestamp() {
1665 return time();
1666 }
1667
1668 /**
1669 * Output cron debugging info
1670 *
1671 * @see mtrace()
1672 * @param string $msg output message
1673 * @param string $eol end of line
1674 */
1675 protected function cron_mtrace($msg, $eol = PHP_EOL) {
1676 mtrace($msg, $eol);
1677 }
1678
1679 /**
1680 * Decide if the autocheck feature is disabled in the server setting
1681 *
1682 * @return bool true if autocheck enabled, false if disabled
1683 */
1684 protected function cron_autocheck_enabled() {
718eb2a5
DM
1685 global $CFG;
1686
be378880
DM
1687 if (empty($CFG->updateautocheck)) {
1688 return false;
1689 } else {
1690 return true;
1691 }
1692 }
1693
1694 /**
1695 * Decide if the recently fetched data are still fresh enough
1696 *
1697 * @param int $now current timestamp
1698 * @return bool true if no need to re-fetch, false otherwise
1699 */
1700 protected function cron_has_fresh_fetch($now) {
1701 $recent = $this->get_last_timefetched();
1702
1703 if (empty($recent)) {
1704 return false;
1705 }
1706
1707 if ($now < $recent) {
1708 $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1709 return true;
1710 }
1711
7092ea5d 1712 if ($now - $recent > 24 * HOURSECS) {
be378880
DM
1713 return false;
1714 }
1715
1716 return true;
1717 }
1718
1719 /**
1720 * Decide if the fetch is outadated or even missing
1721 *
1722 * @param int $now current timestamp
1723 * @return bool false if no need to re-fetch, true otherwise
1724 */
1725 protected function cron_has_outdated_fetch($now) {
1726 $recent = $this->get_last_timefetched();
1727
1728 if (empty($recent)) {
1729 return true;
1730 }
1731
1732 if ($now < $recent) {
1733 $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1734 return false;
1735 }
1736
1737 if ($now - $recent > 48 * HOURSECS) {
1738 return true;
1739 }
1740
1741 return false;
1742 }
1743
1744 /**
1745 * Returns the cron execution offset for this site
1746 *
1747 * The main {@link self::cron()} is supposed to run every night in some random time
1748 * between 01:00 and 06:00 AM (local time). The exact moment is defined by so called
1749 * execution offset, that is the amount of time after 01:00 AM. The offset value is
1750 * initially generated randomly and then used consistently at the site. This way, the
1751 * regular checks against the download.moodle.org server are spread in time.
1752 *
1753 * @return int the offset number of seconds from range 1 sec to 5 hours
1754 */
1755 protected function cron_execution_offset() {
1756 global $CFG;
1757
1758 if (empty($CFG->updatecronoffset)) {
1759 set_config('updatecronoffset', rand(1, 5 * HOURSECS));
1760 }
1761
1762 return $CFG->updatecronoffset;
1763 }
1764
1765 /**
1766 * Fetch available updates info and eventually send notification to site admins
1767 */
1768 protected function cron_execute() {
7b35553b 1769
19d11b3b 1770 try {
fd87d0bf
AB
1771 $this->restore_response();
1772 $previous = $this->recentresponse;
1773 $this->fetch();
1774 $this->restore_response(true);
1775 $current = $this->recentresponse;
19d11b3b
DM
1776 $changes = $this->compare_responses($previous, $current);
1777 $notifications = $this->cron_notifications($changes);
1778 $this->cron_notify($notifications);
a77141a7 1779 $this->cron_mtrace('done');
19d11b3b
DM
1780 } catch (available_update_checker_exception $e) {
1781 $this->cron_mtrace('FAILED!');
1782 }
1783 }
1784
1785 /**
1786 * Given the list of changes in available updates, pick those to send to site admins
1787 *
1788 * @param array $changes as returned by {@link self::compare_responses()}
1789 * @return array of available_update_info objects to send to site admins
1790 */
1791 protected function cron_notifications(array $changes) {
1792 global $CFG;
1793
1794 $notifications = array();
1795 $pluginman = plugin_manager::instance();
1796 $plugins = $pluginman->get_plugins(true);
1797
1798 foreach ($changes as $component => $componentchanges) {
718eb2a5
DM
1799 if (empty($componentchanges)) {
1800 continue;
1801 }
19d11b3b
DM
1802 $componentupdates = $this->get_update_info($component,
1803 array('minmaturity' => $CFG->updateminmaturity, 'notifybuilds' => $CFG->updatenotifybuilds));
718eb2a5
DM
1804 if (empty($componentupdates)) {
1805 continue;
1806 }
19d11b3b
DM
1807 // notify only about those $componentchanges that are present in $componentupdates
1808 // to respect the preferences
1809 foreach ($componentchanges as $componentchange) {
1810 foreach ($componentupdates as $componentupdate) {
1811 if ($componentupdate->version == $componentchange['version']) {
1812 if ($component == 'core') {
fa1415f1
DM
1813 // In case of 'core', we already know that the $componentupdate
1814 // is a real update with higher version ({@see self::get_update_info()}).
1815 // We just perform additional check for the release property as there
1816 // can be two Moodle releases having the same version (e.g. 2.4.0 and 2.5dev shortly
1817 // after the release). We can do that because we have the release info
1818 // always available for the core.
1819 if ((string)$componentupdate->release === (string)$componentchange['release']) {
1820 $notifications[] = $componentupdate;
1821 }
19d11b3b 1822 } else {
d2713eff
DM
1823 // Use the plugin_manager to check if the detected $componentchange
1824 // is a real update with higher version. That is, the $componentchange
1825 // is present in the array of {@link available_update_info} objects
1826 // returned by the plugin's available_updates() method.
56da374e 1827 list($plugintype, $pluginname) = core_component::normalize_component($component);
d2713eff
DM
1828 if (!empty($plugins[$plugintype][$pluginname])) {
1829 $availableupdates = $plugins[$plugintype][$pluginname]->available_updates();
1830 if (!empty($availableupdates)) {
1831 foreach ($availableupdates as $availableupdate) {
1832 if ($availableupdate->version == $componentchange['version']) {
1833 $notifications[] = $componentupdate;
1834 }
19d11b3b
DM
1835 }
1836 }
1837 }
1838 }
1839 }
1840 }
1841 }
1842 }
1843
1844 return $notifications;
be378880 1845 }
a77141a7
DM
1846
1847 /**
1848 * Sends the given notifications to site admins via messaging API
1849 *
1850 * @param array $notifications array of available_update_info objects to send
1851 */
1852 protected function cron_notify(array $notifications) {
1853 global $CFG;
1854
1855 if (empty($notifications)) {
1856 return;
1857 }
1858
1859 $admins = get_admins();
1860
1861 if (empty($admins)) {
1862 return;
1863 }
1864
1865 $this->cron_mtrace('sending notifications ... ', '');
1866
1867 $text = get_string('updatenotifications', 'core_admin') . PHP_EOL;
1868 $html = html_writer::tag('h1', get_string('updatenotifications', 'core_admin')) . PHP_EOL;
1869
1870 $coreupdates = array();
1871 $pluginupdates = array();
1872
660c4d46 1873 foreach ($notifications as $notification) {
a77141a7
DM
1874 if ($notification->component == 'core') {
1875 $coreupdates[] = $notification;
1876 } else {
1877 $pluginupdates[] = $notification;
1878 }
1879 }
1880
1881 if (!empty($coreupdates)) {
1882 $text .= PHP_EOL . get_string('updateavailable', 'core_admin') . PHP_EOL;
1883 $html .= html_writer::tag('h2', get_string('updateavailable', 'core_admin')) . PHP_EOL;
1884 $html .= html_writer::start_tag('ul') . PHP_EOL;
1885 foreach ($coreupdates as $coreupdate) {
1886 $html .= html_writer::start_tag('li');
1887 if (isset($coreupdate->release)) {
1888 $text .= get_string('updateavailable_release', 'core_admin', $coreupdate->release);
1889 $html .= html_writer::tag('strong', get_string('updateavailable_release', 'core_admin', $coreupdate->release));
1890 }
1891 if (isset($coreupdate->version)) {
1892 $text .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
1893 $html .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
1894 }
1895 if (isset($coreupdate->maturity)) {
1896 $text .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
1897 $html .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
1898 }
1899 $text .= PHP_EOL;
1900 $html .= html_writer::end_tag('li') . PHP_EOL;
1901 }
1902 $text .= PHP_EOL;
1903 $html .= html_writer::end_tag('ul') . PHP_EOL;
1904
1905 $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/index.php');
1906 $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
1907 $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/index.php', $CFG->wwwroot.'/'.$CFG->admin.'/index.php'));
1908 $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
1909 }
1910
1911 if (!empty($pluginupdates)) {
1912 $text .= PHP_EOL . get_string('updateavailableforplugin', 'core_admin') . PHP_EOL;
1913 $html .= html_writer::tag('h2', get_string('updateavailableforplugin', 'core_admin')) . PHP_EOL;
1914
1915 $html .= html_writer::start_tag('ul') . PHP_EOL;
1916 foreach ($pluginupdates as $pluginupdate) {
1917 $html .= html_writer::start_tag('li');
1918 $text .= get_string('pluginname', $pluginupdate->component);
1919 $html .= html_writer::tag('strong', get_string('pluginname', $pluginupdate->component));
1920
1921 $text .= ' ('.$pluginupdate->component.')';
1922 $html .= ' ('.$pluginupdate->component.')';
1923
1924 $text .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
1925 $html .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
1926
1927 $text .= PHP_EOL;
1928 $html .= html_writer::end_tag('li') . PHP_EOL;
1929 }
1930 $text .= PHP_EOL;
1931 $html .= html_writer::end_tag('ul') . PHP_EOL;
b9934a17 1932
a77141a7
DM
1933 $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php');
1934 $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
1935 $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/plugins.php', $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php'));
1936 $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
1937 }
1938
1939 $a = array('siteurl' => $CFG->wwwroot);
1940 $text .= get_string('updatenotificationfooter', 'core_admin', $a) . PHP_EOL;
1941 $a = array('siteurl' => html_writer::link($CFG->wwwroot, $CFG->wwwroot));
1942 $html .= html_writer::tag('footer', html_writer::tag('p', get_string('updatenotificationfooter', 'core_admin', $a),
1943 array('style' => 'font-size:smaller; color:#333;')));
1944
a77141a7
DM
1945 foreach ($admins as $admin) {
1946 $message = new stdClass();
1947 $message->component = 'moodle';
1948 $message->name = 'availableupdate';
55079015 1949 $message->userfrom = get_admin();
a77141a7 1950 $message->userto = $admin;
2399585f 1951 $message->subject = get_string('updatenotificationsubject', 'core_admin', array('siteurl' => $CFG->wwwroot));
a77141a7
DM
1952 $message->fullmessage = $text;
1953 $message->fullmessageformat = FORMAT_PLAIN;
1954 $message->fullmessagehtml = $html;
cd89994d
DM
1955 $message->smallmessage = get_string('updatenotifications', 'core_admin');
1956 $message->notification = 1;
a77141a7
DM
1957 message_send($message);
1958 }
1959 }
b9934a17
DM
1960
1961 /**
4442cc80 1962 * Compare two release labels and decide if they are the same
b9934a17 1963 *
4442cc80
DM
1964 * @param string $remote release info of the available update
1965 * @param null|string $local release info of the local code, defaults to $release defined in version.php
1966 * @return boolean true if the releases declare the same minor+major version
b9934a17 1967 */
4442cc80 1968 protected function is_same_release($remote, $local=null) {
b9934a17 1969
4442cc80
DM
1970 if (is_null($local)) {
1971 $this->load_current_environment();
1972 $local = $this->currentrelease;
1973 }
0242bdc7 1974
4442cc80 1975 $pattern = '/^([0-9\.\+]+)([^(]*)/';
b9934a17 1976
4442cc80
DM
1977 preg_match($pattern, $remote, $remotematches);
1978 preg_match($pattern, $local, $localmatches);
b9934a17 1979
4442cc80
DM
1980 $remotematches[1] = str_replace('+', '', $remotematches[1]);
1981 $localmatches[1] = str_replace('+', '', $localmatches[1]);
1982
1983 if ($remotematches[1] === $localmatches[1] and rtrim($remotematches[2]) === rtrim($localmatches[2])) {
1984 return true;
1985 } else {
1986 return false;
1987 }
1988 }
cd0bb55f
DM
1989}
1990
1991
7d8de6d8
DM
1992/**
1993 * Defines the structure of objects returned by {@link available_update_checker::get_update_info()}
1994 */
1995class available_update_info {
1996
1997 /** @var string frankenstyle component name */
1998 public $component;
1999 /** @var int the available version of the component */
2000 public $version;
2001 /** @var string|null optional release name */
2002 public $release = null;
2003 /** @var int|null optional maturity info, eg {@link MATURITY_STABLE} */
2004 public $maturity = null;
2005 /** @var string|null optional URL of a page with more info about the update */
2006 public $url = null;
2007 /** @var string|null optional URL of a ZIP package that can be downloaded and installed */
2008 public $download = null;
6b75106a
DM
2009 /** @var string|null of self::download is set, then this must be the MD5 hash of the ZIP */
2010 public $downloadmd5 = null;
7d8de6d8
DM
2011
2012 /**
2013 * Creates new instance of the class
b9934a17 2014 *
7d8de6d8
DM
2015 * The $info array must provide at least the 'version' value and optionally all other
2016 * values to populate the object's properties.
b9934a17 2017 *
7d8de6d8
DM
2018 * @param string $name the frankenstyle component name
2019 * @param array $info associative array with other properties
2020 */
2021 public function __construct($name, array $info) {
2022 $this->component = $name;
2023 foreach ($info as $k => $v) {
2024 if (property_exists('available_update_info', $k) and $k != 'component') {
2025 $this->$k = $v;
2026 }
2027 }
2028 }
2029}
2030
2031
7683e550
DM
2032/**
2033 * Implements a communication bridge to the mdeploy.php utility
2034 */
2035class available_update_deployer {
2036
2037 const HTTP_PARAM_PREFIX = 'updteautodpldata_'; // Hey, even Google has not heard of such a prefix! So it MUST be safe :-p
2038 const HTTP_PARAM_CHECKER = 'datapackagesize'; // Name of the parameter that holds the number of items in the received data items
2039
2040 /** @var available_update_deployer holds the singleton instance */
2041 protected static $singletoninstance;
2042 /** @var moodle_url URL of a page that includes the deployer UI */
2043 protected $callerurl;
2044 /** @var moodle_url URL to return after the deployment */
2045 protected $returnurl;
2046
2047 /**
2048 * Direct instantiation not allowed, use the factory method {@link self::instance()}
2049 */
2050 protected function __construct() {
2051 }
2052
2053 /**
2054 * Sorry, this is singleton
2055 */
2056 protected function __clone() {
2057 }
2058
2059 /**
2060 * Factory method for this class
2061 *
2062 * @return available_update_deployer the singleton instance
2063 */
2064 public static function instance() {
2065 if (is_null(self::$singletoninstance)) {
2066 self::$singletoninstance = new self();
2067 }
2068 return self::$singletoninstance;
2069 }
2070
dc11af19
DM
2071 /**
2072 * Reset caches used by this script
2073 *
2074 * @param bool $phpunitreset is this called as a part of PHPUnit reset?
2075 */
2076 public static function reset_caches($phpunitreset = false) {
2077 if ($phpunitreset) {
2078 self::$singletoninstance = null;
2079 }
2080 }
2081
7683e550
DM
2082 /**
2083 * Is automatic deployment enabled?
2084 *
2085 * @return bool
2086 */
2087 public function enabled() {
2088 global $CFG;
2089
2090 if (!empty($CFG->disableupdateautodeploy)) {
2091 // The feature is prohibited via config.php
2092 return false;
2093 }
2094
2095 return get_config('updateautodeploy');
2096 }
2097
2098 /**
2099 * Sets some base properties of the class to make it usable.
2100 *
2101 * @param moodle_url $callerurl the base URL of a script that will handle the class'es form data
2102 * @param moodle_url $returnurl the final URL to return to when the deployment is finished
2103 */
2104 public function initialize(moodle_url $callerurl, moodle_url $returnurl) {
2105
2106 if (!$this->enabled()) {
2107 throw new coding_exception('Unable to initialize the deployer, the feature is not enabled.');
2108 }
2109
2110 $this->callerurl = $callerurl;
2111 $this->returnurl = $returnurl;
2112 }
2113
2114 /**
2115 * Has the deployer been initialized?
2116 *
2117 * Initialized deployer means that the following properties were set:
2118 * callerurl, returnurl
2119 *
2120 * @return bool
2121 */
2122 public function initialized() {
2123
2124 if (!$this->enabled()) {
2125 return false;
2126 }
2127
2128 if (empty($this->callerurl)) {
2129 return false;
2130 }
2131
2132 if (empty($this->returnurl)) {
2133 return false;
2134 }
2135
2136 return true;
2137 }
2138
2139 /**
0daa6428 2140 * Returns a list of reasons why the deployment can not happen
7683e550 2141 *
0daa6428
DM
2142 * If the returned array is empty, the deployment seems to be possible. The returned
2143 * structure is an associative array with keys representing individual impediments.
2144 * Possible keys are: missingdownloadurl, missingdownloadmd5, notwritable.
7683e550
DM
2145 *
2146 * @param available_update_info $info
0daa6428 2147 * @return array
7683e550 2148 */
0daa6428
DM
2149 public function deployment_impediments(available_update_info $info) {
2150
2151 $impediments = array();
7683e550
DM
2152
2153 if (empty($info->download)) {
0daa6428 2154 $impediments['missingdownloadurl'] = true;
7683e550
DM
2155 }
2156
6b75106a 2157 if (empty($info->downloadmd5)) {
0daa6428 2158 $impediments['missingdownloadmd5'] = true;
6b75106a
DM
2159 }
2160
30e26827
DM
2161 if (!empty($info->download) and !$this->update_downloadable($info->download)) {
2162 $impediments['notdownloadable'] = true;
2163 }
2164
0daa6428
DM
2165 if (!$this->component_writable($info->component)) {
2166 $impediments['notwritable'] = true;
2167 }
2168
2169 return $impediments;
7683e550
DM
2170 }
2171
08c3bc00
DM
2172 /**
2173 * Check to see if the current version of the plugin seems to be a checkout of an external repository.
2174 *
436d9447 2175 * @see plugin_manager::plugin_external_source()
08c3bc00
DM
2176 * @param available_update_info $info
2177 * @return false|string
2178 */
2179 public function plugin_external_source(available_update_info $info) {
2180
46f6f7f2 2181 $paths = core_component::get_plugin_types();
56da374e 2182 list($plugintype, $pluginname) = core_component::normalize_component($info->component);
08c3bc00
DM
2183 $pluginroot = $paths[$plugintype].'/'.$pluginname;
2184
2185 if (is_dir($pluginroot.'/.git')) {
2186 return 'git';
2187 }
2188
2189 if (is_dir($pluginroot.'/CVS')) {
2190 return 'cvs';
2191 }
2192
2193 if (is_dir($pluginroot.'/.svn')) {
2194 return 'svn';
2195 }
2196
2197 return false;
2198 }
2199
7683e550
DM
2200 /**
2201 * Prepares a renderable widget to confirm installation of an available update.
2202 *
2203 * @param available_update_info $info component version to deploy
2204 * @return renderable
2205 */
2206 public function make_confirm_widget(available_update_info $info) {
2207
2208 if (!$this->initialized()) {
2209 throw new coding_exception('Illegal method call - deployer not initialized.');
2210 }
2211
2212 $params = $this->data_to_params(array(
2213 'updateinfo' => (array)$info, // see http://www.php.net/manual/en/language.types.array.php#language.types.array.casting
2214 ));
2215
2216 $widget = new single_button(
2217 new moodle_url($this->callerurl, $params),
2218 get_string('updateavailableinstall', 'core_admin'),
2219 'post'
2220 );
2221
2222 return $widget;
2223 }
2224
2225 /**
2226 * Prepares a renderable widget to execute installation of an available update.
2227 *
2228 * @param available_update_info $info component version to deploy
5d7a4bab 2229 * @param moodle_url $returnurl URL to return after the installation execution
7683e550
DM
2230 * @return renderable
2231 */
5d7a4bab 2232 public function make_execution_widget(available_update_info $info, moodle_url $returnurl = null) {
7683e550
DM
2233 global $CFG;
2234
2235 if (!$this->initialized()) {
2236 throw new coding_exception('Illegal method call - deployer not initialized.');
2237 }
2238
46f6f7f2 2239 $pluginrootpaths = core_component::get_plugin_types();
7683e550 2240
56da374e 2241 list($plugintype, $pluginname) = core_component::normalize_component($info->component);
7683e550
DM
2242
2243 if (empty($pluginrootpaths[$plugintype])) {
2244 throw new coding_exception('Unknown plugin type root location', $plugintype);
2245 }
2246
3daedb5c
DM
2247 list($passfile, $password) = $this->prepare_authorization();
2248
5d7a4bab
DM
2249 if (is_null($returnurl)) {
2250 $returnurl = new moodle_url('/admin');
2251 } else {
2252 $returnurl = $returnurl;
2253 }
23137c4a 2254
7683e550
DM
2255 $params = array(
2256 'upgrade' => true,
2257 'type' => $plugintype,
2258 'name' => $pluginname,
2259 'typeroot' => $pluginrootpaths[$plugintype],
4c72f555 2260 'package' => $info->download,
6b75106a 2261 'md5' => $info->downloadmd5,
7683e550
DM
2262 'dataroot' => $CFG->dataroot,
2263 'dirroot' => $CFG->dirroot,
3daedb5c
DM
2264 'passfile' => $passfile,
2265 'password' => $password,
5d7a4bab 2266 'returnurl' => $returnurl->out(false),
7683e550
DM
2267 );
2268
63def597 2269 if (!empty($CFG->proxyhost)) {
0dcae7cd
DP
2270 // MDL-36973 - Beware - we should call just !is_proxybypass() here. But currently, our
2271 // cURL wrapper class does not do it. So, to have consistent behaviour, we pass proxy
2272 // setting regardless the $CFG->proxybypass setting. Once the {@link curl} class is
2273 // fixed, the condition should be amended.
63def597
DM
2274 if (true or !is_proxybypass($info->download)) {
2275 if (empty($CFG->proxyport)) {
2276 $params['proxy'] = $CFG->proxyhost;
2277 } else {
2278 $params['proxy'] = $CFG->proxyhost.':'.$CFG->proxyport;
2279 }
2280
2281 if (!empty($CFG->proxyuser) and !empty($CFG->proxypassword)) {
2282 $params['proxyuserpwd'] = $CFG->proxyuser.':'.$CFG->proxypassword;
2283 }
2284
2285 if (!empty($CFG->proxytype)) {
2286 $params['proxytype'] = $CFG->proxytype;
2287 }
2288 }
2289 }
2290
7683e550
DM
2291 $widget = new single_button(
2292 new moodle_url('/mdeploy.php', $params),
2293 get_string('updateavailableinstall', 'core_admin'),
2294 'post'
2295 );
2296
2297 return $widget;
2298 }
2299
2300 /**
2301 * Returns array of data objects passed to this tool.
2302 *
2303 * @return array
2304 */
2305 public function submitted_data() {
2306
2307 $data = $this->params_to_data($_POST);
2308
2309 if (empty($data) or empty($data[self::HTTP_PARAM_CHECKER])) {
2310 return false;
2311 }
2312
2313 if (!empty($data['updateinfo']) and is_object($data['updateinfo'])) {
2314 $updateinfo = $data['updateinfo'];
2315 if (!empty($updateinfo->component) and !empty($updateinfo->version)) {
2316 $data['updateinfo'] = new available_update_info($updateinfo->component, (array)$updateinfo);
2317 }
2318 }
2319
2320 if (!empty($data['callerurl'])) {
2321 $data['callerurl'] = new moodle_url($data['callerurl']);
2322 }
2323
2324 if (!empty($data['returnurl'])) {
2325 $data['returnurl'] = new moodle_url($data['returnurl']);
2326 }
2327
2328 return $data;
2329 }
2330
2331 /**
2332 * Handles magic getters and setters for protected properties.
2333 *
2334 * @param string $name method name, e.g. set_returnurl()
2335 * @param array $arguments arguments to be passed to the array
2336 */
2337 public function __call($name, array $arguments = array()) {
2338
2339 if (substr($name, 0, 4) === 'set_') {
2340 $property = substr($name, 4);
2341 if (empty($property)) {
2342 throw new coding_exception('Invalid property name (empty)');
2343 }
2344 if (empty($arguments)) {
2345 $arguments = array(true); // Default value for flag-like properties.
2346 }
2347 // Make sure it is a protected property.
2348 $isprotected = false;
2349 $reflection = new ReflectionObject($this);
2350 foreach ($reflection->getProperties(ReflectionProperty::IS_PROTECTED) as $reflectionproperty) {
2351 if ($reflectionproperty->getName() === $property) {
2352 $isprotected = true;
2353 break;
2354 }
2355 }
2356 if (!$isprotected) {
2357 throw new coding_exception('Unable to set property - it does not exist or it is not protected');
2358 }
2359 $value = reset($arguments);
2360 $this->$property = $value;
2361 return;
2362 }
2363
2364 if (substr($name, 0, 4) === 'get_') {
2365 $property = substr($name, 4);
2366 if (empty($property)) {
2367 throw new coding_exception('Invalid property name (empty)');
2368 }
2369 if (!empty($arguments)) {
2370 throw new coding_exception('No parameter expected');
2371 }
2372 // Make sure it is a protected property.
2373 $isprotected = false;
2374 $reflection = new ReflectionObject($this);
2375 foreach ($reflection->getProperties(ReflectionProperty::IS_PROTECTED) as $reflectionproperty) {
2376 if ($reflectionproperty->getName() === $property) {
2377 $isprotected = true;
2378 break;
2379 }
2380 }
2381 if (!$isprotected) {
2382 throw new coding_exception('Unable to get property - it does not exist or it is not protected');
2383 }
2384 return $this->$property;
2385 }
2386 }
2387
3daedb5c
DM
2388 /**
2389 * Generates a random token and stores it in a file in moodledata directory.
2390 *
2391 * @return array of the (string)filename and (string)password in this order
2392 */
2393 public function prepare_authorization() {
2394 global $CFG;
2395
2396 make_upload_directory('mdeploy/auth/');
2397
2398 $attempts = 0;
2399 $success = false;
2400
2401 while (!$success and $attempts < 5) {
2402 $attempts++;
2403
2404 $passfile = $this->generate_passfile();
2405 $password = $this->generate_password();
2406 $now = time();
2407
2408 $filepath = $CFG->dataroot.'/mdeploy/auth/'.$passfile;
2409
2410 if (!file_exists($filepath)) {
2411 $success = file_put_contents($filepath, $password . PHP_EOL . $now . PHP_EOL, LOCK_EX);
eb459f71 2412 chmod($filepath, $CFG->filepermissions);
3daedb5c
DM
2413 }
2414 }
2415
2416 if ($success) {
2417 return array($passfile, $password);
2418
2419 } else {
2420 throw new moodle_exception('unable_prepare_authorization', 'core_plugin');
2421 }
2422 }
2423
7683e550
DM
2424 // End of external API
2425
2426 /**
2427 * Prepares an array of HTTP parameters that can be passed to another page.
2428 *
2429 * @param array|object $data associative array or an object holding the data, data JSON-able
2430 * @return array suitable as a param for moodle_url
2431 */
2432 protected function data_to_params($data) {
2433
2434 // Append some our own data
2435 if (!empty($this->callerurl)) {
2436 $data['callerurl'] = $this->callerurl->out(false);
2437 }
7b1e0645 2438 if (!empty($this->returnurl)) {
7683e550
DM
2439 $data['returnurl'] = $this->returnurl->out(false);
2440 }
2441
2442 // Finally append the count of items in the package.
2443 $data[self::HTTP_PARAM_CHECKER] = count($data);
2444
2445 // Generate params
2446 $params = array();
2447 foreach ($data as $name => $value) {
2448 $transname = self::HTTP_PARAM_PREFIX.$name;
2449 $transvalue = json_encode($value);
2450 $params[$transname] = $transvalue;
2451 }
2452
2453 return $params;
2454 }
2455
2456 /**
2457 * Converts HTTP parameters passed to the script into native PHP data
2458 *
2459 * @param array $params such as $_REQUEST or $_POST
2460 * @return array data passed for this class
2461 */
2462 protected function params_to_data(array $params) {
2463
2464 if (empty($params)) {
2465 return array();
2466 }
2467
2468 $data = array();
2469 foreach ($params as $name => $value) {
2470 if (strpos($name, self::HTTP_PARAM_PREFIX) === 0) {
2471 $realname = substr($name, strlen(self::HTTP_PARAM_PREFIX));
2472 $realvalue = json_decode($value);
2473 $data[$realname] = $realvalue;
2474 }
2475 }
2476
2477 return $data;
2478 }
3daedb5c
DM
2479
2480 /**
2481 * Returns a random string to be used as a filename of the password storage.
2482 *
2483 * @return string
2484 */
2485 protected function generate_passfile() {
2486 return clean_param(uniqid('mdeploy_', true), PARAM_FILE);
2487 }
2488
2489 /**
2490 * Returns a random string to be used as the authorization token
2491 *
2492 * @return string
2493 */
2494 protected function generate_password() {
2495 return complex_random_string();
2496 }
0daa6428
DM
2497
2498 /**
2499 * Checks if the given component's directory is writable
2500 *
2501 * For the purpose of the deployment, the web server process has to have
2502 * write access to all files in the component's directory (recursively) and for the
2503 * directory itself.
2504 *
2505 * @see worker::move_directory_source_precheck()
2506 * @param string $component normalized component name
2507 * @return boolean
2508 */
2509 protected function component_writable($component) {
2510
56da374e 2511 list($plugintype, $pluginname) = core_component::normalize_component($component);
0daa6428 2512
1c74b260 2513 $directory = core_component::get_plugin_directory($plugintype, $pluginname);
0daa6428
DM
2514
2515 if (is_null($directory)) {
2516 throw new coding_exception('Unknown component location', $component);
2517 }
2518
2519 return $this->directory_writable($directory);
2520 }
2521
30e26827
DM
2522 /**
2523 * Checks if the mdeploy.php will be able to fetch the ZIP from the given URL
2524 *
2525 * This is mainly supposed to check if the transmission over HTTPS would
2526 * work. That is, if the CA certificates are present at the server.
2527 *
2528 * @param string $downloadurl the URL of the ZIP package to download
2529 * @return bool
2530 */
2531 protected function update_downloadable($downloadurl) {
2532 global $CFG;
2533
2534 $curloptions = array(
2535 'CURLOPT_SSL_VERIFYHOST' => 2, // this is the default in {@link curl} class but just in case
2536 'CURLOPT_SSL_VERIFYPEER' => true,
2537 );
2538
30e26827
DM
2539 $curl = new curl(array('proxy' => true));
2540 $result = $curl->head($downloadurl, $curloptions);
2541 $errno = $curl->get_errno();
2542 if (empty($errno)) {
2543 return true;
2544 } else {
2545 return false;
2546 }
2547 }
2548
0daa6428
DM
2549 /**
2550 * Checks if the directory and all its contents (recursively) is writable
2551 *
2552 * @param string $path full path to a directory
2553 * @return boolean
2554 */
2555 private function directory_writable($path) {
2556
2557 if (!is_writable($path)) {
2558 return false;
2559 }
2560
2561 if (is_dir($path)) {
2562 $handle = opendir($path);
2563 } else {
2564 return false;
2565 }
2566
2567 $result = true;
2568
2569 while ($filename = readdir($handle)) {
2570 $filepath = $path.'/'.$filename;
2571
2572 if ($filename === '.' or $filename === '..') {
2573 continue;
2574 }
2575
2576 if (is_dir($filepath)) {
2577 $result = $result && $this->directory_writable($filepath);
2578
2579 } else {
2580 $result = $result && is_writable($filepath);
2581 }
2582 }
2583
2584 closedir($handle);
2585
2586 return $result;
2587 }
7683e550
DM
2588}
2589
2590
00ef3c3e
DM
2591/**
2592 * Factory class producing required subclasses of {@link plugininfo_base}
2593 */
2594class plugininfo_default_factory {
b9934a17
DM
2595
2596 /**
00ef3c3e 2597 * Makes a new instance of the plugininfo class
b9934a17 2598 *
00ef3c3e
DM
2599 * @param string $type the plugin type, eg. 'mod'
2600 * @param string $typerootdir full path to the location of all the plugins of this type
2601 * @param string $name the plugin name, eg. 'workshop'
2602 * @param string $namerootdir full path to the location of the plugin
2603 * @param string $typeclass the name of class that holds the info about the plugin
2604 * @return plugininfo_base the instance of $typeclass
2605 */
2606 public static function make($type, $typerootdir, $name, $namerootdir, $typeclass) {
2607 $plugin = new $typeclass();
2608 $plugin->type = $type;
2609 $plugin->typerootdir = $typerootdir;
2610 $plugin->name = $name;
2611 $plugin->rootdir = $namerootdir;
2612
2613 $plugin->init_display_name();
2614 $plugin->load_disk_version();
2615 $plugin->load_db_version();
00ef3c3e 2616 $plugin->init_is_standard();
473289a0 2617
00ef3c3e
DM
2618 return $plugin;
2619 }
b9934a17
DM
2620}
2621
00ef3c3e 2622
b9934a17 2623/**
b6ad8594 2624 * Base class providing access to the information about a plugin
828788f0
TH
2625 *
2626 * @property-read string component the component name, type_name
b9934a17 2627 */
b6ad8594 2628abstract class plugininfo_base {
b9934a17
DM
2629
2630 /** @var string the plugintype name, eg. mod, auth or workshopform */
2631 public $type;
2632 /** @var string full path to the location of all the plugins of this type */
2633 public $typerootdir;
2634 /** @var string the plugin name, eg. assignment, ldap */
2635 public $name;
2636 /** @var string the localized plugin name */
2637 public $displayname;
2638 /** @var string the plugin source, one of plugin_manager::PLUGIN_SOURCE_xxx constants */
2639 public $source;
bde002b8 2640 /** @var string fullpath to the location of this plugin */
b9934a17
DM
2641 public $rootdir;
2642 /** @var int|string the version of the plugin's source code */
2643 public $versiondisk;
2644 /** @var int|string the version of the installed plugin */
2645 public $versiondb;
2646 /** @var int|float|string required version of Moodle core */
2647 public $versionrequires;
b6ad8594
DM
2648 /** @var array other plugins that this one depends on, lazy-loaded by {@link get_other_required_plugins()} */
2649 public $dependencies;
b9934a17
DM
2650 /** @var int number of instances of the plugin - not supported yet */
2651 public $instances;
2652 /** @var int order of the plugin among other plugins of the same type - not supported yet */
2653 public $sortorder;
7d8de6d8
DM
2654 /** @var array|null array of {@link available_update_info} for this plugin */
2655 public $availableupdates;
b9934a17
DM
2656
2657 /**
bde002b8
PS
2658 * Finds all enabled plugins, the result may include missing plugins.
2659 * @return array|null of enabled plugins $pluginname=>$pluginname, null means unknown
2660 */
2661 public static function get_enabled_plugins() {
2662 return null;
2663 }
2664
2665 /**
2666 * Gathers and returns the information about all plugins of the given type,
2667 * either on disk or previously installed.
b6ad8594 2668 *
b6ad8594
DM
2669 * @param string $type the name of the plugintype, eg. mod, auth or workshopform
2670 * @param string $typerootdir full path to the location of the plugin dir
2671 * @param string $typeclass the name of the actually called class
2672 * @return array of plugintype classes, indexed by the plugin name
b9934a17
DM
2673 */
2674 public static function get_plugins($type, $typerootdir, $typeclass) {
bde002b8 2675 // Get the information about plugins at the disk.
bd3b3bba 2676 $plugins = core_component::get_plugin_list($type);
bde002b8 2677 $return = array();
b9934a17 2678 foreach ($plugins as $pluginname => $pluginrootdir) {
bde002b8 2679 $return[$pluginname] = plugininfo_default_factory::make($type, $typerootdir,
00ef3c3e 2680 $pluginname, $pluginrootdir, $typeclass);
b9934a17 2681 }
bde002b8
PS
2682
2683 // Fetch missing incorrectly uninstalled plugins.
2684 $manager = plugin_manager::instance();
2685 $plugins = $manager->get_installed_plugins($type);
2686
2687 foreach ($plugins as $name => $version) {
2688 if (isset($return[$name])) {
2689 continue;
2690 }
2691 $plugin = new $typeclass();
2692 $plugin->type = $type;
2693 $plugin->typerootdir = $typerootdir;
2694 $plugin->name = $name;
2695 $plugin->rootdir = null;
2696 $plugin->displayname = $name;
2697 $plugin->versiondb = $version;
2698 $plugin->init_is_standard();
2699
2700 $return[$name] = $plugin;
2701 }
2702
2703 return $return;
b9934a17
DM
2704 }
2705
394372b7
PS
2706 /**
2707 * Is this plugin already installed and updated?
2708 * @return bool true if plugin installed and upgraded.
2709 */
2710 public function is_updated() {
2711 if (!$this->rootdir) {
2712 return false;
2713 }
2714 if ($this->versiondb === null and $this->versiondisk === null) {
2715 // There is no version.php or version info inside,
2716 // for now let's pretend it is ok.
2717 // TODO: return false once we require version in each plugin.
2718 return true;
2719 }
2720
2721 return ((float)$this->versiondb === (float)$this->versiondisk);
2722 }
2723
b9934a17 2724 /**
b6ad8594 2725 * Sets {@link $displayname} property to a localized name of the plugin
b9934a17 2726 */
b8343e68 2727 public function init_display_name() {
828788f0
TH
2728 if (!get_string_manager()->string_exists('pluginname', $this->component)) {
2729 $this->displayname = '[pluginname,' . $this->component . ']';
b9934a17 2730 } else {
828788f0
TH
2731 $this->displayname = get_string('pluginname', $this->component);
2732 }
2733 }
2734
2735 /**
2736 * Magic method getter, redirects to read only values.
b6ad8594 2737 *
828788f0
TH
2738 * @param string $name
2739 * @return mixed
2740 */
2741 public function __get($name) {
2742 switch ($name) {
2743 case 'component': return $this->type . '_' . $this->name;
2744
2745 default:
2746 debugging('Invalid plugin property accessed! '.$name);
2747 return null;
b9934a17
DM
2748 }
2749 }
2750
2751 /**
b6ad8594
DM
2752 * Return the full path name of a file within the plugin.
2753 *
2754 * No check is made to see if the file exists.
2755 *
2756 * @param string $relativepath e.g. 'version.php'.
2757 * @return string e.g. $CFG->dirroot . '/mod/quiz/version.php'.
b9934a17 2758 */
473289a0 2759 public function full_path($relativepath) {
b9934a17 2760 if (empty($this->rootdir)) {
473289a0 2761 return '';
b9934a17 2762 }
473289a0
TH
2763 return $this->rootdir . '/' . $relativepath;
2764 }
b9934a17 2765
b9934a17 2766 /**
b6ad8594
DM
2767 * Sets {@link $versiondisk} property to a numerical value representing the
2768 * version of the plugin's source code.
2769 *
2770 * If the value is null after calling this method, either the plugin
2771 * does not use versioning (typically does not have any database
2772 * data) or is missing from disk.
b9934a17 2773 */
473289a0 2774 public function load_disk_version() {
bde002b8
PS
2775 $versions = plugin_manager::instance()->get_present_plugins($this->type);
2776
2777 $this->versiondisk = null;
2778 $this->versionrequires = null;
2779 $this->dependencies = array();
2780
2781 if (!isset($versions[$this->name])) {
2782 return;
2783 }
2784
2785 $plugin = $versions[$this->name];
2786
473289a0
TH
2787 if (isset($plugin->version)) {
2788 $this->versiondisk = $plugin->version;
b9934a17 2789 }
473289a0
TH
2790 if (isset($plugin->requires)) {
2791 $this->versionrequires = $plugin->requires;
b9934a17 2792 }
bde002b8 2793 if (isset($plugin->dependencies)) {
777781d1 2794 $this->dependencies = $plugin->dependencies;
0242bdc7
TH
2795 }
2796 }
2797
2798 /**
b6ad8594
DM
2799 * Get the list of other plugins that this plugin requires to be installed.
2800 *
2801 * @return array with keys the frankenstyle plugin name, and values either
2802 * a version string (like '2011101700') or the constant ANY_VERSION.
0242bdc7
TH
2803 */
2804 public function get_other_required_plugins() {
777781d1 2805 if (is_null($this->dependencies)) {
bde002b8 2806 $this->load_disk_version();
0242bdc7 2807 }
777781d1 2808 return $this->dependencies;
0242bdc7
TH
2809 }
2810
73658371
DM
2811 /**
2812 * Is this is a subplugin?
2813 *
2814 * @return boolean
2815 */
2816 public function is_subplugin() {
2817 return ($this->get_parent_plugin() !== false);
2818 }
2819
2820 /**
2821 * If I am a subplugin, return the name of my parent plugin.
2822 *
2823 * @return string|bool false if not a subplugin, name of the parent otherwise
2824 */
2825 public function get_parent_plugin() {
2826 return $this->get_plugin_manager()->get_parent_of_subplugin($this->type);
2827 }
2828
473289a0 2829 /**
b6ad8594
DM
2830 * Sets {@link $versiondb} property to a numerical value representing the
2831 * currently installed version of the plugin.
2832 *
2833 * If the value is null after calling this method, either the plugin
2834 * does not use versioning (typically does not have any database
2835 * data) or has not been installed yet.
473289a0
TH
2836 */
2837 public function load_db_version() {
bde002b8
PS
2838 $versions = plugin_manager::instance()->get_installed_plugins($this->type);
2839
2840 if (isset($versions[$this->name])) {
2841 $this->versiondb = $versions[$this->name];
2842 } else {
2843 $this->versiondb = null;
b9934a17
DM
2844 }
2845 }
2846
2847 /**
b6ad8594
DM
2848 * Sets {@link $source} property to one of plugin_manager::PLUGIN_SOURCE_xxx
2849 * constants.
2850 *
2851 * If the property's value is null after calling this method, then
2852 * the type of the plugin has not been recognized and you should throw
2853 * an exception.
b9934a17 2854 */
b8343e68 2855 public function init_is_standard() {
b9934a17
DM
2856
2857 $standard = plugin_manager::standard_plugins_list($this->type);
2858
2859 if ($standard !== false) {
2860 $standard = array_flip($standard);
2861 if (isset($standard[$this->name])) {
2862 $this->source = plugin_manager::PLUGIN_SOURCE_STANDARD;
ec8935f5
PS
2863 } else if (!is_null($this->versiondb) and is_null($this->versiondisk)
2864 and plugin_manager::is_deleted_standard_plugin($this->type, $this->name)) {
2865 $this->source = plugin_manager::PLUGIN_SOURCE_STANDARD; // to be deleted
b9934a17
DM
2866 } else {
2867 $this->source = plugin_manager::PLUGIN_SOURCE_EXTENSION;
2868 }
2869 }
2870 }
2871
2872 /**
b6ad8594
DM
2873 * Returns true if the plugin is shipped with the official distribution
2874 * of the current Moodle version, false otherwise.
2875 *
2876 * @return bool
b9934a17
DM
2877 */
2878 public function is_standard() {
2879 return $this->source === plugin_manager::PLUGIN_SOURCE_STANDARD;
2880 }
2881
3a2300f5
DM
2882 /**
2883 * Returns true if the the given Moodle version is enough to run this plugin
2884 *
2885 * @param string|int|double $moodleversion
2886 * @return bool
2887 */
2888 public function is_core_dependency_satisfied($moodleversion) {
2889
2890 if (empty($this->versionrequires)) {
2891 return true;
2892
2893 } else {
2894 return (double)$this->versionrequires <= (double)$moodleversion;
2895 }
2896 }
2897
b9934a17 2898 /**
b6ad8594
DM
2899 * Returns the status of the plugin
2900 *
2901 * @return string one of plugin_manager::PLUGIN_STATUS_xxx constants
b9934a17
DM
2902 */
2903 public function get_status() {
2904
2905 if (is_null($this->versiondb) and is_null($this->versiondisk)) {
2906 return plugin_manager::PLUGIN_STATUS_NODB;
2907
2908 } else if (is_null($this->versiondb) and !is_null($this->versiondisk)) {
2909 return plugin_manager::PLUGIN_STATUS_NEW;
2910
2911 } else if (!is_null($this->versiondb) and is_null($this->versiondisk)) {
ec8935f5
PS
2912 if (plugin_manager::is_deleted_standard_plugin($this->type, $this->name)) {
2913 return plugin_manager::PLUGIN_STATUS_DELETE;
2914 } else {
2915 return plugin_manager::PLUGIN_STATUS_MISSING;
2916 }
b9934a17 2917
bde002b8
PS
2918 } else if ((float)$this->versiondb === (float)$this->versiondisk) {
2919 // Note: the float comparison should work fine here
2920 // because there are no arithmetic operations with the numbers.
b9934a17
DM
2921 return plugin_manager::PLUGIN_STATUS_UPTODATE;
2922
2923 } else if ($this->versiondb < $this->versiondisk) {
2924 return plugin_manager::PLUGIN_STATUS_UPGRADE;
2925
2926 } else if ($this->versiondb > $this->versiondisk) {
2927 return plugin_manager::PLUGIN_STATUS_DOWNGRADE;
2928
2929 } else {
2930 // $version = pi(); and similar funny jokes - hopefully Donald E. Knuth will never contribute to Moodle ;-)
2931 throw new coding_exception('Unable to determine plugin state, check the plugin versions');
2932 }
2933 }
2934
2935 /**
b6ad8594
DM
2936 * Returns the information about plugin availability
2937 *
2938 * True means that the plugin is enabled. False means that the plugin is
2939 * disabled. Null means that the information is not available, or the
2940 * plugin does not support configurable availability or the availability
2941 * can not be changed.
2942 *
2943 * @return null|bool
b9934a17
DM
2944 */
2945 public function is_enabled() {
bde002b8
PS
2946 if (!$this->rootdir) {
2947 // Plugin missing.
2948 return false;
2949 }
2950
2951 $enabled = plugin_manager::instance()->get_enabled_plugins($this->type);
2952
2953 if (!is_array($enabled)) {
2954 return null;
2955 }
2956
2957 return isset($enabled[$this->name]);
b9934a17
DM
2958 }
2959
2960 /**
7d8de6d8 2961 * Populates the property {@link $availableupdates} with the information provided by
dd119e21
DM
2962 * available update checker
2963 *
2964 * @param available_update_checker $provider the class providing the available update info
2965 */
7d8de6d8 2966 public function check_available_updates(available_update_checker $provider) {
c6f008e7
DM
2967 global $CFG;
2968
2969 if (isset($CFG->updateminmaturity)) {
2970 $minmaturity = $CFG->updateminmaturity;
2971 } else {
2972 // this can happen during the very first upgrade to 2.3
2973 $minmaturity = MATURITY_STABLE;
2974 }
2975
2976 $this->availableupdates = $provider->get_update_info($this->component,
2977 array('minmaturity' => $minmaturity));
dd119e21
DM
2978 }
2979
d26f3ddd 2980 /**
7d8de6d8 2981 * If there are updates for this plugin available, returns them.
d26f3ddd 2982 *
7d8de6d8
DM
2983 * Returns array of {@link available_update_info} objects, if some update
2984 * is available. Returns null if there is no update available or if the update
2985 * availability is unknown.
d26f3ddd 2986 *
7d8de6d8 2987 * @return array|null
d26f3ddd 2988 */
7d8de6d8 2989 public function available_updates() {
dd119e21 2990
7d8de6d8 2991 if (empty($this->availableupdates) or !is_array($this->availableupdates)) {
dd119e21
DM
2992 return null;
2993 }
2994
7d8de6d8
DM
2995 $updates = array();
2996
2997 foreach ($this->availableupdates as $availableupdate) {
2998 if ($availableupdate->version > $this->versiondisk) {
2999 $updates[] = $availableupdate;
3000 }
3001 }
3002
3003 if (empty($updates)) {
3004 return null;
dd119e21
DM
3005 }
3006
7d8de6d8 3007 return $updates;
d26f3ddd
DM
3008 }
3009
5cdb1893
MG
3010 /**
3011 * Returns the node name used in admin settings menu for this plugin settings (if applicable)
3012 *
3013 * @return null|string node name or null if plugin does not create settings node (default)
3014 */
3015 public function get_settings_section_name() {
3016 return null;
3017 }
3018
b9934a17 3019 /**
b6ad8594
DM
3020 * Returns the URL of the plugin settings screen
3021 *
3022 * Null value means that the plugin either does not have the settings screen
3023 * or its location is not available via this library.
3024 *
3025 * @return null|moodle_url
b9934a17
DM
3026 */
3027 public function get_settings_url() {
5cdb1893
MG
3028 $section = $this->get_settings_section_name();
3029 if ($section === null) {
3030 return null;
3031 }
3032 $settings = admin_get_root()->locate($section);
3033 if ($settings && $settings instanceof admin_settingpage) {
3034 return new moodle_url('/admin/settings.php', array('section' => $section));
3035 } else if ($settings && $settings instanceof admin_externalpage) {
3036 return new moodle_url($settings->url);
3037 } else {
3038 return null;
3039 }
3040 }
3041
3042 /**
3043 * Loads plugin settings to the settings tree
3044 *
3045 * This function usually includes settings.php file in plugins folder.
3046 * Alternatively it can create a link to some settings page (instance of admin_externalpage)
3047 *
3048 * @param part_of_admin_tree $adminroot
3049 * @param string $parentnodename
3050 * @param bool $hassiteconfig whether the current user has moodle/site:config capability
3051 */
3052 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
b9934a17
DM
3053 }
3054
3055 /**
73658371 3056 * Should there be a way to uninstall the plugin via the administration UI
b6ad8594 3057 *
73658371
DM
3058 * By default, uninstallation is allowed for all non-standard add-ons. Subclasses
3059 * may want to override this to allow uninstallation of all plugins (simply by
3060 * returning true unconditionally). Subplugins follow their parent plugin's
3061 * decision by default.
0b733dd9 3062 *
73658371
DM
3063 * Note that even if true is returned, the core may still prohibit the uninstallation,
3064 * e.g. in case there are other plugins that depend on this one.
b6ad8594 3065 *
bde002b8 3066 * @return bool
b9934a17 3067 */
73658371 3068 public function is_uninstall_allowed() {
0b733dd9 3069
73658371
DM
3070 if ($this->is_subplugin()) {
3071 return $this->get_plugin_manager()->get_plugin_info($this->get_parent_plugin())->is_uninstall_allowed();
0b733dd9
DM
3072 }
3073
73658371
DM
3074 if ($this->is_standard()) {
3075 return false;
0b733dd9
DM
3076 }
3077
73658371
DM
3078 return true;
3079 }
3080
2f87bb03
PS
3081 /**
3082 * Optional extra warning before uninstallation, for example number of uses in courses.
3083 *
3084 * @return string
3085 */
3086 public function get_uninstall_extra_warning() {
3087 return '';
3088 }
3089
b9934a17 3090 /**
b6ad8594
DM
3091 * Returns the URL of the screen where this plugin can be uninstalled
3092 *
3093 * Visiting that URL must be safe, that is a manual confirmation is needed
73658371
DM
3094 * for actual uninstallation of the plugin. By default, URL to a common
3095 * uninstalling tool is returned.
b6ad8594 3096 *
73658371 3097 * @return moodle_url
b9934a17
DM
3098 */
3099 public function get_uninstall_url() {
0b733dd9 3100 return $this->get_default_uninstall_url();
b9934a17
DM
3101 }
3102
a31e0b40
PS
3103 /**
3104 * Pre-uninstall hook.
3105 *
3106 * This is intended for disabling of plugin, some DB table purging, etc.
3107 *
3108 * NOTE: to be called from uninstall_plugin() only.
3109 * @private
3110 */
3111 public function uninstall_cleanup() {
3112 // Override when extending class,
3113 // do not forget to call parent::pre_uninstall_cleanup() at the end.
3114 }
3115
b9934a17 3116 /**
b6ad8594
DM
3117 * Returns relative directory of the plugin with heading '/'
3118 *
3119 * @return string
b9934a17
DM
3120 */
3121 public function get_dir() {
3122 global $CFG;
3123
3124 return substr($this->rootdir, strlen($CFG->dirroot));
3125 }
3126
436d9447
DM
3127 /**
3128 * Hook method to implement certain steps when uninstalling the plugin.
3129 *
3130 * This hook is called by {@link plugin_manager::uninstall_plugin()} so
3131 * it is basically usable only for those plugin types that use the default
3132 * uninstall tool provided by {@link self::get_default_uninstall_url()}.
3133 *
3ca1b546 3134 * @param progress_trace $progress traces the process
436d9447
DM
3135 * @return bool true on success, false on failure
3136 */
3ca1b546 3137 public function uninstall(progress_trace $progress) {
436d9447
DM
3138 return true;
3139 }
3140
0b733dd9
DM
3141 /**
3142 * Returns URL to a script that handles common plugin uninstall procedure.
3143 *
3144 * This URL is suitable for plugins that do not have their own UI
3145 * for uninstalling.
3146 *
3147 * @return moodle_url
3148 */
73658371 3149 protected final function get_default_uninstall_url() {
0b733dd9
DM
3150 return new moodle_url('/admin/plugins.php', array(
3151 'sesskey' => sesskey(),
3152 'uninstall' => $this->component,
3153 'confirm' => 0,
3154 ));
3155 }
3156
73658371
DM
3157 /**
3158 * Provides access to the plugin_manager singleton.
3159 *
bde002b8 3160 * @return plugin_manager
73658371
DM
3161 */
3162 protected function get_plugin_manager() {
3163 return plugin_manager::instance();
3164 }
b9934a17
DM
3165}
3166
b6ad8594 3167
b9934a17
DM
3168/**
3169 * General class for all plugin types that do not have their own class
3170 */
b6ad8594 3171class plugininfo_general extends plugininfo_base {
b9934a17
DM
3172}
3173
b6ad8594 3174
b9934a17
DM
3175/**
3176 * Class for page side blocks
3177 */
b6ad8594 3178class plugininfo_block extends plugininfo_base {
bde002b8
PS
3179 /**
3180 * Finds all enabled plugins, the result may include missing plugins.
3181 * @return array|null of enabled plugins $pluginname=>$pluginname, null means unknown
3182 */
3183 public static function get_enabled_plugins() {
3184 global $DB;
b9934a17 3185
bde002b8 3186 return $DB->get_records_menu('block', array('visible'=>1), 'name ASC', 'name, name AS val');
b9934a17
DM
3187 }
3188
870d4280
MG
3189 /**
3190 * Magic method getter, redirects to read only values.
3191 *
3192 * For block plugins pretends the object has 'visible' property for compatibility
3193 * with plugins developed for Moodle version below 2.4
3194 *
3195 * @param string $name
3196 * @return mixed
3197 */
3198 public function __get($name) {
3199 if ($name === 'visible') {
3200 debugging('This is now an instance of plugininfo_block, please use $block->is_enabled() instead of $block->visible', DEBUG_DEVELOPER);
3201 return ($this->is_enabled() !== false);
3202 }
3203 return parent::__get($name);
3204 }
3205
b8343e68 3206 public function init_display_name() {
b9934a17
DM
3207
3208 if (get_string_manager()->string_exists('pluginname', 'block_' . $this->name)) {
3209 $this->displayname = get_string('pluginname', 'block_' . $this->name);
3210
3211 } else if (($block = block_instance($this->name)) !== false) {
3212 $this->displayname = $block->get_title();
3213
3214 } else {
b8343e68 3215 parent::init_display_name();
b9934a17
DM
3216 }
3217 }
3218
870d4280
MG
3219 public function get_settings_section_name() {
3220 return 'blocksetting' . $this->name;
3221 }
b9934a17 3222
870d4280
MG
3223 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3224 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3225 $ADMIN = $adminroot; // may be used in settings.php
3226 $block = $this; // also can be used inside settings.php
bde002b8
PS
3227
3228 if (!$this->rootdir) {
3229 // Plugin missing.
3230 return;
3231 }
3232
870d4280 3233 $section = $this->get_settings_section_name();
b9934a17 3234
870d4280
MG
3235 if (!$hassiteconfig || (($blockinstance = block_instance($this->name)) === false)) {
3236 return;
3237 }
b9934a17 3238
870d4280
MG
3239 $settings = null;
3240 if ($blockinstance->has_config()) {
6740c605 3241 if (file_exists($this->full_path('settings.php'))) {
870d4280
MG
3242 $settings = new admin_settingpage($section, $this->displayname,
3243 'moodle/site:config', $this->is_enabled() === false);
3244 include($this->full_path('settings.php')); // this may also set $settings to null
b9934a17 3245 }
870d4280
MG
3246 }
3247 if ($settings) {
3248 $ADMIN->add($parentnodename, $settings);
b9934a17
DM
3249 }
3250 }
3251
73658371
DM
3252 public function is_uninstall_allowed() {
3253 return true;
3254 }
b9934a17 3255
6584d8a8
PS
3256 /**
3257 * Warnign with number of block instances.
3258 *
3259 * @return string
3260 */
3261 public function get_uninstall_extra_warning() {
3262 global $DB;
3263
3264 if (!$count = $DB->count_records('block_instances', array('blockname'=>$this->name))) {
3265 return '';
3266 }
3267
3268 return '<p>'.get_string('uninstallextraconfirmblock', 'core_plugin', array('instances'=>$count)).'</p>';
b9934a17 3269 }
bde002b8 3270}
b9934a17 3271
b6ad8594 3272
b9934a17
DM
3273/**
3274 * Class for text filters
3275 */
b6ad8594 3276class plugininfo_filter extends plugininfo_base {
b9934a17 3277
b8343e68 3278 public function init_display_name() {
bde002b8
PS
3279 if (!get_string_manager()->string_exists('filtername', $this->component)) {
3280 $this->displayname = '[filtername,' . $this->component . ']';
3281 } else {
3282 $this->displayname = get_string('filtername', $this->component);
3283 }
b9934a17
DM
3284 }
3285
bde002b8
PS
3286 /**
3287 * Finds all enabled plugins, the result may include missing plugins.
3288 * @return array|null of enabled plugins $pluginname=>$pluginname, null means unknown
3289 */
3290 public static function get_enabled_plugins() {
3291 global $DB, $CFG;
3292 require_once("$CFG->libdir/filterlib.php");
b9934a17 3293
bde002b8
PS
3294 $enabled = array();
3295 $filters = $DB->get_records_select('filter_active', "active <> :disabled", array('disabled'=>TEXTFILTER_DISABLED), 'filter ASC', 'id, filter');
3296 foreach ($filters as $filter) {
3297 $enabled[$filter->filter] = $filter->filter;
b9934a17
DM
3298 }
3299
bde002b8 3300 return $enabled;
b9934a17
DM
3301 }
3302
1de1a666 3303 public function get_settings_section_name() {
0662bd67 3304 return 'filtersetting' . $this->name;
1de1a666
MG
3305 }
3306
3307 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3308 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3309 $ADMIN = $adminroot; // may be used in settings.php
3310 $filter = $this; // also can be used inside settings.php
3311
bde002b8
PS
3312 if (!$this->rootdir) {
3313 // Plugin missing.
3314 return;
3315 }
3316
1de1a666 3317 $settings = null;
8d211302 3318 if ($hassiteconfig && file_exists($this->full_path('filtersettings.php'))) {
1de1a666
MG
3319 $section = $this->get_settings_section_name();
3320 $settings = new admin_settingpage($section, $this->displayname,
3321 'moodle/site:config', $this->is_enabled() === false);
3322 include($this->full_path('filtersettings.php')); // this may also set $settings to null
3323 }
3324 if ($settings) {
3325 $ADMIN->add($parentnodename, $settings);
b9934a17
DM
3326 }
3327 }
3328
73658371
DM
3329 public function is_uninstall_allowed() {
3330 return true;
3331 }
3332
a31e0b40
PS
3333 /**
3334 * Pre-uninstall hook.
3335 *
3336 * This is intended for disabling of plugin, some DB table purging, etc.
3337 *
3338 * NOTE: to be called from uninstall_plugin() only.
3339 * @private
3340 */
3341 public function uninstall_cleanup() {
3342 global $DB;
3343
3344 $DB->delete_records('filter_active', array('filter' => $this->name));
3345 $DB->delete_records('filter_config', array('filter' => $this->name));
3346
3347 parent::uninstall_cleanup();
b9934a17 3348 }
b9934a17
DM
3349}
3350
b6ad8594 3351
b9934a17
DM
3352/**
3353 * Class for activity modules
3354 */
b6ad8594 3355class plugininfo_mod extends plugininfo_base {
bde002b8
PS
3356 /**
3357 * Finds all enabled plugins, the result may include missing plugins.
3358 * @return array|null of enabled plugins $pluginname=>$pluginname, null means unknown
3359 */
3360 public static function get_enabled_plugins() {
3361 global $DB;
3362 return $DB->get_records_menu('modules', array('visible'=>1), 'name ASC', 'name, name AS val');
b9934a17
DM
3363 }
3364
fde6f79f
MG
3365 /**
3366 * Magic method getter, redirects to read only values.
3367 *
3368 * For module plugins we pretend the object has 'visible' property for compatibility
3369 * with plugins developed for Moodle version below 2.4
3370 *
3371 * @param string $name
3372 * @return mixed
3373 */
3374 public function __get($name) {
3375 if ($name === 'visible') {
3376 debugging('This is now an instance of plugininfo_mod, please use $module->is_enabled() instead of $module->visible', DEBUG_DEVELOPER);
3377 return ($this->is_enabled() !== false);
3378 }
3379 return parent::__get($name);
3380 }
3381
b8343e68 3382 public function init_display_name() {
828788f0
TH
3383 if (get_string_manager()->string_exists('pluginname', $this->component)) {
3384 $this->displayname = get_string('pluginname', $this->component);
b9934a17 3385 } else {
828788f0 3386 $this->displayname = get_string('modulename', $this->component);
b9934a17
DM
3387 }
3388 }
3389
fde6f79f
MG
3390 public function get_settings_section_name() {
3391 return 'modsetting' . $this->name;
3392 }
b9934a17 3393
fde6f79f
MG
3394 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3395 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3396 $ADMIN = $adminroot; // may be used in settings.php
3397 $module = $this; // also can be used inside settings.php
bde002b8
PS
3398
3399 if (!$this->rootdir) {
3400 // Plugin missing.
3401 return;
3402 }
3403
fde6f79f
MG
3404 $section = $this->get_settings_section_name();
3405
3406 $settings = null;
bde002b8 3407 if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
fde6f79f
MG
3408 $settings = new admin_settingpage($section, $this->displayname,
3409 'moodle/site:config', $this->is_enabled() === false);
3410 include($this->full_path('settings.php')); // this may also set $settings to null
3411 }
3412 if ($settings) {
3413 $ADMIN->add($parentnodename, $settings);
b9934a17
DM
3414 }
3415 }
3416
73658371
DM
3417 /**
3418 * Allow all activity modules but Forum to be uninstalled.
b9934a17 3419
73658371
DM
3420 * This exception for the Forum has been hard-coded in Moodle since ages,
3421 * we may want to re-think it one day.
3422 */
3423 public function is_uninstall_allowed() {
3424 if ($this->name === 'forum') {
3425 return false;
b9934a17 3426 } else {
73658371 3427 return true;
b9934a17
DM
3428 }
3429 }
3430
cd79930e
PS
3431 /**
3432 * Return warning with number of activities and number of affected courses.
3433 *
3434 * @return string
3435 */
3436 public function get_uninstall_extra_warning() {
3437 global $DB;
3438
3439 if (!$module = $DB->get_record('modules', array('name'=>$this->name))) {
3440 return '';
3441 }
3442
3443 if (!$count = $DB->count_records('course_modules', array('module'=>$module->id))) {
3444 return '';
3445 }
3446
02a541ee
PS
3447 $sql = "SELECT COUNT('x')
3448 FROM (
3449 SELECT course
3450 FROM {course_modules}
3451 WHERE module = :mid
3452 GROUP BY course
3453 ) c";
cd79930e
PS
3454 $courses = $DB->count_records_sql($sql, array('mid'=>$module->id));
3455
3456 return '<p>'.get_string('uninstallextraconfirmmod', 'core_plugin', array('instances'=>$count, 'courses'=>$courses)).'</p>';
73658371 3457 }
b9934a17
DM
3458}
3459
0242bdc7
TH
3460
3461/**
3462 * Class for question behaviours.
3463 */
b6ad8594
DM
3464class plugininfo_qbehaviour extends plugininfo_base {
3465
73658371
DM
3466 public function is_uninstall_allowed() {
3467 return true;
3468 }
3469
828788f0
TH
3470 public function get_uninstall_url() {
3471 return new moodle_url('/admin/qbehaviours.php',
3472 array('delete' => $this->name, 'sesskey' => sesskey()));
3473 }
0242bdc7
TH
3474}
3475
3476
b9934a17
DM
3477/**
3478 * Class for question types
3479 */
b6ad8594
DM
3480class plugininfo_qtype extends plugininfo_base {
3481
73658371
DM
3482 public function is_uninstall_allowed() {
3483 return true;
3484 }
3485
828788f0
TH
3486 public function get_uninstall_url() {
3487 return new moodle_url('/admin/qtypes.php',
3488 array('delete' => $this->name, 'sesskey' => sesskey()));
3489 }
66f3684a
MG
3490
3491 public function get_settings_section_name() {
3492 return 'qtypesetting' . $this->name;
3493 }
3494
3495 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3496 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3497 $ADMIN = $adminroot; // may be used in settings.php
3498 $qtype = $this; // also can be used inside settings.php
bde002b8
PS
3499
3500 if (!$this->rootdir) {
3501 // Most probably somebody deleted dir without proper uninstall.
3502 return;
3503 }
66f3684a
MG
3504 $section = $this->get_settings_section_name();
3505
3506 $settings = null;
837e1812
TH
3507 $systemcontext = context_system::instance();
3508 if (($hassiteconfig || has_capability('moodle/question:config', $systemcontext)) &&
3509 file_exists($this->full_path('settings.php'))) {
66f3684a 3510 $settings = new admin_settingpage($section, $this->displayname,
837e1812 3511 'moodle/question:config', $this->is_enabled() === false);
66f3684a
MG
3512 include($this->full_path('settings.php')); // this may also set $settings to null
3513 }
3514 if ($settings) {
3515 $ADMIN->add($parentnodename, $settings);
3516 }
3517 }
b9934a17
DM
3518}
3519
b9934a17
DM
3520
3521/**
3522 * Class for authentication plugins
3523 */
b6ad8594 3524class plugininfo_auth extends plugininfo_base {
bde002b8
PS
3525 /**
3526 * Finds all enabled plugins, the result may include missing plugins.
3527 * @return array|null of enabled plugins $pluginname=>$pluginname, null means unknown
3528 */
3529 public static function get_enabled_plugins() {
b9934a17 3530 global $CFG;
b9934a17 3531
bde002b8
PS
3532 // These two are always enabled and can't be disabled.
3533 $enabled = array('nologin'=>'nologin', 'manual'=>'manual');
3534 foreach (explode(',', $CFG->auth) as $auth) {
3535 $enabled[$auth] = $auth;
b9934a17
DM
3536 }
3537
bde002b8 3538 return $enabled;
b9934a17
DM
3539 }
3540
cbe9f609
MG
3541 public function get_settings_section_name() {
3542 return 'authsetting' . $this->name;
3543 }
3544
3545 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3546 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3547 $ADMIN = $adminroot; // may be used in settings.php
3548 $auth = $this; // also to be used inside settings.php
bde002b8
PS
3549
3550 if (!$this->rootdir) {
3551 // Plugin missing.
3552 return;
3553 }
3554
cbe9f609
MG
3555 $section = $this->get_settings_section_name();
3556
3557 $settings = null;
3558 if ($hassiteconfig) {
3559 if (file_exists($this->full_path('settings.php'))) {
3560 // TODO: finish implementation of common settings - locking, etc.
3561 $settings = new admin_settingpage($section, $this->displayname,
3562 'moodle/site:config', $this->is_enabled() === false);
3563 include($this->full_path('settings.php')); // this may also set $settings to null
3564 } else {
3565 $settingsurl = new moodle_url('/admin/auth_config.php', array('auth' => $this->name));
3566 $settings = new admin_externalpage($section, $this->displayname,
3567 $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
3568 }
3569 }
3570 if ($settings) {
3571 $ADMIN->add($parentnodename, $settings);
b9934a17
DM
3572 }
3573 }
3574}
3575
b6ad8594 3576
b9934a17
DM
3577/**
3578 * Class for enrolment plugins
3579 */
b6ad8594 3580class plugininfo_enrol extends plugininfo_base {
bde002b8
PS
3581 /**
3582 * Finds all enabled plugins, the result may include missing plugins.
3583 * @return array|null of enabled plugins $pluginname=>$pluginname, null means unknown
3584 */
3585 public static function get_enabled_plugins() {
b9934a17 3586 global $CFG;
b9934a17 3587
bde002b8
PS
3588 $enabled = array();
3589 foreach (explode(',', $CFG->enrol_plugins_enabled) as $enrol) {
3590 $enabled[$enrol] = $enrol;
3591 }
b9934a17 3592
bde002b8 3593 return $enabled;
b9934a17
DM
3594 }
3595
79c5c3fa 3596 public function get_settings_section_name() {
c7a33990
PS
3597 if (file_exists($this->full_path('settings.php'))) {
3598 return 'enrolsettings' . $this->name;
3599 } else {
3600 return null;
3601 }
79c5c3fa 3602 }
b9934a17 3603
79c5c3fa
MG
3604 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3605 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
c7a33990 3606
bde002b8
PS
3607 if (!$this->rootdir) {
3608 // Plugin missing.
3609 return;