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