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