weekly release 2.6dev
[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(
4f100820 812 'afterburner', 'anomaly', 'arialist', 'base', 'binarius', 'bootstrapbase',
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
4785c45d
DM
1432 return $options;
1433 }
1434
be378880
DM
1435 /**
1436 * Returns the current timestamp
1437 *
1438 * @return int the timestamp
1439 */
1440 protected function cron_current_timestamp() {
1441 return time();
1442 }
1443
1444 /**
1445 * Output cron debugging info
1446 *
1447 * @see mtrace()
1448 * @param string $msg output message
1449 * @param string $eol end of line
1450 */
1451 protected function cron_mtrace($msg, $eol = PHP_EOL) {
1452 mtrace($msg, $eol);
1453 }
1454
1455 /**
1456 * Decide if the autocheck feature is disabled in the server setting
1457 *
1458 * @return bool true if autocheck enabled, false if disabled
1459 */
1460 protected function cron_autocheck_enabled() {
718eb2a5
DM
1461 global $CFG;
1462
be378880
DM
1463 if (empty($CFG->updateautocheck)) {
1464 return false;
1465 } else {
1466 return true;
1467 }
1468 }
1469
1470 /**
1471 * Decide if the recently fetched data are still fresh enough
1472 *
1473 * @param int $now current timestamp
1474 * @return bool true if no need to re-fetch, false otherwise
1475 */
1476 protected function cron_has_fresh_fetch($now) {
1477 $recent = $this->get_last_timefetched();
1478
1479 if (empty($recent)) {
1480 return false;
1481 }
1482
1483 if ($now < $recent) {
1484 $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1485 return true;
1486 }
1487
7092ea5d 1488 if ($now - $recent > 24 * HOURSECS) {
be378880
DM
1489 return false;
1490 }
1491
1492 return true;
1493 }
1494
1495 /**
1496 * Decide if the fetch is outadated or even missing
1497 *
1498 * @param int $now current timestamp
1499 * @return bool false if no need to re-fetch, true otherwise
1500 */
1501 protected function cron_has_outdated_fetch($now) {
1502 $recent = $this->get_last_timefetched();
1503
1504 if (empty($recent)) {
1505 return true;
1506 }
1507
1508 if ($now < $recent) {
1509 $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1510 return false;
1511 }
1512
1513 if ($now - $recent > 48 * HOURSECS) {
1514 return true;
1515 }
1516
1517 return false;
1518 }
1519
1520 /**
1521 * Returns the cron execution offset for this site
1522 *
1523 * The main {@link self::cron()} is supposed to run every night in some random time
1524 * between 01:00 and 06:00 AM (local time). The exact moment is defined by so called
1525 * execution offset, that is the amount of time after 01:00 AM. The offset value is
1526 * initially generated randomly and then used consistently at the site. This way, the
1527 * regular checks against the download.moodle.org server are spread in time.
1528 *
1529 * @return int the offset number of seconds from range 1 sec to 5 hours
1530 */
1531 protected function cron_execution_offset() {
1532 global $CFG;
1533
1534 if (empty($CFG->updatecronoffset)) {
1535 set_config('updatecronoffset', rand(1, 5 * HOURSECS));
1536 }
1537
1538 return $CFG->updatecronoffset;
1539 }
1540
1541 /**
1542 * Fetch available updates info and eventually send notification to site admins
1543 */
1544 protected function cron_execute() {
7b35553b 1545
19d11b3b 1546 try {
fd87d0bf
AB
1547 $this->restore_response();
1548 $previous = $this->recentresponse;
1549 $this->fetch();
1550 $this->restore_response(true);
1551 $current = $this->recentresponse;
19d11b3b
DM
1552 $changes = $this->compare_responses($previous, $current);
1553 $notifications = $this->cron_notifications($changes);
1554 $this->cron_notify($notifications);
a77141a7 1555 $this->cron_mtrace('done');
19d11b3b
DM
1556 } catch (available_update_checker_exception $e) {
1557 $this->cron_mtrace('FAILED!');
1558 }
1559 }
1560
1561 /**
1562 * Given the list of changes in available updates, pick those to send to site admins
1563 *
1564 * @param array $changes as returned by {@link self::compare_responses()}
1565 * @return array of available_update_info objects to send to site admins
1566 */
1567 protected function cron_notifications(array $changes) {
1568 global $CFG;
1569
1570 $notifications = array();
1571 $pluginman = plugin_manager::instance();
1572 $plugins = $pluginman->get_plugins(true);
1573
1574 foreach ($changes as $component => $componentchanges) {
718eb2a5
DM
1575 if (empty($componentchanges)) {
1576 continue;
1577 }
19d11b3b
DM
1578 $componentupdates = $this->get_update_info($component,
1579 array('minmaturity' => $CFG->updateminmaturity, 'notifybuilds' => $CFG->updatenotifybuilds));
718eb2a5
DM
1580 if (empty($componentupdates)) {
1581 continue;
1582 }
19d11b3b
DM
1583 // notify only about those $componentchanges that are present in $componentupdates
1584 // to respect the preferences
1585 foreach ($componentchanges as $componentchange) {
1586 foreach ($componentupdates as $componentupdate) {
1587 if ($componentupdate->version == $componentchange['version']) {
1588 if ($component == 'core') {
fa1415f1
DM
1589 // In case of 'core', we already know that the $componentupdate
1590 // is a real update with higher version ({@see self::get_update_info()}).
1591 // We just perform additional check for the release property as there
1592 // can be two Moodle releases having the same version (e.g. 2.4.0 and 2.5dev shortly
1593 // after the release). We can do that because we have the release info
1594 // always available for the core.
1595 if ((string)$componentupdate->release === (string)$componentchange['release']) {
1596 $notifications[] = $componentupdate;
1597 }
19d11b3b 1598 } else {
d2713eff
DM
1599 // Use the plugin_manager to check if the detected $componentchange
1600 // is a real update with higher version. That is, the $componentchange
1601 // is present in the array of {@link available_update_info} objects
1602 // returned by the plugin's available_updates() method.
19d11b3b 1603 list($plugintype, $pluginname) = normalize_component($component);
d2713eff
DM
1604 if (!empty($plugins[$plugintype][$pluginname])) {
1605 $availableupdates = $plugins[$plugintype][$pluginname]->available_updates();
1606 if (!empty($availableupdates)) {
1607 foreach ($availableupdates as $availableupdate) {
1608 if ($availableupdate->version == $componentchange['version']) {
1609 $notifications[] = $componentupdate;
1610 }
19d11b3b
DM
1611 }
1612 }
1613 }
1614 }
1615 }
1616 }
1617 }
1618 }
1619
1620 return $notifications;
be378880 1621 }
a77141a7
DM
1622
1623 /**
1624 * Sends the given notifications to site admins via messaging API
1625 *
1626 * @param array $notifications array of available_update_info objects to send
1627 */
1628 protected function cron_notify(array $notifications) {
1629 global $CFG;
1630
1631 if (empty($notifications)) {
1632 return;
1633 }
1634
1635 $admins = get_admins();
1636
1637 if (empty($admins)) {
1638 return;
1639 }
1640
1641 $this->cron_mtrace('sending notifications ... ', '');
1642
1643 $text = get_string('updatenotifications', 'core_admin') . PHP_EOL;
1644 $html = html_writer::tag('h1', get_string('updatenotifications', 'core_admin')) . PHP_EOL;
1645
1646 $coreupdates = array();
1647 $pluginupdates = array();
1648
660c4d46 1649 foreach ($notifications as $notification) {
a77141a7
DM
1650 if ($notification->component == 'core') {
1651 $coreupdates[] = $notification;
1652 } else {
1653 $pluginupdates[] = $notification;
1654 }
1655 }
1656
1657 if (!empty($coreupdates)) {
1658 $text .= PHP_EOL . get_string('updateavailable', 'core_admin') . PHP_EOL;
1659 $html .= html_writer::tag('h2', get_string('updateavailable', 'core_admin')) . PHP_EOL;
1660 $html .= html_writer::start_tag('ul') . PHP_EOL;
1661 foreach ($coreupdates as $coreupdate) {
1662 $html .= html_writer::start_tag('li');
1663 if (isset($coreupdate->release)) {
1664 $text .= get_string('updateavailable_release', 'core_admin', $coreupdate->release);
1665 $html .= html_writer::tag('strong', get_string('updateavailable_release', 'core_admin', $coreupdate->release));
1666 }
1667 if (isset($coreupdate->version)) {
1668 $text .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
1669 $html .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
1670 }
1671 if (isset($coreupdate->maturity)) {
1672 $text .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
1673 $html .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
1674 }
1675 $text .= PHP_EOL;
1676 $html .= html_writer::end_tag('li') . PHP_EOL;
1677 }
1678 $text .= PHP_EOL;
1679 $html .= html_writer::end_tag('ul') . PHP_EOL;
1680
1681 $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/index.php');
1682 $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
1683 $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/index.php', $CFG->wwwroot.'/'.$CFG->admin.'/index.php'));
1684 $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
1685 }
1686
1687 if (!empty($pluginupdates)) {
1688 $text .= PHP_EOL . get_string('updateavailableforplugin', 'core_admin') . PHP_EOL;
1689 $html .= html_writer::tag('h2', get_string('updateavailableforplugin', 'core_admin')) . PHP_EOL;
1690
1691 $html .= html_writer::start_tag('ul') . PHP_EOL;
1692 foreach ($pluginupdates as $pluginupdate) {
1693 $html .= html_writer::start_tag('li');
1694 $text .= get_string('pluginname', $pluginupdate->component);
1695 $html .= html_writer::tag('strong', get_string('pluginname', $pluginupdate->component));
1696
1697 $text .= ' ('.$pluginupdate->component.')';
1698 $html .= ' ('.$pluginupdate->component.')';
1699
1700 $text .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
1701 $html .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
1702
1703 $text .= PHP_EOL;
1704 $html .= html_writer::end_tag('li') . PHP_EOL;
1705 }
1706 $text .= PHP_EOL;
1707 $html .= html_writer::end_tag('ul') . PHP_EOL;
b9934a17 1708
a77141a7
DM
1709 $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php');
1710 $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
1711 $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/plugins.php', $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php'));
1712 $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
1713 }
1714
1715 $a = array('siteurl' => $CFG->wwwroot);
1716 $text .= get_string('updatenotificationfooter', 'core_admin', $a) . PHP_EOL;
1717 $a = array('siteurl' => html_writer::link($CFG->wwwroot, $CFG->wwwroot));
1718 $html .= html_writer::tag('footer', html_writer::tag('p', get_string('updatenotificationfooter', 'core_admin', $a),
1719 array('style' => 'font-size:smaller; color:#333;')));
1720
a77141a7
DM
1721 foreach ($admins as $admin) {
1722 $message = new stdClass();
1723 $message->component = 'moodle';
1724 $message->name = 'availableupdate';
55079015 1725 $message->userfrom = get_admin();
a77141a7 1726 $message->userto = $admin;
2399585f 1727 $message->subject = get_string('updatenotificationsubject', 'core_admin', array('siteurl' => $CFG->wwwroot));
a77141a7
DM
1728 $message->fullmessage = $text;
1729 $message->fullmessageformat = FORMAT_PLAIN;
1730 $message->fullmessagehtml = $html;
cd89994d
DM
1731 $message->smallmessage = get_string('updatenotifications', 'core_admin');
1732 $message->notification = 1;
a77141a7
DM
1733 message_send($message);
1734 }
1735 }
b9934a17
DM
1736
1737 /**
4442cc80 1738 * Compare two release labels and decide if they are the same
b9934a17 1739 *
4442cc80
DM
1740 * @param string $remote release info of the available update
1741 * @param null|string $local release info of the local code, defaults to $release defined in version.php
1742 * @return boolean true if the releases declare the same minor+major version
b9934a17 1743 */
4442cc80 1744 protected function is_same_release($remote, $local=null) {
b9934a17 1745
4442cc80
DM
1746 if (is_null($local)) {
1747 $this->load_current_environment();
1748 $local = $this->currentrelease;
1749 }
0242bdc7 1750
4442cc80 1751 $pattern = '/^([0-9\.\+]+)([^(]*)/';
b9934a17 1752
4442cc80
DM
1753 preg_match($pattern, $remote, $remotematches);
1754 preg_match($pattern, $local, $localmatches);
b9934a17 1755
4442cc80
DM
1756 $remotematches[1] = str_replace('+', '', $remotematches[1]);
1757 $localmatches[1] = str_replace('+', '', $localmatches[1]);
1758
1759 if ($remotematches[1] === $localmatches[1] and rtrim($remotematches[2]) === rtrim($localmatches[2])) {
1760 return true;
1761 } else {
1762 return false;
1763 }
1764 }
cd0bb55f
DM
1765}
1766
1767
7d8de6d8
DM
1768/**
1769 * Defines the structure of objects returned by {@link available_update_checker::get_update_info()}
1770 */
1771class available_update_info {
1772
1773 /** @var string frankenstyle component name */
1774 public $component;
1775 /** @var int the available version of the component */
1776 public $version;
1777 /** @var string|null optional release name */
1778 public $release = null;
1779 /** @var int|null optional maturity info, eg {@link MATURITY_STABLE} */
1780 public $maturity = null;
1781 /** @var string|null optional URL of a page with more info about the update */
1782 public $url = null;
1783 /** @var string|null optional URL of a ZIP package that can be downloaded and installed */
1784 public $download = null;
6b75106a
DM
1785 /** @var string|null of self::download is set, then this must be the MD5 hash of the ZIP */
1786 public $downloadmd5 = null;
7d8de6d8
DM
1787
1788 /**
1789 * Creates new instance of the class
b9934a17 1790 *
7d8de6d8
DM
1791 * The $info array must provide at least the 'version' value and optionally all other
1792 * values to populate the object's properties.
b9934a17 1793 *
7d8de6d8
DM
1794 * @param string $name the frankenstyle component name
1795 * @param array $info associative array with other properties
1796 */
1797 public function __construct($name, array $info) {
1798 $this->component = $name;
1799 foreach ($info as $k => $v) {
1800 if (property_exists('available_update_info', $k) and $k != 'component') {
1801 $this->$k = $v;
1802 }
1803 }
1804 }
1805}
1806
1807
7683e550
DM
1808/**
1809 * Implements a communication bridge to the mdeploy.php utility
1810 */
1811class available_update_deployer {
1812
1813 const HTTP_PARAM_PREFIX = 'updteautodpldata_'; // Hey, even Google has not heard of such a prefix! So it MUST be safe :-p
1814 const HTTP_PARAM_CHECKER = 'datapackagesize'; // Name of the parameter that holds the number of items in the received data items
1815
1816 /** @var available_update_deployer holds the singleton instance */
1817 protected static $singletoninstance;
1818 /** @var moodle_url URL of a page that includes the deployer UI */
1819 protected $callerurl;
1820 /** @var moodle_url URL to return after the deployment */
1821 protected $returnurl;
1822
1823 /**
1824 * Direct instantiation not allowed, use the factory method {@link self::instance()}
1825 */
1826 protected function __construct() {
1827 }
1828
1829 /**
1830 * Sorry, this is singleton
1831 */
1832 protected function __clone() {
1833 }
1834
1835 /**
1836 * Factory method for this class
1837 *
1838 * @return available_update_deployer the singleton instance
1839 */
1840 public static function instance() {
1841 if (is_null(self::$singletoninstance)) {
1842 self::$singletoninstance = new self();
1843 }
1844 return self::$singletoninstance;
1845 }
1846
dc11af19
DM
1847 /**
1848 * Reset caches used by this script
1849 *
1850 * @param bool $phpunitreset is this called as a part of PHPUnit reset?
1851 */
1852 public static function reset_caches($phpunitreset = false) {
1853 if ($phpunitreset) {
1854 self::$singletoninstance = null;
1855 }
1856 }
1857
7683e550
DM
1858 /**
1859 * Is automatic deployment enabled?
1860 *
1861 * @return bool
1862 */
1863 public function enabled() {
1864 global $CFG;
1865
1866 if (!empty($CFG->disableupdateautodeploy)) {
1867 // The feature is prohibited via config.php
1868 return false;
1869 }
1870
1871 return get_config('updateautodeploy');
1872 }
1873
1874 /**
1875 * Sets some base properties of the class to make it usable.
1876 *
1877 * @param moodle_url $callerurl the base URL of a script that will handle the class'es form data
1878 * @param moodle_url $returnurl the final URL to return to when the deployment is finished
1879 */
1880 public function initialize(moodle_url $callerurl, moodle_url $returnurl) {
1881
1882 if (!$this->enabled()) {
1883 throw new coding_exception('Unable to initialize the deployer, the feature is not enabled.');
1884 }
1885
1886 $this->callerurl = $callerurl;
1887 $this->returnurl = $returnurl;
1888 }
1889
1890 /**
1891 * Has the deployer been initialized?
1892 *
1893 * Initialized deployer means that the following properties were set:
1894 * callerurl, returnurl
1895 *
1896 * @return bool
1897 */
1898 public function initialized() {
1899
1900 if (!$this->enabled()) {
1901 return false;
1902 }
1903
1904 if (empty($this->callerurl)) {
1905 return false;
1906 }
1907
1908 if (empty($this->returnurl)) {
1909 return false;
1910 }
1911
1912 return true;
1913 }
1914
1915 /**
0daa6428 1916 * Returns a list of reasons why the deployment can not happen
7683e550 1917 *
0daa6428
DM
1918 * If the returned array is empty, the deployment seems to be possible. The returned
1919 * structure is an associative array with keys representing individual impediments.
1920 * Possible keys are: missingdownloadurl, missingdownloadmd5, notwritable.
7683e550
DM
1921 *
1922 * @param available_update_info $info
0daa6428 1923 * @return array
7683e550 1924 */
0daa6428
DM
1925 public function deployment_impediments(available_update_info $info) {
1926
1927 $impediments = array();
7683e550
DM
1928
1929 if (empty($info->download)) {
0daa6428 1930 $impediments['missingdownloadurl'] = true;
7683e550
DM
1931 }
1932
6b75106a 1933 if (empty($info->downloadmd5)) {
0daa6428 1934 $impediments['missingdownloadmd5'] = true;
6b75106a
DM
1935 }
1936
30e26827
DM
1937 if (!empty($info->download) and !$this->update_downloadable($info->download)) {
1938 $impediments['notdownloadable'] = true;
1939 }
1940
0daa6428
DM
1941 if (!$this->component_writable($info->component)) {
1942 $impediments['notwritable'] = true;
1943 }
1944
1945 return $impediments;
7683e550
DM
1946 }
1947
08c3bc00
DM
1948 /**
1949 * Check to see if the current version of the plugin seems to be a checkout of an external repository.
1950 *
436d9447 1951 * @see plugin_manager::plugin_external_source()
08c3bc00
DM
1952 * @param available_update_info $info
1953 * @return false|string
1954 */
1955 public function plugin_external_source(available_update_info $info) {
1956
1957 $paths = get_plugin_types(true);
1958 list($plugintype, $pluginname) = normalize_component($info->component);
1959 $pluginroot = $paths[$plugintype].'/'.$pluginname;
1960
1961 if (is_dir($pluginroot.'/.git')) {
1962 return 'git';
1963 }
1964
1965 if (is_dir($pluginroot.'/CVS')) {
1966 return 'cvs';
1967 }
1968
1969 if (is_dir($pluginroot.'/.svn')) {
1970 return 'svn';
1971 }
1972
1973 return false;
1974 }
1975
7683e550
DM
1976 /**
1977 * Prepares a renderable widget to confirm installation of an available update.
1978 *
1979 * @param available_update_info $info component version to deploy
1980 * @return renderable
1981 */
1982 public function make_confirm_widget(available_update_info $info) {
1983
1984 if (!$this->initialized()) {
1985 throw new coding_exception('Illegal method call - deployer not initialized.');
1986 }
1987
1988 $params = $this->data_to_params(array(
1989 'updateinfo' => (array)$info, // see http://www.php.net/manual/en/language.types.array.php#language.types.array.casting
1990 ));
1991
1992 $widget = new single_button(
1993 new moodle_url($this->callerurl, $params),
1994 get_string('updateavailableinstall', 'core_admin'),
1995 'post'
1996 );
1997
1998 return $widget;
1999 }
2000
2001 /**
2002 * Prepares a renderable widget to execute installation of an available update.
2003 *
2004 * @param available_update_info $info component version to deploy
5d7a4bab 2005 * @param moodle_url $returnurl URL to return after the installation execution
7683e550
DM
2006 * @return renderable
2007 */
5d7a4bab 2008 public function make_execution_widget(available_update_info $info, moodle_url $returnurl = null) {
7683e550
DM
2009 global $CFG;
2010
2011 if (!$this->initialized()) {
2012 throw new coding_exception('Illegal method call - deployer not initialized.');
2013 }
2014
2015 $pluginrootpaths = get_plugin_types(true);
2016
2017 list($plugintype, $pluginname) = normalize_component($info->component);
2018
2019 if (empty($pluginrootpaths[$plugintype])) {
2020 throw new coding_exception('Unknown plugin type root location', $plugintype);
2021 }
2022
3daedb5c
DM
2023 list($passfile, $password) = $this->prepare_authorization();
2024
5d7a4bab
DM
2025 if (is_null($returnurl)) {
2026 $returnurl = new moodle_url('/admin');
2027 } else {
2028 $returnurl = $returnurl;
2029 }
23137c4a 2030
7683e550
DM
2031 $params = array(
2032 'upgrade' => true,
2033 'type' => $plugintype,
2034 'name' => $pluginname,
2035 'typeroot' => $pluginrootpaths[$plugintype],
4c72f555 2036 'package' => $info->download,
6b75106a 2037 'md5' => $info->downloadmd5,
7683e550
DM
2038 'dataroot' => $CFG->dataroot,
2039 'dirroot' => $CFG->dirroot,
3daedb5c
DM
2040 'passfile' => $passfile,
2041 'password' => $password,
5d7a4bab 2042 'returnurl' => $returnurl->out(false),
7683e550
DM
2043 );
2044
63def597 2045 if (!empty($CFG->proxyhost)) {
0dcae7cd
DP
2046 // MDL-36973 - Beware - we should call just !is_proxybypass() here. But currently, our
2047 // cURL wrapper class does not do it. So, to have consistent behaviour, we pass proxy
2048 // setting regardless the $CFG->proxybypass setting. Once the {@link curl} class is
2049 // fixed, the condition should be amended.
63def597
DM
2050 if (true or !is_proxybypass($info->download)) {
2051 if (empty($CFG->proxyport)) {
2052 $params['proxy'] = $CFG->proxyhost;
2053 } else {
2054 $params['proxy'] = $CFG->proxyhost.':'.$CFG->proxyport;
2055 }
2056
2057 if (!empty($CFG->proxyuser) and !empty($CFG->proxypassword)) {
2058 $params['proxyuserpwd'] = $CFG->proxyuser.':'.$CFG->proxypassword;
2059 }
2060
2061 if (!empty($CFG->proxytype)) {
2062 $params['proxytype'] = $CFG->proxytype;
2063 }
2064 }
2065 }
2066
7683e550
DM
2067 $widget = new single_button(
2068 new moodle_url('/mdeploy.php', $params),
2069 get_string('updateavailableinstall', 'core_admin'),
2070 'post'
2071 );
2072
2073 return $widget;
2074 }
2075
2076 /**
2077 * Returns array of data objects passed to this tool.
2078 *
2079 * @return array
2080 */
2081 public function submitted_data() {
2082
2083 $data = $this->params_to_data($_POST);
2084
2085 if (empty($data) or empty($data[self::HTTP_PARAM_CHECKER])) {
2086 return false;
2087 }
2088
2089 if (!empty($data['updateinfo']) and is_object($data['updateinfo'])) {
2090 $updateinfo = $data['updateinfo'];
2091 if (!empty($updateinfo->component) and !empty($updateinfo->version)) {
2092 $data['updateinfo'] = new available_update_info($updateinfo->component, (array)$updateinfo);
2093 }
2094 }
2095
2096 if (!empty($data['callerurl'])) {
2097 $data['callerurl'] = new moodle_url($data['callerurl']);
2098 }
2099
2100 if (!empty($data['returnurl'])) {
2101 $data['returnurl'] = new moodle_url($data['returnurl']);
2102 }
2103
2104 return $data;
2105 }
2106
2107 /**
2108 * Handles magic getters and setters for protected properties.
2109 *
2110 * @param string $name method name, e.g. set_returnurl()
2111 * @param array $arguments arguments to be passed to the array
2112 */
2113 public function __call($name, array $arguments = array()) {
2114
2115 if (substr($name, 0, 4) === 'set_') {
2116 $property = substr($name, 4);
2117 if (empty($property)) {
2118 throw new coding_exception('Invalid property name (empty)');
2119 }
2120 if (empty($arguments)) {
2121 $arguments = array(true); // Default value for flag-like properties.
2122 }
2123 // Make sure it is a protected property.
2124 $isprotected = false;
2125 $reflection = new ReflectionObject($this);
2126 foreach ($reflection->getProperties(ReflectionProperty::IS_PROTECTED) as $reflectionproperty) {
2127 if ($reflectionproperty->getName() === $property) {
2128 $isprotected = true;
2129 break;
2130 }
2131 }
2132 if (!$isprotected) {
2133 throw new coding_exception('Unable to set property - it does not exist or it is not protected');
2134 }
2135 $value = reset($arguments);
2136 $this->$property = $value;
2137 return;
2138 }
2139
2140 if (substr($name, 0, 4) === 'get_') {
2141 $property = substr($name, 4);
2142 if (empty($property)) {
2143 throw new coding_exception('Invalid property name (empty)');
2144 }
2145 if (!empty($arguments)) {
2146 throw new coding_exception('No parameter expected');
2147 }
2148 // Make sure it is a protected property.
2149 $isprotected = false;
2150 $reflection = new ReflectionObject($this);
2151 foreach ($reflection->getProperties(ReflectionProperty::IS_PROTECTED) as $reflectionproperty) {
2152 if ($reflectionproperty->getName() === $property) {
2153 $isprotected = true;
2154 break;
2155 }
2156 }
2157 if (!$isprotected) {
2158 throw new coding_exception('Unable to get property - it does not exist or it is not protected');
2159 }
2160 return $this->$property;
2161 }
2162 }
2163
3daedb5c
DM
2164 /**
2165 * Generates a random token and stores it in a file in moodledata directory.
2166 *
2167 * @return array of the (string)filename and (string)password in this order
2168 */
2169 public function prepare_authorization() {
2170 global $CFG;
2171
2172 make_upload_directory('mdeploy/auth/');
2173
2174 $attempts = 0;
2175 $success = false;
2176
2177 while (!$success and $attempts < 5) {
2178 $attempts++;
2179
2180 $passfile = $this->generate_passfile();
2181 $password = $this->generate_password();
2182 $now = time();
2183
2184 $filepath = $CFG->dataroot.'/mdeploy/auth/'.$passfile;
2185
2186 if (!file_exists($filepath)) {
2187 $success = file_put_contents($filepath, $password . PHP_EOL . $now . PHP_EOL, LOCK_EX);
2188 }
2189 }
2190
2191 if ($success) {
2192 return array($passfile, $password);
2193
2194 } else {
2195 throw new moodle_exception('unable_prepare_authorization', 'core_plugin');
2196 }
2197 }
2198
7683e550
DM
2199 // End of external API
2200
2201 /**
2202 * Prepares an array of HTTP parameters that can be passed to another page.
2203 *
2204 * @param array|object $data associative array or an object holding the data, data JSON-able
2205 * @return array suitable as a param for moodle_url
2206 */
2207 protected function data_to_params($data) {
2208
2209 // Append some our own data
2210 if (!empty($this->callerurl)) {
2211 $data['callerurl'] = $this->callerurl->out(false);
2212 }
7b1e0645 2213 if (!empty($this->returnurl)) {
7683e550
DM
2214 $data['returnurl'] = $this->returnurl->out(false);
2215 }
2216
2217 // Finally append the count of items in the package.
2218 $data[self::HTTP_PARAM_CHECKER] = count($data);
2219
2220 // Generate params
2221 $params = array();
2222 foreach ($data as $name => $value) {
2223 $transname = self::HTTP_PARAM_PREFIX.$name;
2224 $transvalue = json_encode($value);
2225 $params[$transname] = $transvalue;
2226 }
2227
2228 return $params;
2229 }
2230
2231 /**
2232 * Converts HTTP parameters passed to the script into native PHP data
2233 *
2234 * @param array $params such as $_REQUEST or $_POST
2235 * @return array data passed for this class
2236 */
2237 protected function params_to_data(array $params) {
2238
2239 if (empty($params)) {
2240 return array();
2241 }
2242
2243 $data = array();
2244 foreach ($params as $name => $value) {
2245 if (strpos($name, self::HTTP_PARAM_PREFIX) === 0) {
2246 $realname = substr($name, strlen(self::HTTP_PARAM_PREFIX));
2247 $realvalue = json_decode($value);
2248 $data[$realname] = $realvalue;
2249 }
2250 }
2251
2252 return $data;
2253 }
3daedb5c
DM
2254
2255 /**
2256 * Returns a random string to be used as a filename of the password storage.
2257 *
2258 * @return string
2259 */
2260 protected function generate_passfile() {
2261 return clean_param(uniqid('mdeploy_', true), PARAM_FILE);
2262 }
2263
2264 /**
2265 * Returns a random string to be used as the authorization token
2266 *
2267 * @return string
2268 */
2269 protected function generate_password() {
2270 return complex_random_string();
2271 }
0daa6428
DM
2272
2273 /**
2274 * Checks if the given component's directory is writable
2275 *
2276 * For the purpose of the deployment, the web server process has to have
2277 * write access to all files in the component's directory (recursively) and for the
2278 * directory itself.
2279 *
2280 * @see worker::move_directory_source_precheck()
2281 * @param string $component normalized component name
2282 * @return boolean
2283 */
2284 protected function component_writable($component) {
2285
2286 list($plugintype, $pluginname) = normalize_component($component);
2287
2288 $directory = get_plugin_directory($plugintype, $pluginname);
2289
2290 if (is_null($directory)) {
2291 throw new coding_exception('Unknown component location', $component);
2292 }
2293
2294 return $this->directory_writable($directory);
2295 }
2296
30e26827
DM
2297 /**
2298 * Checks if the mdeploy.php will be able to fetch the ZIP from the given URL
2299 *
2300 * This is mainly supposed to check if the transmission over HTTPS would
2301 * work. That is, if the CA certificates are present at the server.
2302 *
2303 * @param string $downloadurl the URL of the ZIP package to download
2304 * @return bool
2305 */
2306 protected function update_downloadable($downloadurl) {
2307 global $CFG;
2308
2309 $curloptions = array(
2310 'CURLOPT_SSL_VERIFYHOST' => 2, // this is the default in {@link curl} class but just in case
2311 'CURLOPT_SSL_VERIFYPEER' => true,
2312 );
2313
30e26827
DM
2314 $curl = new curl(array('proxy' => true));
2315 $result = $curl->head($downloadurl, $curloptions);
2316 $errno = $curl->get_errno();
2317 if (empty($errno)) {
2318 return true;
2319 } else {
2320 return false;
2321 }
2322 }
2323
0daa6428
DM
2324 /**
2325 * Checks if the directory and all its contents (recursively) is writable
2326 *
2327 * @param string $path full path to a directory
2328 * @return boolean
2329 */
2330 private function directory_writable($path) {
2331
2332 if (!is_writable($path)) {
2333 return false;
2334 }
2335
2336 if (is_dir($path)) {
2337 $handle = opendir($path);
2338 } else {
2339 return false;
2340 }
2341
2342 $result = true;
2343
2344 while ($filename = readdir($handle)) {
2345 $filepath = $path.'/'.$filename;
2346
2347 if ($filename === '.' or $filename === '..') {
2348 continue;
2349 }
2350
2351 if (is_dir($filepath)) {
2352 $result = $result && $this->directory_writable($filepath);
2353
2354 } else {
2355 $result = $result && is_writable($filepath);
2356 }
2357 }
2358
2359 closedir($handle);
2360
2361 return $result;
2362 }
7683e550
DM
2363}
2364
2365
00ef3c3e
DM
2366/**
2367 * Factory class producing required subclasses of {@link plugininfo_base}
2368 */
2369class plugininfo_default_factory {
b9934a17
DM
2370
2371 /**
00ef3c3e 2372 * Makes a new instance of the plugininfo class
b9934a17 2373 *
00ef3c3e
DM
2374 * @param string $type the plugin type, eg. 'mod'
2375 * @param string $typerootdir full path to the location of all the plugins of this type
2376 * @param string $name the plugin name, eg. 'workshop'
2377 * @param string $namerootdir full path to the location of the plugin
2378 * @param string $typeclass the name of class that holds the info about the plugin
2379 * @return plugininfo_base the instance of $typeclass
2380 */
2381 public static function make($type, $typerootdir, $name, $namerootdir, $typeclass) {
2382 $plugin = new $typeclass();
2383 $plugin->type = $type;
2384 $plugin->typerootdir = $typerootdir;
2385 $plugin->name = $name;
2386 $plugin->rootdir = $namerootdir;
2387
2388 $plugin->init_display_name();
2389 $plugin->load_disk_version();
2390 $plugin->load_db_version();
2391 $plugin->load_required_main_version();
2392 $plugin->init_is_standard();
473289a0 2393
00ef3c3e
DM
2394 return $plugin;
2395 }
b9934a17
DM
2396}
2397
00ef3c3e 2398
b9934a17 2399/**
b6ad8594 2400 * Base class providing access to the information about a plugin
828788f0
TH
2401 *
2402 * @property-read string component the component name, type_name
b9934a17 2403 */
b6ad8594 2404abstract class plugininfo_base {
b9934a17
DM
2405
2406 /** @var string the plugintype name, eg. mod, auth or workshopform */
2407 public $type;
2408 /** @var string full path to the location of all the plugins of this type */
2409 public $typerootdir;
2410 /** @var string the plugin name, eg. assignment, ldap */
2411 public $name;
2412 /** @var string the localized plugin name */
2413 public $displayname;
2414 /** @var string the plugin source, one of plugin_manager::PLUGIN_SOURCE_xxx constants */
2415 public $source;
2416 /** @var fullpath to the location of this plugin */
2417 public $rootdir;
2418 /** @var int|string the version of the plugin's source code */
2419 public $versiondisk;
2420 /** @var int|string the version of the installed plugin */
2421 public $versiondb;
2422 /** @var int|float|string required version of Moodle core */
2423 public $versionrequires;
b6ad8594
DM
2424 /** @var array other plugins that this one depends on, lazy-loaded by {@link get_other_required_plugins()} */
2425 public $dependencies;
b9934a17
DM
2426 /** @var int number of instances of the plugin - not supported yet */
2427 public $instances;
2428 /** @var int order of the plugin among other plugins of the same type - not supported yet */
2429 public $sortorder;
7d8de6d8
DM
2430 /** @var array|null array of {@link available_update_info} for this plugin */
2431 public $availableupdates;
b9934a17
DM
2432
2433 /**
b6ad8594
DM
2434 * Gathers and returns the information about all plugins of the given type
2435 *
b6ad8594
DM
2436 * @param string $type the name of the plugintype, eg. mod, auth or workshopform
2437 * @param string $typerootdir full path to the location of the plugin dir
2438 * @param string $typeclass the name of the actually called class
2439 * @return array of plugintype classes, indexed by the plugin name
b9934a17
DM
2440 */
2441 public static function get_plugins($type, $typerootdir, $typeclass) {
2442
2443 // get the information about plugins at the disk
2444 $plugins = get_plugin_list($type);
2445 $ondisk = array();
2446 foreach ($plugins as $pluginname => $pluginrootdir) {
00ef3c3e
DM
2447 $ondisk[$pluginname] = plugininfo_default_factory::make($type, $typerootdir,
2448 $pluginname, $pluginrootdir, $typeclass);
b9934a17
DM
2449 }
2450 return $ondisk;
2451 }
2452
2453 /**
b6ad8594 2454 * Sets {@link $displayname} property to a localized name of the plugin
b9934a17 2455 */
b8343e68 2456 public function init_display_name() {
828788f0
TH
2457 if (!get_string_manager()->string_exists('pluginname', $this->component)) {
2458 $this->displayname = '[pluginname,' . $this->component . ']';
b9934a17 2459 } else {
828788f0
TH
2460 $this->displayname = get_string('pluginname', $this->component);
2461 }
2462 }
2463
2464 /**
2465 * Magic method getter, redirects to read only values.
b6ad8594 2466 *
828788f0
TH
2467 * @param string $name
2468 * @return mixed
2469 */
2470 public function __get($name) {
2471 switch ($name) {
2472 case 'component': return $this->type . '_' . $this->name;
2473
2474 default:
2475 debugging('Invalid plugin property accessed! '.$name);
2476 return null;
b9934a17
DM
2477 }
2478 }
2479
2480 /**
b6ad8594
DM
2481 * Return the full path name of a file within the plugin.
2482 *
2483 * No check is made to see if the file exists.
2484 *
2485 * @param string $relativepath e.g. 'version.php'.
2486 * @return string e.g. $CFG->dirroot . '/mod/quiz/version.php'.
b9934a17 2487 */
473289a0 2488 public function full_path($relativepath) {
b9934a17 2489 if (empty($this->rootdir)) {
473289a0 2490 return '';
b9934a17 2491 }
473289a0
TH
2492 return $this->rootdir . '/' . $relativepath;
2493 }
b9934a17 2494
473289a0
TH
2495 /**
2496 * Load the data from version.php.
b6ad8594 2497 *
9d6eb027 2498 * @param bool $disablecache do not attempt to obtain data from the cache
b6ad8594 2499 * @return stdClass the object called $plugin defined in version.php
473289a0 2500 */
9d6eb027
DM
2501 protected function load_version_php($disablecache=false) {
2502
2503 $cache = cache::make('core', 'plugininfo_base');
2504
2505 $versionsphp = $cache->get('versions_php');
2506
2507 if (!$disablecache and $versionsphp !== false and isset($versionsphp[$this->component])) {
2508 return $versionsphp[$this->component];
2509 }
2510
473289a0 2511 $versionfile = $this->full_path('version.php');
b9934a17 2512
473289a0 2513 $plugin = new stdClass();
b9934a17
DM
2514 if (is_readable($versionfile)) {
2515 include($versionfile);
b9934a17 2516 }
9d6eb027
DM
2517 $versionsphp[$this->component] = $plugin;
2518 $cache->set('versions_php', $versionsphp);
2519
473289a0 2520 return $plugin;
b9934a17
DM
2521 }
2522
2523 /**
b6ad8594
DM
2524 * Sets {@link $versiondisk} property to a numerical value representing the
2525 * version of the plugin's source code.
2526 *
2527 * If the value is null after calling this method, either the plugin
2528 * does not use versioning (typically does not have any database
2529 * data) or is missing from disk.
b9934a17 2530 */
473289a0
TH
2531 public function load_disk_version() {
2532 $plugin = $this->load_version_php();
2533 if (isset($plugin->version)) {
2534 $this->versiondisk = $plugin->version;
b9934a17
DM
2535 }
2536 }
2537
2538 /**
b6ad8594
DM
2539 * Sets {@link $versionrequires} property to a numerical value representing
2540 * the version of Moodle core that this plugin requires.
b9934a17 2541 */
b8343e68 2542 public function load_required_main_version() {
473289a0
TH
2543 $plugin = $this->load_version_php();
2544 if (isset($plugin->requires)) {
2545 $this->versionrequires = $plugin->requires;
b9934a17 2546 }
473289a0 2547 }
b9934a17 2548
0242bdc7 2549 /**
777781d1 2550 * Initialise {@link $dependencies} to the list of other plugins (in any)
0242bdc7
TH
2551 * that this one requires to be installed.
2552 */
2553 protected function load_other_required_plugins() {
2554 $plugin = $this->load_version_php();
777781d1
TH
2555 if (!empty($plugin->dependencies)) {
2556 $this->dependencies = $plugin->dependencies;
0242bdc7 2557 } else {
777781d1 2558 $this->dependencies = array(); // By default, no dependencies.
0242bdc7
TH
2559 }
2560 }
2561
2562 /**
b6ad8594
DM
2563 * Get the list of other plugins that this plugin requires to be installed.
2564 *
2565 * @return array with keys the frankenstyle plugin name, and values either
2566 * a version string (like '2011101700') or the constant ANY_VERSION.
0242bdc7
TH
2567 */
2568 public function get_other_required_plugins() {
777781d1 2569 if (is_null($this->dependencies)) {
0242bdc7
TH
2570 $this->load_other_required_plugins();
2571 }
777781d1 2572 return $this->dependencies;
0242bdc7
TH
2573 }
2574
73658371
DM
2575 /**
2576 * Is this is a subplugin?
2577 *
2578 * @return boolean
2579 */
2580 public function is_subplugin() {
2581 return ($this->get_parent_plugin() !== false);
2582 }
2583
2584 /**
2585 * If I am a subplugin, return the name of my parent plugin.
2586 *
2587 * @return string|bool false if not a subplugin, name of the parent otherwise
2588 */
2589 public function get_parent_plugin() {
2590 return $this->get_plugin_manager()->get_parent_of_subplugin($this->type);
2591 }
2592
473289a0 2593 /**
b6ad8594
DM
2594 * Sets {@link $versiondb} property to a numerical value representing the
2595 * currently installed version of the plugin.
2596 *
2597 * If the value is null after calling this method, either the plugin
2598 * does not use versioning (typically does not have any database
2599 * data) or has not been installed yet.
473289a0
TH
2600 */
2601 public function load_db_version() {
828788f0 2602 if ($ver = self::get_version_from_config_plugins($this->component)) {
473289a0 2603 $this->versiondb = $ver;
b9934a17
DM
2604 }
2605 }
2606
2607 /**
b6ad8594
DM
2608 * Sets {@link $source} property to one of plugin_manager::PLUGIN_SOURCE_xxx
2609 * constants.
2610 *
2611 * If the property's value is null after calling this method, then
2612 * the type of the plugin has not been recognized and you should throw
2613 * an exception.
b9934a17 2614 */
b8343e68 2615 public function init_is_standard() {
b9934a17
DM
2616
2617 $standard = plugin_manager::standard_plugins_list($this->type);
2618
2619 if ($standard !== false) {
2620 $standard = array_flip($standard);
2621 if (isset($standard[$this->name])) {
2622 $this->source = plugin_manager::PLUGIN_SOURCE_STANDARD;
ec8935f5
PS
2623 } else if (!is_null($this->versiondb) and is_null($this->versiondisk)
2624 and plugin_manager::is_deleted_standard_plugin($this->type, $this->name)) {
2625 $this->source = plugin_manager::PLUGIN_SOURCE_STANDARD; // to be deleted
b9934a17
DM
2626 } else {
2627 $this->source = plugin_manager::PLUGIN_SOURCE_EXTENSION;
2628 }
2629 }
2630 }
2631
2632 /**
b6ad8594
DM
2633 * Returns true if the plugin is shipped with the official distribution
2634 * of the current Moodle version, false otherwise.
2635 *
2636 * @return bool
b9934a17
DM
2637 */
2638 public function is_standard() {
2639 return $this->source === plugin_manager::PLUGIN_SOURCE_STANDARD;
2640 }
2641
3a2300f5
DM
2642 /**
2643 * Returns true if the the given Moodle version is enough to run this plugin
2644 *
2645 * @param string|int|double $moodleversion
2646 * @return bool
2647 */
2648 public function is_core_dependency_satisfied($moodleversion) {
2649
2650 if (empty($this->versionrequires)) {
2651 return true;
2652
2653 } else {
2654 return (double)$this->versionrequires <= (double)$moodleversion;
2655 }
2656 }
2657
b9934a17 2658 /**
b6ad8594
DM
2659 * Returns the status of the plugin
2660 *
2661 * @return string one of plugin_manager::PLUGIN_STATUS_xxx constants
b9934a17
DM
2662 */
2663 public function get_status() {
2664
2665 if (is_null($this->versiondb) and is_null($this->versiondisk)) {
2666 return plugin_manager::PLUGIN_STATUS_NODB;
2667
2668 } else if (is_null($this->versiondb) and !is_null($this->versiondisk)) {
2669 return plugin_manager::PLUGIN_STATUS_NEW;
2670
2671 } else if (!is_null($this->versiondb) and is_null($this->versiondisk)) {
ec8935f5
PS
2672 if (plugin_manager::is_deleted_standard_plugin($this->type, $this->name)) {
2673 return plugin_manager::PLUGIN_STATUS_DELETE;
2674 } else {
2675 return plugin_manager::PLUGIN_STATUS_MISSING;
2676 }
b9934a17
DM
2677
2678 } else if ((string)$this->versiondb === (string)$this->versiondisk) {
2679 return plugin_manager::PLUGIN_STATUS_UPTODATE;
2680
2681 } else if ($this->versiondb < $this->versiondisk) {
2682 return plugin_manager::PLUGIN_STATUS_UPGRADE;
2683
2684 } else if ($this->versiondb > $this->versiondisk) {
2685 return plugin_manager::PLUGIN_STATUS_DOWNGRADE;
2686
2687 } else {
2688 // $version = pi(); and similar funny jokes - hopefully Donald E. Knuth will never contribute to Moodle ;-)
2689 throw new coding_exception('Unable to determine plugin state, check the plugin versions');
2690 }
2691 }
2692
2693 /**
b6ad8594
DM
2694 * Returns the information about plugin availability
2695 *
2696 * True means that the plugin is enabled. False means that the plugin is
2697 * disabled. Null means that the information is not available, or the
2698 * plugin does not support configurable availability or the availability
2699 * can not be changed.
2700 *
2701 * @return null|bool
b9934a17
DM
2702 */
2703 public function is_enabled() {
2704 return null;
2705 }
2706
2707 /**
7d8de6d8 2708 * Populates the property {@link $availableupdates} with the information provided by
dd119e21
DM
2709 * available update checker
2710 *
2711 * @param available_update_checker $provider the class providing the available update info
2712 */
7d8de6d8 2713 public function check_available_updates(available_update_checker $provider) {
c6f008e7
DM
2714 global $CFG;
2715
2716 if (isset($CFG->updateminmaturity)) {
2717 $minmaturity = $CFG->updateminmaturity;
2718 } else {
2719 // this can happen during the very first upgrade to 2.3
2720 $minmaturity = MATURITY_STABLE;
2721 }
2722
2723 $this->availableupdates = $provider->get_update_info($this->component,
2724 array('minmaturity' => $minmaturity));
dd119e21
DM
2725 }
2726
d26f3ddd 2727 /**
7d8de6d8 2728 * If there are updates for this plugin available, returns them.
d26f3ddd 2729 *
7d8de6d8
DM
2730 * Returns array of {@link available_update_info} objects, if some update
2731 * is available. Returns null if there is no update available or if the update
2732 * availability is unknown.
d26f3ddd 2733 *
7d8de6d8 2734 * @return array|null
d26f3ddd 2735 */
7d8de6d8 2736 public function available_updates() {
dd119e21 2737
7d8de6d8 2738 if (empty($this->availableupdates) or !is_array($this->availableupdates)) {
dd119e21
DM
2739 return null;
2740 }
2741
7d8de6d8
DM
2742 $updates = array();
2743
2744 foreach ($this->availableupdates as $availableupdate) {
2745 if ($availableupdate->version > $this->versiondisk) {
2746 $updates[] = $availableupdate;
2747 }
2748 }
2749
2750 if (empty($updates)) {
2751 return null;
dd119e21
DM
2752 }
2753
7d8de6d8 2754 return $updates;
d26f3ddd
DM
2755 }
2756
5cdb1893
MG
2757 /**
2758 * Returns the node name used in admin settings menu for this plugin settings (if applicable)
2759 *
2760 * @return null|string node name or null if plugin does not create settings node (default)
2761 */
2762 public function get_settings_section_name() {
2763 return null;
2764 }
2765
b9934a17 2766 /**
b6ad8594
DM
2767 * Returns the URL of the plugin settings screen
2768 *
2769 * Null value means that the plugin either does not have the settings screen
2770 * or its location is not available via this library.
2771 *
2772 * @return null|moodle_url
b9934a17
DM
2773 */
2774 public function get_settings_url() {
5cdb1893
MG
2775 $section = $this->get_settings_section_name();
2776 if ($section === null) {
2777 return null;
2778 }
2779 $settings = admin_get_root()->locate($section);
2780 if ($settings && $settings instanceof admin_settingpage) {
2781 return new moodle_url('/admin/settings.php', array('section' => $section));
2782 } else if ($settings && $settings instanceof admin_externalpage) {
2783 return new moodle_url($settings->url);
2784 } else {
2785 return null;
2786 }
2787 }
2788
2789 /**
2790 * Loads plugin settings to the settings tree
2791 *
2792 * This function usually includes settings.php file in plugins folder.
2793 * Alternatively it can create a link to some settings page (instance of admin_externalpage)
2794 *
2795 * @param part_of_admin_tree $adminroot
2796 * @param string $parentnodename
2797 * @param bool $hassiteconfig whether the current user has moodle/site:config capability
2798 */
2799 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
b9934a17
DM
2800 }
2801
2802 /**
73658371 2803 * Should there be a way to uninstall the plugin via the administration UI
b6ad8594 2804 *
73658371
DM
2805 * By default, uninstallation is allowed for all non-standard add-ons. Subclasses
2806 * may want to override this to allow uninstallation of all plugins (simply by
2807 * returning true unconditionally). Subplugins follow their parent plugin's
2808 * decision by default.
0b733dd9 2809 *
73658371
DM
2810 * Note that even if true is returned, the core may still prohibit the uninstallation,
2811 * e.g. in case there are other plugins that depend on this one.
b6ad8594 2812 *
73658371 2813 * @return boolean
b9934a17 2814 */
73658371 2815 public function is_uninstall_allowed() {
0b733dd9 2816
73658371
DM
2817 if ($this->is_subplugin()) {
2818 return $this->get_plugin_manager()->get_plugin_info($this->get_parent_plugin())->is_uninstall_allowed();
0b733dd9
DM
2819 }
2820
73658371
DM
2821 if ($this->is_standard()) {
2822 return false;
0b733dd9
DM
2823 }
2824
73658371
DM
2825 return true;
2826 }
2827
b9934a17 2828 /**
b6ad8594
DM
2829 * Returns the URL of the screen where this plugin can be uninstalled
2830 *
2831 * Visiting that URL must be safe, that is a manual confirmation is needed
73658371
DM
2832 * for actual uninstallation of the plugin. By default, URL to a common
2833 * uninstalling tool is returned.
b6ad8594 2834 *
73658371 2835 * @return moodle_url
b9934a17
DM
2836 */
2837 public function get_uninstall_url() {
0b733dd9 2838 return $this->get_default_uninstall_url();
b9934a17
DM
2839 }
2840
2841 /**
b6ad8594
DM
2842 * Returns relative directory of the plugin with heading '/'
2843 *
2844 * @return string
b9934a17
DM
2845 */
2846 public function get_dir() {
2847 global $CFG;
2848
2849 return substr($this->rootdir, strlen($CFG->dirroot));
2850 }
2851
436d9447
DM
2852 /**
2853 * Hook method to implement certain steps when uninstalling the plugin.
2854 *
2855 * This hook is called by {@link plugin_manager::uninstall_plugin()} so
2856 * it is basically usable only for those plugin types that use the default
2857 * uninstall tool provided by {@link self::get_default_uninstall_url()}.
2858 *
3ca1b546 2859 * @param progress_trace $progress traces the process
436d9447
DM
2860 * @return bool true on success, false on failure
2861 */
3ca1b546 2862 public function uninstall(progress_trace $progress) {
436d9447
DM
2863 return true;
2864 }
2865
0b733dd9
DM
2866 /**
2867 * Returns URL to a script that handles common plugin uninstall procedure.
2868 *
2869 * This URL is suitable for plugins that do not have their own UI
2870 * for uninstalling.
2871 *
2872 * @return moodle_url
2873 */
73658371 2874 protected final function get_default_uninstall_url() {
0b733dd9
DM
2875 return new moodle_url('/admin/plugins.php', array(
2876 'sesskey' => sesskey(),
2877 'uninstall' => $this->component,
2878 'confirm' => 0,
2879 ));
2880 }
2881
b9934a17 2882 /**
b8a6f26e 2883 * Provides access to plugin versions from the {config_plugins} table
b9934a17
DM
2884 *
2885 * @param string $plugin plugin name
b8a6f26e
DM
2886 * @param bool $disablecache do not attempt to obtain data from the cache
2887 * @return int|bool the stored value or false if not found
b9934a17
DM
2888 */
2889 protected function get_version_from_config_plugins($plugin, $disablecache=false) {
2890 global $DB;
b9934a17 2891
ad3ed98b 2892 $cache = cache::make('core', 'plugininfo_base');
b8a6f26e
DM
2893
2894 $pluginversions = $cache->get('versions_db');
2895
2896 if ($pluginversions === false or $disablecache) {
f433088d
PS
2897 try {
2898 $pluginversions = $DB->get_records_menu('config_plugins', array('name' => 'version'), 'plugin', 'plugin,value');
2899 } catch (dml_exception $e) {
2900 // before install
2901 $pluginversions = array();
2902 }
b8a6f26e 2903 $cache->set('versions_db', $pluginversions);
b9934a17
DM
2904 }
2905
b8a6f26e
DM
2906 if (isset($pluginversions[$plugin])) {
2907 return $pluginversions[$plugin];
2908 } else {
b9934a17
DM
2909 return false;
2910 }
b9934a17 2911 }
73658371
DM
2912
2913 /**
2914 * Provides access to the plugin_manager singleton.
2915 *
2916 * @return plugin_manmager
2917 */
2918 protected function get_plugin_manager() {
2919 return plugin_manager::instance();
2920 }
b9934a17
DM
2921}
2922
b6ad8594 2923
b9934a17
DM
2924/**
2925 * General class for all plugin types that do not have their own class
2926 */
b6ad8594 2927class plugininfo_general extends plugininfo_base {
b9934a17
DM
2928}
2929
b6ad8594 2930
b9934a17
DM
2931/**
2932 * Class for page side blocks
2933 */
b6ad8594 2934class plugininfo_block extends plugininfo_base {
b9934a17 2935
b9934a17
DM
2936 public static function get_plugins($type, $typerootdir, $typeclass) {
2937
2938 // get the information about blocks at the disk
2939 $blocks = parent::get_plugins($type, $typerootdir, $typeclass);
2940
2941 // add blocks missing from disk
2942 $blocksinfo = self::get_blocks_info();
2943 foreach ($blocksinfo as $blockname => $blockinfo) {
2944 if (isset($blocks[$blockname])) {
2945 continue;
2946 }
2947 $plugin = new $typeclass();
2948 $plugin->type = $type;
2949 $plugin->typerootdir = $typerootdir;
2950 $plugin->name = $blockname;
2951 $plugin->rootdir = null;
2952 $plugin->displayname = $blockname;
2953 $plugin->versiondb = $blockinfo->version;
b8343e68 2954 $plugin->init_is_standard();
b9934a17
DM
2955
2956 $blocks[$blockname] = $plugin;
2957 }
2958
2959 return $blocks;
2960 }
2961
870d4280
MG
2962 /**
2963 * Magic method getter, redirects to read only values.
2964 *
2965 * For block plugins pretends the object has 'visible' property for compatibility
2966 * with plugins developed for Moodle version below 2.4
2967 *
2968 * @param string $name
2969 * @return mixed
2970 */
2971 public function __get($name) {
2972 if ($name === 'visible') {
2973 debugging('This is now an instance of plugininfo_block, please use $block->is_enabled() instead of $block->visible', DEBUG_DEVELOPER);
2974 return ($this->is_enabled() !== false);
2975 }
2976 return parent::__get($name);
2977 }
2978
b8343e68 2979 public function init_display_name() {
b9934a17
DM
2980
2981 if (get_string_manager()->string_exists('pluginname', 'block_' . $this->name)) {
2982 $this->displayname = get_string('pluginname', 'block_' . $this->name);
2983
2984 } else if (($block = block_instance($this->name)) !== false) {
2985 $this->displayname = $block->get_title();
2986
2987 } else {
b8343e68 2988 parent::init_display_name();
b9934a17
DM
2989 }
2990 }
2991
b8343e68 2992 public function load_db_version() {
b9934a17
DM
2993 global $DB;
2994
2995 $blocksinfo = self::get_blocks_info();
2996 if (isset($blocksinfo[$this->name]->version)) {
2997 $this->versiondb = $blocksinfo[$this->name]->version;
2998 }
2999 }
3000
b9934a17
DM
3001 public function is_enabled() {
3002
3003 $blocksinfo = self::get_blocks_info();
3004 if (isset($blocksinfo[$this->name]->visible)) {
3005 if ($blocksinfo[$this->name]->visible) {
3006 return true;
3007 } else {
3008 return false;
3009 }
3010 } else {
3011 return parent::is_enabled();
3012 }
3013 }
3014
870d4280
MG
3015 public function get_settings_section_name() {
3016 return 'blocksetting' . $this->name;
3017 }
b9934a17 3018
870d4280
MG
3019 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3020 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3021 $ADMIN = $adminroot; // may be used in settings.php
3022 $block = $this; // also can be used inside settings.php
3023 $section = $this->get_settings_section_name();
b9934a17 3024
870d4280
MG
3025 if (!$hassiteconfig || (($blockinstance = block_instance($this->name)) === false)) {
3026 return;
3027 }
b9934a17 3028
870d4280
MG
3029 $settings = null;
3030 if ($blockinstance->has_config()) {
6740c605 3031 if (file_exists($this->full_path('settings.php'))) {
870d4280
MG
3032 $settings = new admin_settingpage($section, $this->displayname,
3033 'moodle/site:config', $this->is_enabled() === false);
3034 include($this->full_path('settings.php')); // this may also set $settings to null
b9934a17
DM
3035 } else {
3036 $blocksinfo = self::get_blocks_info();
870d4280
MG
3037 $settingsurl = new moodle_url('/admin/block.php', array('block' => $blocksinfo[$this->name]->id));
3038 $settings = new admin_externalpage($section, $this->displayname,
3039 $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
b9934a17 3040 }
870d4280
MG
3041 }
3042 if ($settings) {
3043 $ADMIN->add($parentnodename, $settings);
b9934a17
DM
3044 }
3045 }
3046
73658371
DM
3047 public function is_uninstall_allowed() {
3048 return true;
3049 }
b9934a17 3050
73658371 3051 public function get_uninstall_url() {
b9934a17
DM
3052 $blocksinfo = self::get_blocks_info();
3053 return new moodle_url('/admin/blocks.php', array('delete' => $blocksinfo[$this->name]->id, 'sesskey' => sesskey()));
3054 }
3055
3056 /**
3057 * Provides access to the records in {block} table
3058 *
b8a6f26e 3059 * @param bool $disablecache do not attempt to obtain data from the cache
b9934a17
DM
3060 * @return array array of stdClasses
3061 */
3062 protected static function get_blocks_info($disablecache=false) {
3063 global $DB;
b9934a17 3064
ad3ed98b 3065 $cache = cache::make('core', 'plugininfo_block');
b8a6f26e
DM
3066
3067 $blocktypes = $cache->get('blocktypes');
3068
3069 if ($blocktypes === false or $disablecache) {
f433088d 3070 try {
b8a6f26e 3071 $blocktypes = $DB->get_records('block', null, 'name', 'name,id,version,visible');
f433088d
PS
3072 } catch (dml_exception $e) {
3073 // before install
b8a6f26e 3074 $blocktypes = array();
f433088d 3075 }
b8a6f26e 3076 $cache->set('blocktypes', $blocktypes);
b9934a17
DM
3077 }
3078
b8a6f26e 3079 return $blocktypes;
b9934a17
DM
3080 }
3081}
3082
b6ad8594 3083
b9934a17
DM
3084/**
3085 * Class for text filters
3086 */
b6ad8594 3087class plugininfo_filter extends plugininfo_base {
b9934a17 3088
b9934a17 3089 public static function get_plugins($type, $typerootdir, $typeclass) {
7c9b837e 3090 global $CFG, $DB;
b9934a17
DM
3091
3092 $filters = array();
3093
8d211302 3094 // get the list of filters in /filter location
b9934a17
DM
3095 $installed = filter_get_all_installed();
3096
0662bd67 3097 foreach ($installed as $name => $displayname) {
b9934a17
DM
3098 $plugin = new $typeclass();
3099 $plugin->type = $type;
3100 $plugin->typerootdir = $typerootdir;
0662bd67
PS
3101 $plugin->name = $name;
3102 $plugin->rootdir = "$CFG->dirroot/filter/$name";
b9934a17
DM
3103 $plugin->displayname = $displayname;
3104
b8343e68
TH
3105 $plugin->load_disk_version();
3106 $plugin->load_db_version();
3107 $plugin->load_required_main_version();
3108 $plugin->init_is_standard();
b9934a17
DM
3109
3110 $filters[$plugin->name] = $plugin;
3111 }
3112
8d211302 3113 // Do not mess with filter registration here!
7c9b837e 3114
8d211302 3115 $globalstates = self::get_global_states();
b9934a17
DM
3116
3117 // make sure that all registered filters are installed, just in case
3118 foreach ($globalstates as $name => $info) {
3119 if (!isset($filters[$name])) {
3120 // oops, there is a record in filter_active but the filter is not installed
3121 $plugin = new $typeclass();
3122 $plugin->type = $type;
3123 $plugin->typerootdir = $typerootdir;
3124 $plugin->name = $name;
0662bd67
PS
3125 $plugin->rootdir = "$CFG->dirroot/filter/$name";
3126 $plugin->displayname = $name;
b9934a17 3127
b8343e68 3128 $plugin->load_db_version();
b9934a17
DM
3129
3130 if (is_null($plugin->versiondb)) {
3131 // this is a hack to stimulate 'Missing from disk' error
3132 // because $plugin->versiondisk will be null !== false
3133 $plugin->versiondb = false;
3134 }
3135
3136 $filters[$plugin->name] = $plugin;
3137 }
3138 }
3139
3140 return $filters;
3141 }
3142
b8343e68 3143 public function init_display_name() {
b9934a17
DM
3144 // do nothing, the name is set in self::get_plugins()
3145 }
3146
b9934a17
DM
3147 public function is_enabled() {
3148
3149 $globalstates = self::get_global_states();
3150
0662bd67 3151 foreach ($globalstates as $name => $info) {
b9934a17
DM
3152 if ($name === $this->name) {
3153 if ($info->active == TEXTFILTER_DISABLED) {
3154 return false;
3155 } else {
3156 // it may be 'On' or 'Off, but available'
3157 return null;
3158 }
3159 }
3160 }
3161
3162 return null;
3163 }
3164
1de1a666 3165 public function get_settings_section_name() {
0662bd67 3166 return 'filtersetting' . $this->name;
1de1a666
MG
3167 }
3168
3169 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3170 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3171 $ADMIN = $adminroot; // may be used in settings.php
3172 $filter = $this; // also can be used inside settings.php
3173
3174 $settings = null;
8d211302 3175 if ($hassiteconfig && file_exists($this->full_path('filtersettings.php'))) {
1de1a666
MG
3176 $section = $this->get_settings_section_name();
3177 $settings = new admin_settingpage($section, $this->displayname,
3178 'moodle/site:config', $this->is_enabled() === false);
3179 include($this->full_path('filtersettings.php')); // this may also set $settings to null
3180 }
3181 if ($settings) {
3182 $ADMIN->add($parentnodename, $settings);
b9934a17
DM
3183 }
3184 }
3185
73658371
DM
3186 public function is_uninstall_allowed() {
3187 return true;
3188 }
3189
b9934a17 3190 public function get_uninstall_url() {
0662bd67 3191 return new moodle_url('/admin/filters.php', array('sesskey' => sesskey(), 'filterpath' => $this->name, 'action' => 'delete'));
b9934a17
DM
3192 }
3193
3194 /**
3195 * Provides access to the results of {@link filter_get_global_states()}
3196 * but indexed by the normalized filter name
3197 *
3198 * The legacy filter name is available as ->legacyname property.
3199 *
b8a6f26e 3200 * @param bool $disablecache do not attempt to obtain data from the cache
b9934a17
DM
3201 * @return array
3202 */
3203 protected static function get_global_states($disablecache=false) {
3204 global $DB;
b9934a17 3205
ad3ed98b 3206 $cache = cache::make('core', 'plugininfo_filter');
b8a6f26e
DM
3207
3208 $globalstates = $cache->get('globalstates');
3209
3210 if ($globalstates === false or $disablecache) {
b9934a17
DM
3211
3212 if (!$DB->get_manager()->table_exists('filter_active')) {
8d211302 3213 // Not installed yet.
b8a6f26e
DM
3214 $cache->set('globalstates', array());
3215 return array();
8d211302 3216 }
b9934a17 3217
b8a6f26e
DM
3218 $globalstates = array();
3219
8d211302
PS
3220 foreach (filter_get_global_states() as $name => $info) {
3221 if (strpos($name, '/') !== false) {
3222 // Skip existing before upgrade to new names.
3223 continue;
b9934a17 3224 }
8d211302 3225
b8a6f26e
DM
3226 $filterinfo = new stdClass();
3227 $filterinfo->active = $info->active;
3228 $filterinfo->sortorder = $info->sortorder;
3229 $globalstates[$name] = $filterinfo;
b9934a17 3230 }
b8a6f26e
DM
3231
3232 $cache->set('globalstates', $globalstates);
b9934a17
DM
3233 }
3234
b8a6f26e 3235 return $globalstates;
b9934a17
DM
3236 }
3237}
3238
b6ad8594 3239
b9934a17
DM
3240/**
3241 * Class for activity modules
3242 */
b6ad8594 3243class plugininfo_mod extends plugininfo_base {
b9934a17 3244
b9934a17
DM
3245 public static function get_plugins($type, $typerootdir, $typeclass) {
3246
3247 // get the information about plugins at the disk
3248 $modules = parent::get_plugins($type, $typerootdir, $typeclass);
3249
3250 // add modules missing from disk
3251 $modulesinfo = self::get_modules_info();
3252 foreach ($modulesinfo as $modulename => $moduleinfo) {
3253 if (isset($modules[$modulename])) {
3254 continue;
3255 }
3256 $plugin = new $typeclass();
3257 $plugin->type = $type;
3258 $plugin->typerootdir = $typerootdir;
3259 $plugin->name = $modulename;
3260 $plugin->rootdir = null;
3261 $plugin->displayname = $modulename;
3262 $plugin->versiondb = $moduleinfo->version;
b8343e68 3263 $plugin->init_is_standard();
b9934a17
DM
3264
3265 $modules[$modulename] = $plugin;
3266 }
3267
3268 return $modules;
3269 }
3270
fde6f79f
MG
3271 /**
3272 * Magic method getter, redirects to read only values.
3273 *
3274 * For module plugins we pretend the object has 'visible' property for compatibility
3275 * with plugins developed for Moodle version below 2.4
3276 *
3277 * @param string $name
3278 * @return mixed
3279 */
3280 public function __get($name) {
3281 if ($name === 'visible') {
3282 debugging('This is now an instance of plugininfo_mod, please use $module->is_enabled() instead of $module->visible', DEBUG_DEVELOPER);
3283 return ($this->is_enabled() !== false);
3284 }
3285 return parent::__get($name);
3286 }
3287
b8343e68 3288 public function init_display_name() {
828788f0
TH
3289 if (get_string_manager()->string_exists('pluginname', $this->component)) {
3290 $this->displayname = get_string('pluginname', $this->component);
b9934a17 3291 } else {
828788f0 3292 $this->displayname = get_string('modulename', $this->component);
b9934a17
DM
3293 }
3294 }
3295
3296 /**
473289a0 3297 * Load the data from version.php.
9d6eb027
DM
3298 *
3299 * @param bool $disablecache do not attempt to obtain data from the cache
473289a0 3300 * @return object the data object defined in version.php.
b9934a17 3301 */
9d6eb027
DM
3302 protected function load_version_php($disablecache=false) {
3303
3304 $cache = cache::make('core', 'plugininfo_base');
3305
3306 $versionsphp = $cache->get('versions_php');
3307
3308 if (!$disablecache and $versionsphp !== false and isset($versionsphp[$this->component])) {
3309 return $versionsphp[$this->component];
3310 }
3311
473289a0 3312 $versionfile = $this->full_path('version.php');
b9934a17 3313
473289a0 3314 $module = new stdClass();
bdbcb6d7 3315 $plugin = new stdClass();
b9934a17
DM
3316 if (is_readable($versionfile)) {
3317 include($versionfile);
b9934a17 3318 }
bdbcb6d7
PS
3319 if (!isset($module->version) and isset($plugin->version)) {
3320 $module = $plugin;
3321 }
9d6eb027
DM
3322 $versionsphp[$this->component] = $module;
3323 $cache->set('versions_php', $versionsphp);
3324
473289a0 3325 return $module;
b9934a17
DM
3326 }
3327
b8343e68 3328 public function load_db_version() {
b9934a17
DM
3329 global $DB;
3330
3331 $modulesinfo = self::get_modules_info();
3332 if (isset($modulesinfo[$this->name]->version)) {
3333 $this->versiondb = $modulesinfo[$this->name]->version;
3334 }
3335 }
3336
b9934a17
DM
3337 public function is_enabled() {
3338
3339 $modulesinfo = self::get_modules_info();
3340 if (isset($modulesinfo[$this->name]->visible)) {
3341 if ($modulesinfo[$this->name]->visible) {
3342 return true;
3343 } else {
3344 return false;
3345 }
3346 } else {
3347 return parent::is_enabled();
3348 }
3349 }
3350
fde6f79f
MG
3351 public function get_settings_section_name() {
3352 return 'modsetting' . $this->name;
3353 }
b9934a17 3354
fde6f79f
MG
3355 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3356 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3357 $ADMIN = $adminroot; // may be used in settings.php
3358 $module = $this; // also can be used inside settings.php
3359 $section = $this->get_settings_section_name();
3360
dddbbac3 3361 $modulesinfo = self::get_modules_info();
fde6f79f 3362 $settings = null;
dddbbac3 3363 if ($hassiteconfig && isset($modulesinfo[$this->name]) && file_exists($this->full_path('settings.php'))) {
fde6f79f
MG
3364 $settings = new admin_settingpage($section, $this->displayname,
3365 'moodle/site:config', $this->is_enabled() === false);
3366 include($this->full_path('settings.php')); // this may also set $settings to null
3367 }
3368 if ($settings) {
3369 $ADMIN->add($parentnodename, $settings);
b9934a17
DM
3370 }
3371 }
3372
73658371
DM
3373 /**
3374 * Allow all activity modules but Forum to be uninstalled.
b9934a17 3375
73658371
DM
3376 * This exception for the Forum has been hard-coded in Moodle since ages,
3377 * we may want to re-think it one day.
3378 */
3379 public function is_uninstall_allowed() {
3380 if ($this->name === 'forum') {
3381 return false;
b9934a17 3382 } else {
73658371 3383 return true;
b9934a17
DM
3384 }
3385 }
3386
73658371
DM
3387 public function get_uninstall_url() {
3388 return new moodle_url('/admin/modules.php', array('delete' => $this->name, 'sesskey' => sesskey()));
3389 }
3390
b9934a17
DM
3391 /**
3392 * Provides access to the records in {modules} table
3393 *
b8a6f26e 3394 * @param bool $disablecache do not attempt to obtain data from the cache
b9934a17
DM
3395 * @return array array of stdClasses
3396 */
3397 protected static function get_modules_info($disablecache=false) {
3398 global $DB;
b9934a17 3399
ad3ed98b 3400 $cache = cache::make('core', 'plugininfo_mod');
b8a6f26e
DM
3401
3402 $modulesinfo = $cache->get('modulesinfo');
3403
3404 if ($modulesinfo === false or $disablecache) {
f433088d 3405 try {
b8a6f26e 3406 $modulesinfo = $DB->get_records('modules', null, 'name', 'name,id,version,visible');
f433088d
PS
3407 } catch (dml_exception $e) {
3408 // before install
b8a6f26e 3409 $modulesinfo = array();
f433088d 3410 }
b8a6f26e 3411 $cache->set('modulesinfo', $modulesinfo);
b9934a17
DM
3412 }
3413
b8a6f26e 3414 return $modulesinfo;
b9934a17
DM
3415 }
3416}
3417
0242bdc7
TH
3418
3419/**
3420 * Class for question behaviours.
3421 */
b6ad8594
DM
3422class plugininfo_qbehaviour extends plugininfo_base {
3423
73658371
DM
3424 public function is_uninstall_allowed() {
3425 return true;
3426 }
3427
828788f0
TH
3428 public function get_uninstall_url() {
3429 return new moodle_url('/admin/qbehaviours.php',
3430 array('delete' => $this->name, 'sesskey' => sesskey()));
3431 }
0242bdc7
TH
3432}
3433
3434
b9934a17
DM
3435/**
3436 * Class for question types
3437 */
b6ad8594
DM
3438class plugininfo_qtype extends plugininfo_base {
3439
73658371
DM
3440 public function is_uninstall_allowed() {
3441 return true;
3442 }
3443
828788f0
TH
3444 public function get_uninstall_url() {
3445 return new moodle_url('/admin/qtypes.php',
3446 array('delete' => $this->name, 'sesskey' => sesskey()));
3447 }
66f3684a
MG
3448
3449 public function get_settings_section_name() {
3450 return 'qtypesetting' . $this->name;
3451 }
3452
3453 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3454 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3455 $ADMIN = $adminroot; // may be used in settings.php
3456 $qtype = $this; // also can be used inside settings.php
3457 $section = $this->get_settings_section_name();
3458
3459 $settings = null;
837e1812
TH
3460 $systemcontext = context_system::instance();
3461 if (($hassiteconfig || has_capability('moodle/question:config', $systemcontext)) &&
3462 file_exists($this->full_path('settings.php'))) {
66f3684a 3463 $settings = new admin_settingpage($section, $this->displayname,
837e1812 3464 'moodle/question:config', $this->is_enabled() === false);
66f3684a
MG
3465 include($this->full_path('settings.php')); // this may also set $settings to null
3466 }
3467 if ($settings) {
3468 $ADMIN->add($parentnodename, $settings);
3469 }
3470 }
b9934a17
DM
3471}
3472
b9934a17
DM
3473
3474/**
3475 * Class for authentication plugins
3476 */
b6ad8594 3477class plugininfo_auth extends plugininfo_base {
b9934a17 3478
b9934a17
DM
3479 public function is_enabled() {
3480 global $CFG;
b9934a17
DM
3481
3482 if (in_array($this->name, array('nologin', 'manual'))) {
3483 // these two are always enabled and can't be disabled
3484 return null;
3485 }
3486
b8a6f26e 3487 $enabled = array_flip(explode(',', $CFG->auth));
b9934a17
DM
3488
3489 return isset($enabled[$this->name]);
3490 }
3491
cbe9f609
MG
3492 public function get_settings_section_name() {
3493 return 'authsetting' . $this->name;
3494 }
3495
3496 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3497 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3498 $ADMIN = $adminroot; // may be used in settings.php
3499 $auth = $this; // also to be used inside settings.php
3500 $section = $this->get_settings_section_name();
3501
3502 $settings = null;
3503 if ($hassiteconfig) {
3504 if (file_exists($this->full_path('settings.php'))) {
3505 // TODO: finish implementation of common settings - locking, etc.
3506 $settings = new admin_settingpage($section, $this->displayname,
3507 'moodle/site:config', $this->is_enabled() === false);
3508 include($this->full_path('settings.php')); // this may also set $settings to null
3509 } else {
3510 $settingsurl = new moodle_url('/admin/auth_config.php', array('auth' => $this->name));
3511 $settings = new admin_externalpage($section, $this->displayname,
3512 $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
3513 }
3514 }
3515 if ($settings) {
3516 $ADMIN->add($parentnodename, $settings);
b9934a17
DM
3517 }
3518 }
3519}
3520
b6ad8594 3521
b9934a17
DM
3522/**
3523 * Class for enrolment plugins
3524 */
b6ad8594 3525class plugininfo_enrol extends plugininfo_base {
b9934a17 3526
b9934a17
DM
3527 public function is_enabled() {
3528 global $CFG;
b9934a17 3529
b6ad8594
DM
3530 // We do not actually need whole enrolment classes here so we do not call
3531 // {@link enrol_get_plugins()}. Note that this may produce slightly different
3532 // results, for example if the enrolment plugin does not contain lib.php
3533 // but it is listed in $CFG->enrol_plugins_enabled
3534
b8a6f26e 3535 $enabled = array_flip(explode(',', $CFG->enrol_plugins_enabled));
b9934a17
DM
3536
3537 return isset($enabled[$this->name]);
3538 }
3539
79c5c3fa 3540 public function get_settings_section_name() {
c7a33990
PS
3541 if (file_exists($this->full_path('settings.php'))) {
3542 return 'enrolsettings' . $this->name;
3543 } else {
3544 return null;
3545 }
79c5c3fa 3546 }
b9934a17 3547
79c5c3fa
MG
3548 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3549 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
c7a33990
PS
3550
3551 if (!$hassiteconfig or !file_exists($this->full_path('settings.php'))) {
3552 return;
3553 }
3554 $section = $this->get_settings_section_name();
3555
79c5c3fa
MG
3556 $ADMIN = $adminroot; // may be used in settings.php
3557 $enrol = $this; // also can be used inside settings.php
c7a33990
PS
3558 $settings = new admin_settingpage($section, $this->displayname,
3559 'moodle/site:config', $this->is_enabled() === false);
3560
3561 include($this->full_path('settings.php')); // This may also set $settings to null!
79c5c3fa 3562
79c5c3fa
MG
3563 if ($settings) {
3564 $ADMIN->add($parentnodename, $settings);