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