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