MDL-35661 Loading of plugin settings for modules (plugininfo_mod)
[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
89 /**
90 * Returns a tree of known plugins and information about them
91 *
92 * @param bool $disablecache force reload, cache can be used otherwise
e61aaece
TH
93 * @return array 2D array. The first keys are plugin type names (e.g. qtype);
94 * the second keys are the plugin local name (e.g. multichoice); and
b6ad8594 95 * the values are the corresponding objects extending {@link plugininfo_base}
b9934a17
DM
96 */
97 public function get_plugins($disablecache=false) {
7716057f 98 global $CFG;
b9934a17
DM
99
100 if ($disablecache or is_null($this->pluginsinfo)) {
7d59d8da
PS
101 // Hack: include mod and editor subplugin management classes first,
102 // the adminlib.php is supposed to contain extra admin settings too.
103 require_once($CFG->libdir.'/adminlib.php');
104 foreach(array('mod', 'editor') as $type) {
105 foreach (get_plugin_list($type) as $dir) {
106 if (file_exists("$dir/adminlib.php")) {
107 include_once("$dir/adminlib.php");
108 }
109 }
110 }
b9934a17
DM
111 $this->pluginsinfo = array();
112 $plugintypes = get_plugin_types();
4ed26680 113 $plugintypes = $this->reorder_plugin_types($plugintypes);
b9934a17
DM
114 foreach ($plugintypes as $plugintype => $plugintyperootdir) {
115 if (in_array($plugintype, array('base', 'general'))) {
116 throw new coding_exception('Illegal usage of reserved word for plugin type');
117 }
b6ad8594
DM
118 if (class_exists('plugininfo_' . $plugintype)) {
119 $plugintypeclass = 'plugininfo_' . $plugintype;
b9934a17 120 } else {
b6ad8594 121 $plugintypeclass = 'plugininfo_general';
b9934a17 122 }
b6ad8594
DM
123 if (!in_array('plugininfo_base', class_parents($plugintypeclass))) {
124 throw new coding_exception('Class ' . $plugintypeclass . ' must extend plugininfo_base');
b9934a17
DM
125 }
126 $plugins = call_user_func(array($plugintypeclass, 'get_plugins'), $plugintype, $plugintyperootdir, $plugintypeclass);
127 $this->pluginsinfo[$plugintype] = $plugins;
128 }
dd119e21 129
7716057f 130 if (empty($CFG->disableupdatenotifications) and !during_initial_install()) {
8411c24e
DP
131 // append the information about available updates provided by {@link available_update_checker()}
132 $provider = available_update_checker::instance();
133 foreach ($this->pluginsinfo as $plugintype => $plugins) {
134 foreach ($plugins as $plugininfoholder) {
135 $plugininfoholder->check_available_updates($provider);
136 }
dd119e21
DM
137 }
138 }
b9934a17
DM
139 }
140
141 return $this->pluginsinfo;
142 }
143
144 /**
0242bdc7
TH
145 * Returns list of plugins that define their subplugins and the information
146 * about them from the db/subplugins.php file.
b9934a17 147 *
c57fc98b 148 * At the moment, only activity modules and editors can define subplugins.
b9934a17 149 *
0242bdc7
TH
150 * @param bool $disablecache force reload, cache can be used otherwise
151 * @return array with keys like 'mod_quiz', and values the data from the
152 * corresponding db/subplugins.php file.
b9934a17
DM
153 */
154 public function get_subplugins($disablecache=false) {
155
156 if ($disablecache or is_null($this->subpluginsinfo)) {
157 $this->subpluginsinfo = array();
c57fc98b 158 foreach (array('mod', 'editor') as $type) {
e197d9a4 159 $owners = get_plugin_list($type);
c57fc98b 160 foreach ($owners as $component => $ownerdir) {
161 $componentsubplugins = array();
162 if (file_exists($ownerdir . '/db/subplugins.php')) {
975311d3 163 $subplugins = array();
c57fc98b 164 include($ownerdir . '/db/subplugins.php');
165 foreach ($subplugins as $subplugintype => $subplugintyperootdir) {
166 $subplugin = new stdClass();
167 $subplugin->type = $subplugintype;
168 $subplugin->typerootdir = $subplugintyperootdir;
169 $componentsubplugins[$subplugintype] = $subplugin;
170 }
171 $this->subpluginsinfo[$type . '_' . $component] = $componentsubplugins;
b9934a17 172 }
b9934a17
DM
173 }
174 }
175 }
176
177 return $this->subpluginsinfo;
178 }
179
180 /**
181 * Returns the name of the plugin that defines the given subplugin type
182 *
183 * If the given subplugin type is not actually a subplugin, returns false.
184 *
185 * @param string $subplugintype the name of subplugin type, eg. workshopform or quiz
186 * @return false|string the name of the parent plugin, eg. mod_workshop
187 */
188 public function get_parent_of_subplugin($subplugintype) {
189
190 $parent = false;
191 foreach ($this->get_subplugins() as $pluginname => $subplugintypes) {
192 if (isset($subplugintypes[$subplugintype])) {
193 $parent = $pluginname;
194 break;
195 }
196 }
197
198 return $parent;
199 }
200
201 /**
202 * Returns a localized name of a given plugin
203 *
204 * @param string $plugin name of the plugin, eg mod_workshop or auth_ldap
205 * @return string
206 */
207 public function plugin_name($plugin) {
208 list($type, $name) = normalize_component($plugin);
209 return $this->pluginsinfo[$type][$name]->displayname;
210 }
211
212 /**
213 * Returns a localized name of a plugin type in plural form
214 *
215 * Most plugin types define their names in core_plugin lang file. In case of subplugins,
216 * we try to ask the parent plugin for the name. In the worst case, we will return
217 * the value of the passed $type parameter.
218 *
219 * @param string $type the type of the plugin, e.g. mod or workshopform
220 * @return string
221 */
222 public function plugintype_name_plural($type) {
223
224 if (get_string_manager()->string_exists('type_' . $type . '_plural', 'core_plugin')) {
225 // for most plugin types, their names are defined in core_plugin lang file
226 return get_string('type_' . $type . '_plural', 'core_plugin');
227
228 } else if ($parent = $this->get_parent_of_subplugin($type)) {
229 // if this is a subplugin, try to ask the parent plugin for the name
230 if (get_string_manager()->string_exists('subplugintype_' . $type . '_plural', $parent)) {
231 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type . '_plural', $parent);
232 } else {
233 return $this->plugin_name($parent) . ' / ' . $type;
234 }
235
236 } else {
237 return $type;
238 }
239 }
240
e61aaece
TH
241 /**
242 * @param string $component frankenstyle component name.
b6ad8594 243 * @return plugininfo_base|null the corresponding plugin information.
e61aaece
TH
244 */
245 public function get_plugin_info($component) {
246 list($type, $name) = normalize_component($component);
247 $plugins = $this->get_plugins();
248 if (isset($plugins[$type][$name])) {
249 return $plugins[$type][$name];
250 } else {
251 return null;
252 }
253 }
254
828788f0 255 /**
b6ad8594 256 * Get a list of any other plugins that require this one.
828788f0
TH
257 * @param string $component frankenstyle component name.
258 * @return array of frankensyle component names that require this one.
259 */
260 public function other_plugins_that_require($component) {
261 $others = array();
262 foreach ($this->get_plugins() as $type => $plugins) {
263 foreach ($plugins as $plugin) {
264 $required = $plugin->get_other_required_plugins();
265 if (isset($required[$component])) {
266 $others[] = $plugin->component;
267 }
268 }
269 }
270 return $others;
271 }
272
e61aaece 273 /**
777781d1
TH
274 * Check a dependencies list against the list of installed plugins.
275 * @param array $dependencies compenent name to required version or ANY_VERSION.
276 * @return bool true if all the dependencies are satisfied.
e61aaece 277 */
777781d1
TH
278 public function are_dependencies_satisfied($dependencies) {
279 foreach ($dependencies as $component => $requiredversion) {
e61aaece
TH
280 $otherplugin = $this->get_plugin_info($component);
281 if (is_null($otherplugin)) {
0242bdc7
TH
282 return false;
283 }
284
3f123d92 285 if ($requiredversion != ANY_VERSION and $otherplugin->versiondisk < $requiredversion) {
0242bdc7
TH
286 return false;
287 }
288 }
289
290 return true;
291 }
292
faadd326 293 /**
927cb511
DM
294 * Checks all dependencies for all installed plugins
295 *
296 * This is used by install and upgrade. The array passed by reference as the second
297 * argument is populated with the list of plugins that have failed dependencies (note that
298 * a single plugin can appear multiple times in the $failedplugins).
299 *
faadd326 300 * @param int $moodleversion the version from version.php.
927cb511 301 * @param array $failedplugins to return the list of plugins with non-satisfied dependencies
777781d1 302 * @return bool true if all the dependencies are satisfied for all plugins.
faadd326 303 */
927cb511
DM
304 public function all_plugins_ok($moodleversion, &$failedplugins = array()) {
305
306 $return = true;
faadd326
TH
307 foreach ($this->get_plugins() as $type => $plugins) {
308 foreach ($plugins as $plugin) {
309
3a2300f5 310 if (!$plugin->is_core_dependency_satisfied($moodleversion)) {
927cb511
DM
311 $return = false;
312 $failedplugins[] = $plugin->component;
faadd326
TH
313 }
314
777781d1 315 if (!$this->are_dependencies_satisfied($plugin->get_other_required_plugins())) {
927cb511
DM
316 $return = false;
317 $failedplugins[] = $plugin->component;
faadd326
TH
318 }
319 }
320 }
321
927cb511 322 return $return;
faadd326
TH
323 }
324
5344ddd1
DM
325 /**
326 * Checks if there are some plugins with a known available update
327 *
328 * @return bool true if there is at least one available update
329 */
330 public function some_plugins_updatable() {
331 foreach ($this->get_plugins() as $type => $plugins) {
332 foreach ($plugins as $plugin) {
333 if ($plugin->available_updates()) {
334 return true;
335 }
336 }
337 }
338
339 return false;
340 }
341
ec8935f5
PS
342 /**
343 * Defines a list of all plugins that were originally shipped in the standard Moodle distribution,
344 * but are not anymore and are deleted during upgrades.
345 *
346 * The main purpose of this list is to hide missing plugins during upgrade.
347 *
348 * @param string $type plugin type
349 * @param string $name plugin name
350 * @return bool
351 */
352 public static function is_deleted_standard_plugin($type, $name) {
353 static $plugins = array(
34c72803 354 // do not add 1.9-2.2 plugin removals here
ec8935f5
PS
355 );
356
357 if (!isset($plugins[$type])) {
358 return false;
359 }
360 return in_array($name, $plugins[$type]);
361 }
362
b9934a17
DM
363 /**
364 * Defines a white list of all plugins shipped in the standard Moodle distribution
365 *
ec8935f5 366 * @param string $type
b9934a17
DM
367 * @return false|array array of standard plugins or false if the type is unknown
368 */
369 public static function standard_plugins_list($type) {
370 static $standard_plugins = array(
371
372 'assignment' => array(
373 'offline', 'online', 'upload', 'uploadsingle'
374 ),
375
1619a38b
DP
376 'assignsubmission' => array(
377 'comments', 'file', 'onlinetext'
378 ),
379
380 'assignfeedback' => array(
fcae4a0c 381 'comments', 'file', 'offline'
1619a38b
DP
382 ),
383
b9934a17
DM
384 'auth' => array(
385 'cas', 'db', 'email', 'fc', 'imap', 'ldap', 'manual', 'mnet',
386 'nntp', 'nologin', 'none', 'pam', 'pop3', 'radius',
387 'shibboleth', 'webservice'
388 ),
389
390 'block' => array(
391 'activity_modules', 'admin_bookmarks', 'blog_menu',
392 'blog_recent', 'blog_tags', 'calendar_month',
393 'calendar_upcoming', 'comments', 'community',
394 'completionstatus', 'course_list', 'course_overview',
395 'course_summary', 'feedback', 'glossary_random', 'html',
396 'login', 'mentees', 'messages', 'mnet_hosts', 'myprofile',
397 'navigation', 'news_items', 'online_users', 'participants',
398 'private_files', 'quiz_results', 'recent_activity',
f68cef22 399 'rss_client', 'search_forums', 'section_links',
b9934a17
DM
400 'selfcompletion', 'settings', 'site_main_menu',
401 'social_activities', 'tag_flickr', 'tag_youtube', 'tags'
402 ),
403
f7e6dd4d
EL
404 'booktool' => array(
405 'exportimscp', 'importhtml', 'print'
406 ),
407
b9934a17 408 'coursereport' => array(
a2a444ab 409 //deprecated!
b9934a17
DM
410 ),
411
412 'datafield' => array(
413 'checkbox', 'date', 'file', 'latlong', 'menu', 'multimenu',
414 'number', 'picture', 'radiobutton', 'text', 'textarea', 'url'
415 ),
416
417 'datapreset' => array(
418 'imagegallery'
419 ),
420
421 'editor' => array(
422 'textarea', 'tinymce'
423 ),
424
425 'enrol' => array(
426 'authorize', 'category', 'cohort', 'database', 'flatfile',
427 'guest', 'imsenterprise', 'ldap', 'manual', 'meta', 'mnet',
428 'paypal', 'self'
429 ),
430
431 'filter' => array(
432 'activitynames', 'algebra', 'censor', 'emailprotect',
433 'emoticon', 'mediaplugin', 'multilang', 'tex', 'tidy',
87783982 434 'urltolink', 'data', 'glossary'
b9934a17
DM
435 ),
436
437 'format' => array(
438 'scorm', 'social', 'topics', 'weeks'
439 ),
440
441 'gradeexport' => array(
442 'ods', 'txt', 'xls', 'xml'
443 ),
444
445 'gradeimport' => array(
446 'csv', 'xml'
447 ),
448
449 'gradereport' => array(
450 'grader', 'outcomes', 'overview', 'user'
451 ),
452
f59f488a 453 'gradingform' => array(
77143217 454 'rubric', 'guide'
f59f488a
DM
455 ),
456
b9934a17
DM
457 'local' => array(
458 ),
459
460 'message' => array(
461 'email', 'jabber', 'popup'
462 ),
463
464 'mnetservice' => array(
465 'enrol'
466 ),
467
468 'mod' => array(
f7e6dd4d 469 'assign', 'assignment', 'book', 'chat', 'choice', 'data', 'feedback', 'folder',
7fdee5b6 470 'forum', 'glossary', 'imscp', 'label', 'lesson', 'lti', 'page',
b9934a17
DM
471 'quiz', 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop'
472 ),
473
474 'plagiarism' => array(
475 ),
476
477 'portfolio' => array(
478 'boxnet', 'download', 'flickr', 'googledocs', 'mahara', 'picasa'
479 ),
480
481 'profilefield' => array(
482 'checkbox', 'datetime', 'menu', 'text', 'textarea'
483 ),
484
d1c77ac3
DM
485 'qbehaviour' => array(
486 'adaptive', 'adaptivenopenalty', 'deferredcbm',
487 'deferredfeedback', 'immediatecbm', 'immediatefeedback',
488 'informationitem', 'interactive', 'interactivecountback',
489 'manualgraded', 'missing'
490 ),
491
b9934a17
DM
492 'qformat' => array(
493 'aiken', 'blackboard', 'blackboard_six', 'examview', 'gift',
2dc54611 494 'learnwise', 'missingword', 'multianswer', 'webct',
b9934a17
DM
495 'xhtml', 'xml'
496 ),
497
498 'qtype' => array(
499 'calculated', 'calculatedmulti', 'calculatedsimple',
500 'description', 'essay', 'match', 'missingtype', 'multianswer',
501 'multichoice', 'numerical', 'random', 'randomsamatch',
502 'shortanswer', 'truefalse'
503 ),
504
505 'quiz' => array(
506 'grading', 'overview', 'responses', 'statistics'
507 ),
508
c999d841
TH
509 'quizaccess' => array(
510 'delaybetweenattempts', 'ipaddress', 'numattempts', 'openclosedate',
511 'password', 'safebrowser', 'securewindow', 'timelimit'
512 ),
513
b9934a17 514 'report' => array(
13fdaaac 515 'backups', 'completion', 'configlog', 'courseoverview',
8a8f29c2 516 'log', 'loglive', 'outline', 'participation', 'progress', 'questioninstances', 'security', 'stats'
b9934a17
DM
517 ),
518
519 'repository' => array(
daf28d86 520 'alfresco', 'boxnet', 'coursefiles', 'dropbox', 'equella', 'filesystem',
b9934a17
DM
521 'flickr', 'flickr_public', 'googledocs', 'local', 'merlot',
522 'picasa', 'recent', 's3', 'upload', 'url', 'user', 'webdav',
523 'wikimedia', 'youtube'
524 ),
525
99e86561 526 'scormreport' => array(
8f1a0d21 527 'basic',
e61a7137
AKA
528 'interactions',
529 'graphs'
99e86561
PS
530 ),
531
29e03690
PS
532 'tinymce' => array(
533 'dragmath', 'moodleemoticon', 'moodleimage', 'moodlemedia', 'moodlenolink', 'spellchecker',
534 ),
535
b9934a17 536 'theme' => array(
bef9ad95
DM
537 'afterburner', 'anomaly', 'arialist', 'base', 'binarius',
538 'boxxie', 'brick', 'canvas', 'formal_white', 'formfactor',
98ca9e84
EL
539 'fusion', 'leatherbound', 'magazine', 'mymobile', 'nimble',
540 'nonzero', 'overlay', 'serenity', 'sky_high', 'splash',
541 'standard', 'standardold'
b9934a17
DM
542 ),
543
11b24ce7 544 'tool' => array(
db9d7be6 545 'assignmentupgrade', 'capability', 'customlang', 'dbtransfer', 'generator',
a3d5830a 546 'health', 'innodb', 'langimport', 'multilangupgrade', 'phpunit', 'profiling',
fab6f7b7 547 'qeupgradehelper', 'replace', 'spamcleaner', 'timezoneimport', 'unittest',
9597e00b 548 'uploaduser', 'unsuproles', 'xmldb'
11b24ce7
PS
549 ),
550
b9934a17
DM
551 'webservice' => array(
552 'amf', 'rest', 'soap', 'xmlrpc'
553 ),
554
555 'workshopallocation' => array(
98621280 556 'manual', 'random', 'scheduled'
b9934a17
DM
557 ),
558
559 'workshopeval' => array(
560 'best'
561 ),
562
563 'workshopform' => array(
564 'accumulative', 'comments', 'numerrors', 'rubric'
565 )
566 );
567
568 if (isset($standard_plugins[$type])) {
569 return $standard_plugins[$type];
b9934a17
DM
570 } else {
571 return false;
572 }
573 }
4ed26680
DM
574
575 /**
660c4d46 576 * Reorders plugin types into a sequence to be displayed
4ed26680
DM
577 *
578 * For technical reasons, plugin types returned by {@link get_plugin_types()} are
579 * in a certain order that does not need to fit the expected order for the display.
580 * Particularly, activity modules should be displayed first as they represent the
581 * real heart of Moodle. They should be followed by other plugin types that are
582 * used to build the courses (as that is what one expects from LMS). After that,
583 * other supportive plugin types follow.
584 *
585 * @param array $types associative array
586 * @return array same array with altered order of items
587 */
588 protected function reorder_plugin_types(array $types) {
589 $fix = array(
590 'mod' => $types['mod'],
591 'block' => $types['block'],
592 'qtype' => $types['qtype'],
593 'qbehaviour' => $types['qbehaviour'],
594 'qformat' => $types['qformat'],
595 'filter' => $types['filter'],
596 'enrol' => $types['enrol'],
597 );
598 foreach ($types as $type => $path) {
599 if (!isset($fix[$type])) {
600 $fix[$type] = $path;
601 }
602 }
603 return $fix;
604 }
b9934a17
DM
605}
606
b9934a17 607
b9934a17 608/**
cd0bb55f 609 * General exception thrown by the {@link available_update_checker} class
b9934a17 610 */
cd0bb55f 611class available_update_checker_exception extends moodle_exception {
b9934a17
DM
612
613 /**
cd0bb55f
DM
614 * @param string $errorcode exception description identifier
615 * @param mixed $debuginfo debugging data to display
616 */
617 public function __construct($errorcode, $debuginfo=null) {
618 parent::__construct($errorcode, 'core_plugin', '', null, print_r($debuginfo, true));
619 }
620}
621
622
623/**
624 * Singleton class that handles checking for available updates
625 */
626class available_update_checker {
627
628 /** @var available_update_checker holds the singleton instance */
629 protected static $singletoninstance;
7d8de6d8
DM
630 /** @var null|int the timestamp of when the most recent response was fetched */
631 protected $recentfetch = null;
632 /** @var null|array the recent response from the update notification provider */
633 protected $recentresponse = null;
55585f3a
DM
634 /** @var null|string the numerical version of the local Moodle code */
635 protected $currentversion = null;
4442cc80
DM
636 /** @var null|string the release info of the local Moodle code */
637 protected $currentrelease = null;
55585f3a
DM
638 /** @var null|string branch of the local Moodle code */
639 protected $currentbranch = null;
640 /** @var array of (string)frankestyle => (string)version list of additional plugins deployed at this site */
641 protected $currentplugins = array();
cd0bb55f
DM
642
643 /**
644 * Direct initiation not allowed, use the factory method {@link self::instance()}
645 */
646 protected function __construct() {
cd0bb55f
DM
647 }
648
649 /**
650 * Sorry, this is singleton
651 */
652 protected function __clone() {
653 }
654
655 /**
656 * Factory method for this class
b9934a17 657 *
cd0bb55f
DM
658 * @return available_update_checker the singleton instance
659 */
660 public static function instance() {
661 if (is_null(self::$singletoninstance)) {
662 self::$singletoninstance = new self();
663 }
664 return self::$singletoninstance;
665 }
666
cd0bb55f
DM
667 /**
668 * Returns the timestamp of the last execution of {@link fetch()}
b9934a17 669 *
cd0bb55f 670 * @return int|null null if it has never been executed or we don't known
b9934a17 671 */
cd0bb55f 672 public function get_last_timefetched() {
7d8de6d8
DM
673
674 $this->restore_response();
675
676 if (!empty($this->recentfetch)) {
677 return $this->recentfetch;
678
cd0bb55f 679 } else {
7d8de6d8 680 return null;
cd0bb55f
DM
681 }
682 }
b9934a17
DM
683
684 /**
cd0bb55f 685 * Fetches the available update status from the remote site
b9934a17 686 *
cd0bb55f 687 * @throws available_update_checker_exception
b9934a17 688 */
cd0bb55f 689 public function fetch() {
7d8de6d8 690 $response = $this->get_response();
cd0bb55f 691 $this->validate_response($response);
7d8de6d8 692 $this->store_response($response);
cd0bb55f 693 }
b9934a17
DM
694
695 /**
cd0bb55f 696 * Returns the available update information for the given component
b9934a17 697 *
cd0bb55f 698 * This method returns null if the most recent response does not contain any information
7d8de6d8
DM
699 * about it. The returned structure is an array of available updates for the given
700 * component. Each update info is an object with at least one property called
701 * 'version'. Other possible properties are 'release', 'maturity', 'url' and 'downloadurl'.
cd0bb55f 702 *
c6f008e7
DM
703 * For the 'core' component, the method returns real updates only (those with higher version).
704 * For all other components, the list of all known remote updates is returned and the caller
705 * (usually the {@link plugin_manager}) is supposed to make the actual comparison of versions.
b9934a17 706 *
cd0bb55f 707 * @param string $component frankenstyle
c6f008e7
DM
708 * @param array $options with supported keys 'minmaturity' and/or 'notifybuilds'
709 * @return null|array null or array of available_update_info objects
b9934a17 710 */
c6f008e7
DM
711 public function get_update_info($component, array $options = array()) {
712
713 if (!isset($options['minmaturity'])) {
714 $options['minmaturity'] = 0;
715 }
716
717 if (!isset($options['notifybuilds'])) {
718 $options['notifybuilds'] = false;
719 }
720
721 if ($component == 'core') {
722 $this->load_current_environment();
723 }
cd0bb55f 724
7d8de6d8 725 $this->restore_response();
cd0bb55f 726
c6f008e7
DM
727 if (empty($this->recentresponse['updates'][$component])) {
728 return null;
729 }
730
731 $updates = array();
732 foreach ($this->recentresponse['updates'][$component] as $info) {
733 $update = new available_update_info($component, $info);
734 if (isset($update->maturity) and ($update->maturity < $options['minmaturity'])) {
735 continue;
7d8de6d8 736 }
c6f008e7
DM
737 if ($component == 'core') {
738 if ($update->version <= $this->currentversion) {
739 continue;
740 }
4442cc80
DM
741 if (empty($options['notifybuilds']) and $this->is_same_release($update->release)) {
742 continue;
743 }
c6f008e7
DM
744 }
745 $updates[] = $update;
746 }
747
748 if (empty($updates)) {
cd0bb55f
DM
749 return null;
750 }
c6f008e7
DM
751
752 return $updates;
cd0bb55f 753 }
b9934a17
DM
754
755 /**
be378880
DM
756 * The method being run via cron.php
757 */
758 public function cron() {
759 global $CFG;
760
761 if (!$this->cron_autocheck_enabled()) {
762 $this->cron_mtrace('Automatic check for available updates not enabled, skipping.');
763 return;
764 }
765
766 $now = $this->cron_current_timestamp();
767
768 if ($this->cron_has_fresh_fetch($now)) {
769 $this->cron_mtrace('Recently fetched info about available updates is still fresh enough, skipping.');
770 return;
771 }
772
773 if ($this->cron_has_outdated_fetch($now)) {
774 $this->cron_mtrace('Outdated or missing info about available updates, forced fetching ... ', '');
775 $this->cron_execute();
776 return;
777 }
778
779 $offset = $this->cron_execution_offset();
780 $start = mktime(1, 0, 0, date('n', $now), date('j', $now), date('Y', $now)); // 01:00 AM today local time
781 if ($now > $start + $offset) {
782 $this->cron_mtrace('Regular daily check for available updates ... ', '');
783 $this->cron_execute();
784 return;
785 }
786 }
787
788 /// end of public API //////////////////////////////////////////////////////
789
cd0bb55f 790 /**
7d8de6d8 791 * Makes cURL request to get data from the remote site
b9934a17 792 *
7d8de6d8 793 * @return string raw request result
cd0bb55f
DM
794 * @throws available_update_checker_exception
795 */
7d8de6d8 796 protected function get_response() {
b4bfdf5a
PS
797 global $CFG;
798 require_once($CFG->libdir.'/filelib.php');
799
cd0bb55f
DM
800 $curl = new curl(array('proxy' => true));
801 $response = $curl->post($this->prepare_request_url(), $this->prepare_request_params());
802 $curlinfo = $curl->get_info();
803 if ($curlinfo['http_code'] != 200) {
804 throw new available_update_checker_exception('err_response_http_code', $curlinfo['http_code']);
805 }
cd0bb55f
DM
806 return $response;
807 }
808
809 /**
810 * Makes sure the response is valid, has correct API format etc.
811 *
7d8de6d8 812 * @param string $response raw response as returned by the {@link self::get_response()}
cd0bb55f
DM
813 * @throws available_update_checker_exception
814 */
7d8de6d8
DM
815 protected function validate_response($response) {
816
817 $response = $this->decode_response($response);
cd0bb55f
DM
818
819 if (empty($response)) {
820 throw new available_update_checker_exception('err_response_empty');
821 }
822
7d8de6d8
DM
823 if (empty($response['status']) or $response['status'] !== 'OK') {
824 throw new available_update_checker_exception('err_response_status', $response['status']);
825 }
826
827 if (empty($response['apiver']) or $response['apiver'] !== '1.0') {
828 throw new available_update_checker_exception('err_response_format_version', $response['apiver']);
cd0bb55f
DM
829 }
830
7d8de6d8 831 if (empty($response['forbranch']) or $response['forbranch'] !== moodle_major_version(true)) {
d5d2e353 832 throw new available_update_checker_exception('err_response_target_version', $response['forbranch']);
cd0bb55f
DM
833 }
834 }
835
836 /**
7d8de6d8 837 * Decodes the raw string response from the update notifications provider
b9934a17 838 *
7d8de6d8
DM
839 * @param string $response as returned by {@link self::get_response()}
840 * @return array decoded response structure
b9934a17 841 */
7d8de6d8
DM
842 protected function decode_response($response) {
843 return json_decode($response, true);
cd0bb55f 844 }
b9934a17
DM
845
846 /**
7d8de6d8
DM
847 * Stores the valid fetched response for later usage
848 *
849 * This implementation uses the config_plugins table as the permanent storage.
b9934a17 850 *
7d8de6d8 851 * @param string $response raw valid data returned by {@link self::get_response()}
b9934a17 852 */
7d8de6d8
DM
853 protected function store_response($response) {
854
855 set_config('recentfetch', time(), 'core_plugin');
856 set_config('recentresponse', $response, 'core_plugin');
857
858 $this->restore_response(true);
cd0bb55f 859 }
b9934a17
DM
860
861 /**
7d8de6d8 862 * Loads the most recent raw response record we have fetched
b9934a17 863 *
c62580b9
DM
864 * After this method is called, $this->recentresponse is set to an array. If the
865 * array is empty, then either no data have been fetched yet or the fetched data
866 * do not have expected format (and thence they are ignored and a debugging
867 * message is displayed).
868 *
7d8de6d8 869 * This implementation uses the config_plugins table as the permanent storage.
b9934a17 870 *
7d8de6d8 871 * @param bool $forcereload reload even if it was already loaded
b9934a17 872 */
7d8de6d8
DM
873 protected function restore_response($forcereload = false) {
874
875 if (!$forcereload and !is_null($this->recentresponse)) {
876 // we already have it, nothing to do
877 return;
cd0bb55f
DM
878 }
879
7d8de6d8
DM
880 $config = get_config('core_plugin');
881
882 if (!empty($config->recentresponse) and !empty($config->recentfetch)) {
883 try {
884 $this->validate_response($config->recentresponse);
885 $this->recentfetch = $config->recentfetch;
886 $this->recentresponse = $this->decode_response($config->recentresponse);
660c4d46 887 } catch (available_update_checker_exception $e) {
c62580b9
DM
888 debugging('Invalid info about available updates detected and will be ignored: '.$e->getMessage(), DEBUG_ALL);
889 $this->recentresponse = array();
7d8de6d8
DM
890 }
891
cd0bb55f 892 } else {
7d8de6d8 893 $this->recentresponse = array();
cd0bb55f
DM
894 }
895 }
896
7b35553b
DM
897 /**
898 * Compares two raw {@link $recentresponse} records and returns the list of changed updates
899 *
900 * This method is used to populate potential update info to be sent to site admins.
901 *
19d11b3b
DM
902 * @param array $old
903 * @param array $new
7b35553b
DM
904 * @throws available_update_checker_exception
905 * @return array parts of $new['updates'] that have changed
906 */
19d11b3b 907 protected function compare_responses(array $old, array $new) {
7b35553b 908
19d11b3b 909 if (empty($new)) {
7b35553b
DM
910 return array();
911 }
912
913 if (!array_key_exists('updates', $new)) {
914 throw new available_update_checker_exception('err_response_format');
915 }
916
19d11b3b 917 if (empty($old)) {
7b35553b
DM
918 return $new['updates'];
919 }
920
921 if (!array_key_exists('updates', $old)) {
922 throw new available_update_checker_exception('err_response_format');
923 }
924
925 $changes = array();
926
927 foreach ($new['updates'] as $newcomponent => $newcomponentupdates) {
928 if (empty($old['updates'][$newcomponent])) {
929 $changes[$newcomponent] = $newcomponentupdates;
930 continue;
931 }
932 foreach ($newcomponentupdates as $newcomponentupdate) {
933 $inold = false;
934 foreach ($old['updates'][$newcomponent] as $oldcomponentupdate) {
935 if ($newcomponentupdate['version'] == $oldcomponentupdate['version']) {
936 $inold = true;
937 }
938 }
939 if (!$inold) {
940 if (!isset($changes[$newcomponent])) {
941 $changes[$newcomponent] = array();
942 }
943 $changes[$newcomponent][] = $newcomponentupdate;
944 }
945 }
946 }
947
948 return $changes;
949 }
950
cd0bb55f
DM
951 /**
952 * Returns the URL to send update requests to
953 *
954 * During the development or testing, you can set $CFG->alternativeupdateproviderurl
955 * to a custom URL that will be used. Otherwise the standard URL will be returned.
956 *
957 * @return string URL
958 */
959 protected function prepare_request_url() {
960 global $CFG;
961
962 if (!empty($CFG->alternativeupdateproviderurl)) {
963 return $CFG->alternativeupdateproviderurl;
964 } else {
965 return 'http://download.moodle.org/api/1.0/updates.php';
966 }
967 }
968
55585f3a 969 /**
4442cc80 970 * Sets the properties currentversion, currentrelease, currentbranch and currentplugins
55585f3a
DM
971 *
972 * @param bool $forcereload
973 */
974 protected function load_current_environment($forcereload=false) {
975 global $CFG;
976
977 if (!is_null($this->currentversion) and !$forcereload) {
978 // nothing to do
979 return;
980 }
981
975311d3
PS
982 $version = null;
983 $release = null;
984
55585f3a
DM
985 require($CFG->dirroot.'/version.php');
986 $this->currentversion = $version;
4442cc80 987 $this->currentrelease = $release;
55585f3a
DM
988 $this->currentbranch = moodle_major_version(true);
989
990 $pluginman = plugin_manager::instance();
991 foreach ($pluginman->get_plugins() as $type => $plugins) {
992 foreach ($plugins as $plugin) {
993 if (!$plugin->is_standard()) {
994 $this->currentplugins[$plugin->component] = $plugin->versiondisk;
995 }
996 }
997 }
998 }
999
cd0bb55f
DM
1000 /**
1001 * Returns the list of HTTP params to be sent to the updates provider URL
1002 *
1003 * @return array of (string)param => (string)value
1004 */
1005 protected function prepare_request_params() {
1006 global $CFG;
1007
55585f3a 1008 $this->load_current_environment();
7d8de6d8
DM
1009 $this->restore_response();
1010
cd0bb55f
DM
1011 $params = array();
1012 $params['format'] = 'json';
1013
7d8de6d8
DM
1014 if (isset($this->recentresponse['ticket'])) {
1015 $params['ticket'] = $this->recentresponse['ticket'];
cd0bb55f
DM
1016 }
1017
55585f3a
DM
1018 if (isset($this->currentversion)) {
1019 $params['version'] = $this->currentversion;
1020 } else {
1021 throw new coding_exception('Main Moodle version must be already known here');
cd0bb55f
DM
1022 }
1023
55585f3a
DM
1024 if (isset($this->currentbranch)) {
1025 $params['branch'] = $this->currentbranch;
1026 } else {
1027 throw new coding_exception('Moodle release must be already known here');
1028 }
1029
1030 $plugins = array();
1031 foreach ($this->currentplugins as $plugin => $version) {
1032 $plugins[] = $plugin.'@'.$version;
1033 }
1034 if (!empty($plugins)) {
1035 $params['plugins'] = implode(',', $plugins);
cd0bb55f
DM
1036 }
1037
cd0bb55f
DM
1038 return $params;
1039 }
be378880
DM
1040
1041 /**
1042 * Returns the current timestamp
1043 *
1044 * @return int the timestamp
1045 */
1046 protected function cron_current_timestamp() {
1047 return time();
1048 }
1049
1050 /**
1051 * Output cron debugging info
1052 *
1053 * @see mtrace()
1054 * @param string $msg output message
1055 * @param string $eol end of line
1056 */
1057 protected function cron_mtrace($msg, $eol = PHP_EOL) {
1058 mtrace($msg, $eol);
1059 }
1060
1061 /**
1062 * Decide if the autocheck feature is disabled in the server setting
1063 *
1064 * @return bool true if autocheck enabled, false if disabled
1065 */
1066 protected function cron_autocheck_enabled() {
718eb2a5
DM
1067 global $CFG;
1068
be378880
DM
1069 if (empty($CFG->updateautocheck)) {
1070 return false;
1071 } else {
1072 return true;
1073 }
1074 }
1075
1076 /**
1077 * Decide if the recently fetched data are still fresh enough
1078 *
1079 * @param int $now current timestamp
1080 * @return bool true if no need to re-fetch, false otherwise
1081 */
1082 protected function cron_has_fresh_fetch($now) {
1083 $recent = $this->get_last_timefetched();
1084
1085 if (empty($recent)) {
1086 return false;
1087 }
1088
1089 if ($now < $recent) {
1090 $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1091 return true;
1092 }
1093
7092ea5d 1094 if ($now - $recent > 24 * HOURSECS) {
be378880
DM
1095 return false;
1096 }
1097
1098 return true;
1099 }
1100
1101 /**
1102 * Decide if the fetch is outadated or even missing
1103 *
1104 * @param int $now current timestamp
1105 * @return bool false if no need to re-fetch, true otherwise
1106 */
1107 protected function cron_has_outdated_fetch($now) {
1108 $recent = $this->get_last_timefetched();
1109
1110 if (empty($recent)) {
1111 return true;
1112 }
1113
1114 if ($now < $recent) {
1115 $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1116 return false;
1117 }
1118
1119 if ($now - $recent > 48 * HOURSECS) {
1120 return true;
1121 }
1122
1123 return false;
1124 }
1125
1126 /**
1127 * Returns the cron execution offset for this site
1128 *
1129 * The main {@link self::cron()} is supposed to run every night in some random time
1130 * between 01:00 and 06:00 AM (local time). The exact moment is defined by so called
1131 * execution offset, that is the amount of time after 01:00 AM. The offset value is
1132 * initially generated randomly and then used consistently at the site. This way, the
1133 * regular checks against the download.moodle.org server are spread in time.
1134 *
1135 * @return int the offset number of seconds from range 1 sec to 5 hours
1136 */
1137 protected function cron_execution_offset() {
1138 global $CFG;
1139
1140 if (empty($CFG->updatecronoffset)) {
1141 set_config('updatecronoffset', rand(1, 5 * HOURSECS));
1142 }
1143
1144 return $CFG->updatecronoffset;
1145 }
1146
1147 /**
1148 * Fetch available updates info and eventually send notification to site admins
1149 */
1150 protected function cron_execute() {
7b35553b 1151
19d11b3b 1152 try {
fd87d0bf
AB
1153 $this->restore_response();
1154 $previous = $this->recentresponse;
1155 $this->fetch();
1156 $this->restore_response(true);
1157 $current = $this->recentresponse;
19d11b3b
DM
1158 $changes = $this->compare_responses($previous, $current);
1159 $notifications = $this->cron_notifications($changes);
1160 $this->cron_notify($notifications);
a77141a7 1161 $this->cron_mtrace('done');
19d11b3b
DM
1162 } catch (available_update_checker_exception $e) {
1163 $this->cron_mtrace('FAILED!');
1164 }
1165 }
1166
1167 /**
1168 * Given the list of changes in available updates, pick those to send to site admins
1169 *
1170 * @param array $changes as returned by {@link self::compare_responses()}
1171 * @return array of available_update_info objects to send to site admins
1172 */
1173 protected function cron_notifications(array $changes) {
1174 global $CFG;
1175
1176 $notifications = array();
1177 $pluginman = plugin_manager::instance();
1178 $plugins = $pluginman->get_plugins(true);
1179
1180 foreach ($changes as $component => $componentchanges) {
718eb2a5
DM
1181 if (empty($componentchanges)) {
1182 continue;
1183 }
19d11b3b
DM
1184 $componentupdates = $this->get_update_info($component,
1185 array('minmaturity' => $CFG->updateminmaturity, 'notifybuilds' => $CFG->updatenotifybuilds));
718eb2a5
DM
1186 if (empty($componentupdates)) {
1187 continue;
1188 }
19d11b3b
DM
1189 // notify only about those $componentchanges that are present in $componentupdates
1190 // to respect the preferences
1191 foreach ($componentchanges as $componentchange) {
1192 foreach ($componentupdates as $componentupdate) {
1193 if ($componentupdate->version == $componentchange['version']) {
1194 if ($component == 'core') {
1195 // in case of 'core' this is enough, we already know that the
1196 // $componentupdate is a real update with higher version
1197 $notifications[] = $componentupdate;
1198 } else {
1199 // use the plugin_manager to check if the reported $componentchange
1200 // is a real update with higher version. such a real update must be
1201 // present in the 'availableupdates' property of one of the component's
1202 // available_update_info object
1203 list($plugintype, $pluginname) = normalize_component($component);
1204 if (!empty($plugins[$plugintype][$pluginname]->availableupdates)) {
1205 foreach ($plugins[$plugintype][$pluginname]->availableupdates as $availableupdate) {
1206 if ($availableupdate->version == $componentchange['version']) {
1207 $notifications[] = $componentupdate;
1208 }
1209 }
1210 }
1211 }
1212 }
1213 }
1214 }
1215 }
1216
1217 return $notifications;
be378880 1218 }
a77141a7
DM
1219
1220 /**
1221 * Sends the given notifications to site admins via messaging API
1222 *
1223 * @param array $notifications array of available_update_info objects to send
1224 */
1225 protected function cron_notify(array $notifications) {
1226 global $CFG;
1227
1228 if (empty($notifications)) {
1229 return;
1230 }
1231
1232 $admins = get_admins();
1233
1234 if (empty($admins)) {
1235 return;
1236 }
1237
1238 $this->cron_mtrace('sending notifications ... ', '');
1239
1240 $text = get_string('updatenotifications', 'core_admin') . PHP_EOL;
1241 $html = html_writer::tag('h1', get_string('updatenotifications', 'core_admin')) . PHP_EOL;
1242
1243 $coreupdates = array();
1244 $pluginupdates = array();
1245
660c4d46 1246 foreach ($notifications as $notification) {
a77141a7
DM
1247 if ($notification->component == 'core') {
1248 $coreupdates[] = $notification;
1249 } else {
1250 $pluginupdates[] = $notification;
1251 }
1252 }
1253
1254 if (!empty($coreupdates)) {
1255 $text .= PHP_EOL . get_string('updateavailable', 'core_admin') . PHP_EOL;
1256 $html .= html_writer::tag('h2', get_string('updateavailable', 'core_admin')) . PHP_EOL;
1257 $html .= html_writer::start_tag('ul') . PHP_EOL;
1258 foreach ($coreupdates as $coreupdate) {
1259 $html .= html_writer::start_tag('li');
1260 if (isset($coreupdate->release)) {
1261 $text .= get_string('updateavailable_release', 'core_admin', $coreupdate->release);
1262 $html .= html_writer::tag('strong', get_string('updateavailable_release', 'core_admin', $coreupdate->release));
1263 }
1264 if (isset($coreupdate->version)) {
1265 $text .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
1266 $html .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
1267 }
1268 if (isset($coreupdate->maturity)) {
1269 $text .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
1270 $html .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
1271 }
1272 $text .= PHP_EOL;
1273 $html .= html_writer::end_tag('li') . PHP_EOL;
1274 }
1275 $text .= PHP_EOL;
1276 $html .= html_writer::end_tag('ul') . PHP_EOL;
1277
1278 $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/index.php');
1279 $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
1280 $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/index.php', $CFG->wwwroot.'/'.$CFG->admin.'/index.php'));
1281 $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
1282 }
1283
1284 if (!empty($pluginupdates)) {
1285 $text .= PHP_EOL . get_string('updateavailableforplugin', 'core_admin') . PHP_EOL;
1286 $html .= html_writer::tag('h2', get_string('updateavailableforplugin', 'core_admin')) . PHP_EOL;
1287
1288 $html .= html_writer::start_tag('ul') . PHP_EOL;
1289 foreach ($pluginupdates as $pluginupdate) {
1290 $html .= html_writer::start_tag('li');
1291 $text .= get_string('pluginname', $pluginupdate->component);
1292 $html .= html_writer::tag('strong', get_string('pluginname', $pluginupdate->component));
1293
1294 $text .= ' ('.$pluginupdate->component.')';
1295 $html .= ' ('.$pluginupdate->component.')';
1296
1297 $text .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
1298 $html .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
1299
1300 $text .= PHP_EOL;
1301 $html .= html_writer::end_tag('li') . PHP_EOL;
1302 }
1303 $text .= PHP_EOL;
1304 $html .= html_writer::end_tag('ul') . PHP_EOL;
b9934a17 1305
a77141a7
DM
1306 $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php');
1307 $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
1308 $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/plugins.php', $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php'));
1309 $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
1310 }
1311
1312 $a = array('siteurl' => $CFG->wwwroot);
1313 $text .= get_string('updatenotificationfooter', 'core_admin', $a) . PHP_EOL;
1314 $a = array('siteurl' => html_writer::link($CFG->wwwroot, $CFG->wwwroot));
1315 $html .= html_writer::tag('footer', html_writer::tag('p', get_string('updatenotificationfooter', 'core_admin', $a),
1316 array('style' => 'font-size:smaller; color:#333;')));
1317
1318 $mainadmin = reset($admins);
1319
1320 foreach ($admins as $admin) {
1321 $message = new stdClass();
1322 $message->component = 'moodle';
1323 $message->name = 'availableupdate';
1324 $message->userfrom = $mainadmin;
1325 $message->userto = $admin;
1326 $message->subject = get_string('updatenotifications', 'core_admin');
1327 $message->fullmessage = $text;
1328 $message->fullmessageformat = FORMAT_PLAIN;
1329 $message->fullmessagehtml = $html;
cd89994d
DM
1330 $message->smallmessage = get_string('updatenotifications', 'core_admin');
1331 $message->notification = 1;
a77141a7
DM
1332 message_send($message);
1333 }
1334 }
b9934a17
DM
1335
1336 /**
4442cc80 1337 * Compare two release labels and decide if they are the same
b9934a17 1338 *
4442cc80
DM
1339 * @param string $remote release info of the available update
1340 * @param null|string $local release info of the local code, defaults to $release defined in version.php
1341 * @return boolean true if the releases declare the same minor+major version
b9934a17 1342 */
4442cc80 1343 protected function is_same_release($remote, $local=null) {
b9934a17 1344
4442cc80
DM
1345 if (is_null($local)) {
1346 $this->load_current_environment();
1347 $local = $this->currentrelease;
1348 }
0242bdc7 1349
4442cc80 1350 $pattern = '/^([0-9\.\+]+)([^(]*)/';
b9934a17 1351
4442cc80
DM
1352 preg_match($pattern, $remote, $remotematches);
1353 preg_match($pattern, $local, $localmatches);
b9934a17 1354
4442cc80
DM
1355 $remotematches[1] = str_replace('+', '', $remotematches[1]);
1356 $localmatches[1] = str_replace('+', '', $localmatches[1]);
1357
1358 if ($remotematches[1] === $localmatches[1] and rtrim($remotematches[2]) === rtrim($localmatches[2])) {
1359 return true;
1360 } else {
1361 return false;
1362 }
1363 }
cd0bb55f
DM
1364}
1365
1366
7d8de6d8
DM
1367/**
1368 * Defines the structure of objects returned by {@link available_update_checker::get_update_info()}
1369 */
1370class available_update_info {
1371
1372 /** @var string frankenstyle component name */
1373 public $component;
1374 /** @var int the available version of the component */
1375 public $version;
1376 /** @var string|null optional release name */
1377 public $release = null;
1378 /** @var int|null optional maturity info, eg {@link MATURITY_STABLE} */
1379 public $maturity = null;
1380 /** @var string|null optional URL of a page with more info about the update */
1381 public $url = null;
1382 /** @var string|null optional URL of a ZIP package that can be downloaded and installed */
1383 public $download = null;
1384
1385 /**
1386 * Creates new instance of the class
b9934a17 1387 *
7d8de6d8
DM
1388 * The $info array must provide at least the 'version' value and optionally all other
1389 * values to populate the object's properties.
b9934a17 1390 *
7d8de6d8
DM
1391 * @param string $name the frankenstyle component name
1392 * @param array $info associative array with other properties
1393 */
1394 public function __construct($name, array $info) {
1395 $this->component = $name;
1396 foreach ($info as $k => $v) {
1397 if (property_exists('available_update_info', $k) and $k != 'component') {
1398 $this->$k = $v;
1399 }
1400 }
1401 }
1402}
1403
1404
00ef3c3e
DM
1405/**
1406 * Factory class producing required subclasses of {@link plugininfo_base}
1407 */
1408class plugininfo_default_factory {
b9934a17
DM
1409
1410 /**
00ef3c3e 1411 * Makes a new instance of the plugininfo class
b9934a17 1412 *
00ef3c3e
DM
1413 * @param string $type the plugin type, eg. 'mod'
1414 * @param string $typerootdir full path to the location of all the plugins of this type
1415 * @param string $name the plugin name, eg. 'workshop'
1416 * @param string $namerootdir full path to the location of the plugin
1417 * @param string $typeclass the name of class that holds the info about the plugin
1418 * @return plugininfo_base the instance of $typeclass
1419 */
1420 public static function make($type, $typerootdir, $name, $namerootdir, $typeclass) {
1421 $plugin = new $typeclass();
1422 $plugin->type = $type;
1423 $plugin->typerootdir = $typerootdir;
1424 $plugin->name = $name;
1425 $plugin->rootdir = $namerootdir;
1426
1427 $plugin->init_display_name();
1428 $plugin->load_disk_version();
1429 $plugin->load_db_version();
1430 $plugin->load_required_main_version();
1431 $plugin->init_is_standard();
473289a0 1432
00ef3c3e
DM
1433 return $plugin;
1434 }
b9934a17
DM
1435}
1436
00ef3c3e 1437
b9934a17 1438/**
b6ad8594 1439 * Base class providing access to the information about a plugin
828788f0
TH
1440 *
1441 * @property-read string component the component name, type_name
b9934a17 1442 */
b6ad8594 1443abstract class plugininfo_base {
b9934a17
DM
1444
1445 /** @var string the plugintype name, eg. mod, auth or workshopform */
1446 public $type;
1447 /** @var string full path to the location of all the plugins of this type */
1448 public $typerootdir;
1449 /** @var string the plugin name, eg. assignment, ldap */
1450 public $name;
1451 /** @var string the localized plugin name */
1452 public $displayname;
1453 /** @var string the plugin source, one of plugin_manager::PLUGIN_SOURCE_xxx constants */
1454 public $source;
1455 /** @var fullpath to the location of this plugin */
1456 public $rootdir;
1457 /** @var int|string the version of the plugin's source code */
1458 public $versiondisk;
1459 /** @var int|string the version of the installed plugin */
1460 public $versiondb;
1461 /** @var int|float|string required version of Moodle core */
1462 public $versionrequires;
b6ad8594
DM
1463 /** @var array other plugins that this one depends on, lazy-loaded by {@link get_other_required_plugins()} */
1464 public $dependencies;
b9934a17
DM
1465 /** @var int number of instances of the plugin - not supported yet */
1466 public $instances;
1467 /** @var int order of the plugin among other plugins of the same type - not supported yet */
1468 public $sortorder;
7d8de6d8
DM
1469 /** @var array|null array of {@link available_update_info} for this plugin */
1470 public $availableupdates;
b9934a17
DM
1471
1472 /**
b6ad8594
DM
1473 * Gathers and returns the information about all plugins of the given type
1474 *
b6ad8594
DM
1475 * @param string $type the name of the plugintype, eg. mod, auth or workshopform
1476 * @param string $typerootdir full path to the location of the plugin dir
1477 * @param string $typeclass the name of the actually called class
1478 * @return array of plugintype classes, indexed by the plugin name
b9934a17
DM
1479 */
1480 public static function get_plugins($type, $typerootdir, $typeclass) {
1481
1482 // get the information about plugins at the disk
1483 $plugins = get_plugin_list($type);
1484 $ondisk = array();
1485 foreach ($plugins as $pluginname => $pluginrootdir) {
00ef3c3e
DM
1486 $ondisk[$pluginname] = plugininfo_default_factory::make($type, $typerootdir,
1487 $pluginname, $pluginrootdir, $typeclass);
b9934a17
DM
1488 }
1489 return $ondisk;
1490 }
1491
1492 /**
b6ad8594 1493 * Sets {@link $displayname} property to a localized name of the plugin
b9934a17 1494 */
b8343e68 1495 public function init_display_name() {
828788f0
TH
1496 if (!get_string_manager()->string_exists('pluginname', $this->component)) {
1497 $this->displayname = '[pluginname,' . $this->component . ']';
b9934a17 1498 } else {
828788f0
TH
1499 $this->displayname = get_string('pluginname', $this->component);
1500 }
1501 }
1502
1503 /**
1504 * Magic method getter, redirects to read only values.
b6ad8594 1505 *
828788f0
TH
1506 * @param string $name
1507 * @return mixed
1508 */
1509 public function __get($name) {
1510 switch ($name) {
1511 case 'component': return $this->type . '_' . $this->name;
1512
1513 default:
1514 debugging('Invalid plugin property accessed! '.$name);
1515 return null;
b9934a17
DM
1516 }
1517 }
1518
1519 /**
b6ad8594
DM
1520 * Return the full path name of a file within the plugin.
1521 *
1522 * No check is made to see if the file exists.
1523 *
1524 * @param string $relativepath e.g. 'version.php'.
1525 * @return string e.g. $CFG->dirroot . '/mod/quiz/version.php'.
b9934a17 1526 */
473289a0 1527 public function full_path($relativepath) {
b9934a17 1528 if (empty($this->rootdir)) {
473289a0 1529 return '';
b9934a17 1530 }
473289a0
TH
1531 return $this->rootdir . '/' . $relativepath;
1532 }
b9934a17 1533
473289a0
TH
1534 /**
1535 * Load the data from version.php.
b6ad8594
DM
1536 *
1537 * @return stdClass the object called $plugin defined in version.php
473289a0
TH
1538 */
1539 protected function load_version_php() {
1540 $versionfile = $this->full_path('version.php');
b9934a17 1541
473289a0 1542 $plugin = new stdClass();
b9934a17
DM
1543 if (is_readable($versionfile)) {
1544 include($versionfile);
b9934a17 1545 }
473289a0 1546 return $plugin;
b9934a17
DM
1547 }
1548
1549 /**
b6ad8594
DM
1550 * Sets {@link $versiondisk} property to a numerical value representing the
1551 * version of the plugin's source code.
1552 *
1553 * If the value is null after calling this method, either the plugin
1554 * does not use versioning (typically does not have any database
1555 * data) or is missing from disk.
b9934a17 1556 */
473289a0
TH
1557 public function load_disk_version() {
1558 $plugin = $this->load_version_php();
1559 if (isset($plugin->version)) {
1560 $this->versiondisk = $plugin->version;
b9934a17
DM
1561 }
1562 }
1563
1564 /**
b6ad8594
DM
1565 * Sets {@link $versionrequires} property to a numerical value representing
1566 * the version of Moodle core that this plugin requires.
b9934a17 1567 */
b8343e68 1568 public function load_required_main_version() {
473289a0
TH
1569 $plugin = $this->load_version_php();
1570 if (isset($plugin->requires)) {
1571 $this->versionrequires = $plugin->requires;
b9934a17 1572 }
473289a0 1573 }
b9934a17 1574
0242bdc7 1575 /**
777781d1 1576 * Initialise {@link $dependencies} to the list of other plugins (in any)
0242bdc7
TH
1577 * that this one requires to be installed.
1578 */
1579 protected function load_other_required_plugins() {
1580 $plugin = $this->load_version_php();
777781d1
TH
1581 if (!empty($plugin->dependencies)) {
1582 $this->dependencies = $plugin->dependencies;
0242bdc7 1583 } else {
777781d1 1584 $this->dependencies = array(); // By default, no dependencies.
0242bdc7
TH
1585 }
1586 }
1587
1588 /**
b6ad8594
DM
1589 * Get the list of other plugins that this plugin requires to be installed.
1590 *
1591 * @return array with keys the frankenstyle plugin name, and values either
1592 * a version string (like '2011101700') or the constant ANY_VERSION.
0242bdc7
TH
1593 */
1594 public function get_other_required_plugins() {
777781d1 1595 if (is_null($this->dependencies)) {
0242bdc7
TH
1596 $this->load_other_required_plugins();
1597 }
777781d1 1598 return $this->dependencies;
0242bdc7
TH
1599 }
1600
473289a0 1601 /**
b6ad8594
DM
1602 * Sets {@link $versiondb} property to a numerical value representing the
1603 * currently installed version of the plugin.
1604 *
1605 * If the value is null after calling this method, either the plugin
1606 * does not use versioning (typically does not have any database
1607 * data) or has not been installed yet.
473289a0
TH
1608 */
1609 public function load_db_version() {
828788f0 1610 if ($ver = self::get_version_from_config_plugins($this->component)) {
473289a0 1611 $this->versiondb = $ver;
b9934a17
DM
1612 }
1613 }
1614
1615 /**
b6ad8594
DM
1616 * Sets {@link $source} property to one of plugin_manager::PLUGIN_SOURCE_xxx
1617 * constants.
1618 *
1619 * If the property's value is null after calling this method, then
1620 * the type of the plugin has not been recognized and you should throw
1621 * an exception.
b9934a17 1622 */
b8343e68 1623 public function init_is_standard() {
b9934a17
DM
1624
1625 $standard = plugin_manager::standard_plugins_list($this->type);
1626
1627 if ($standard !== false) {
1628 $standard = array_flip($standard);
1629 if (isset($standard[$this->name])) {
1630 $this->source = plugin_manager::PLUGIN_SOURCE_STANDARD;
ec8935f5
PS
1631 } else if (!is_null($this->versiondb) and is_null($this->versiondisk)
1632 and plugin_manager::is_deleted_standard_plugin($this->type, $this->name)) {
1633 $this->source = plugin_manager::PLUGIN_SOURCE_STANDARD; // to be deleted
b9934a17
DM
1634 } else {
1635 $this->source = plugin_manager::PLUGIN_SOURCE_EXTENSION;
1636 }
1637 }
1638 }
1639
1640 /**
b6ad8594
DM
1641 * Returns true if the plugin is shipped with the official distribution
1642 * of the current Moodle version, false otherwise.
1643 *
1644 * @return bool
b9934a17
DM
1645 */
1646 public function is_standard() {
1647 return $this->source === plugin_manager::PLUGIN_SOURCE_STANDARD;
1648 }
1649
3a2300f5
DM
1650 /**
1651 * Returns true if the the given Moodle version is enough to run this plugin
1652 *
1653 * @param string|int|double $moodleversion
1654 * @return bool
1655 */
1656 public function is_core_dependency_satisfied($moodleversion) {
1657
1658 if (empty($this->versionrequires)) {
1659 return true;
1660
1661 } else {
1662 return (double)$this->versionrequires <= (double)$moodleversion;
1663 }
1664 }
1665
b9934a17 1666 /**
b6ad8594
DM
1667 * Returns the status of the plugin
1668 *
1669 * @return string one of plugin_manager::PLUGIN_STATUS_xxx constants
b9934a17
DM
1670 */
1671 public function get_status() {
1672
1673 if (is_null($this->versiondb) and is_null($this->versiondisk)) {
1674 return plugin_manager::PLUGIN_STATUS_NODB;
1675
1676 } else if (is_null($this->versiondb) and !is_null($this->versiondisk)) {
1677 return plugin_manager::PLUGIN_STATUS_NEW;
1678
1679 } else if (!is_null($this->versiondb) and is_null($this->versiondisk)) {
ec8935f5
PS
1680 if (plugin_manager::is_deleted_standard_plugin($this->type, $this->name)) {
1681 return plugin_manager::PLUGIN_STATUS_DELETE;
1682 } else {
1683 return plugin_manager::PLUGIN_STATUS_MISSING;
1684 }
b9934a17
DM
1685
1686 } else if ((string)$this->versiondb === (string)$this->versiondisk) {
1687 return plugin_manager::PLUGIN_STATUS_UPTODATE;
1688
1689 } else if ($this->versiondb < $this->versiondisk) {
1690 return plugin_manager::PLUGIN_STATUS_UPGRADE;
1691
1692 } else if ($this->versiondb > $this->versiondisk) {
1693 return plugin_manager::PLUGIN_STATUS_DOWNGRADE;
1694
1695 } else {
1696 // $version = pi(); and similar funny jokes - hopefully Donald E. Knuth will never contribute to Moodle ;-)
1697 throw new coding_exception('Unable to determine plugin state, check the plugin versions');
1698 }
1699 }
1700
1701 /**
b6ad8594
DM
1702 * Returns the information about plugin availability
1703 *
1704 * True means that the plugin is enabled. False means that the plugin is
1705 * disabled. Null means that the information is not available, or the
1706 * plugin does not support configurable availability or the availability
1707 * can not be changed.
1708 *
1709 * @return null|bool
b9934a17
DM
1710 */
1711 public function is_enabled() {
1712 return null;
1713 }
1714
1715 /**
7d8de6d8 1716 * Populates the property {@link $availableupdates} with the information provided by
dd119e21
DM
1717 * available update checker
1718 *
1719 * @param available_update_checker $provider the class providing the available update info
1720 */
7d8de6d8 1721 public function check_available_updates(available_update_checker $provider) {
c6f008e7
DM
1722 global $CFG;
1723
1724 if (isset($CFG->updateminmaturity)) {
1725 $minmaturity = $CFG->updateminmaturity;
1726 } else {
1727 // this can happen during the very first upgrade to 2.3
1728 $minmaturity = MATURITY_STABLE;
1729 }
1730
1731 $this->availableupdates = $provider->get_update_info($this->component,
1732 array('minmaturity' => $minmaturity));
dd119e21
DM
1733 }
1734
d26f3ddd 1735 /**
7d8de6d8 1736 * If there are updates for this plugin available, returns them.
d26f3ddd 1737 *
7d8de6d8
DM
1738 * Returns array of {@link available_update_info} objects, if some update
1739 * is available. Returns null if there is no update available or if the update
1740 * availability is unknown.
d26f3ddd 1741 *
7d8de6d8 1742 * @return array|null
d26f3ddd 1743 */
7d8de6d8 1744 public function available_updates() {
dd119e21 1745
7d8de6d8 1746 if (empty($this->availableupdates) or !is_array($this->availableupdates)) {
dd119e21
DM
1747 return null;
1748 }
1749
7d8de6d8
DM
1750 $updates = array();
1751
1752 foreach ($this->availableupdates as $availableupdate) {
1753 if ($availableupdate->version > $this->versiondisk) {
1754 $updates[] = $availableupdate;
1755 }
1756 }
1757
1758 if (empty($updates)) {
1759 return null;
dd119e21
DM
1760 }
1761
7d8de6d8 1762 return $updates;
d26f3ddd
DM
1763 }
1764
5cdb1893
MG
1765 /**
1766 * Returns the node name used in admin settings menu for this plugin settings (if applicable)
1767 *
1768 * @return null|string node name or null if plugin does not create settings node (default)
1769 */
1770 public function get_settings_section_name() {
1771 return null;
1772 }
1773
b9934a17 1774 /**
b6ad8594
DM
1775 * Returns the URL of the plugin settings screen
1776 *
1777 * Null value means that the plugin either does not have the settings screen
1778 * or its location is not available via this library.
1779 *
1780 * @return null|moodle_url
b9934a17
DM
1781 */
1782 public function get_settings_url() {
5cdb1893
MG
1783 $section = $this->get_settings_section_name();
1784 if ($section === null) {
1785 return null;
1786 }
1787 $settings = admin_get_root()->locate($section);
1788 if ($settings && $settings instanceof admin_settingpage) {
1789 return new moodle_url('/admin/settings.php', array('section' => $section));
1790 } else if ($settings && $settings instanceof admin_externalpage) {
1791 return new moodle_url($settings->url);
1792 } else {
1793 return null;
1794 }
1795 }
1796
1797 /**
1798 * Loads plugin settings to the settings tree
1799 *
1800 * This function usually includes settings.php file in plugins folder.
1801 * Alternatively it can create a link to some settings page (instance of admin_externalpage)
1802 *
1803 * @param part_of_admin_tree $adminroot
1804 * @param string $parentnodename
1805 * @param bool $hassiteconfig whether the current user has moodle/site:config capability
1806 */
1807 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
b9934a17
DM
1808 }
1809
1810 /**
b6ad8594
DM
1811 * Returns the URL of the screen where this plugin can be uninstalled
1812 *
1813 * Visiting that URL must be safe, that is a manual confirmation is needed
1814 * for actual uninstallation of the plugin. Null value means that the
1815 * plugin either does not support uninstallation, or does not require any
1816 * database cleanup or the location of the screen is not available via this
1817 * library.
1818 *
1819 * @return null|moodle_url
b9934a17
DM
1820 */
1821 public function get_uninstall_url() {
1822 return null;
1823 }
1824
1825 /**
b6ad8594
DM
1826 * Returns relative directory of the plugin with heading '/'
1827 *
1828 * @return string
b9934a17
DM
1829 */
1830 public function get_dir() {
1831 global $CFG;
1832
1833 return substr($this->rootdir, strlen($CFG->dirroot));
1834 }
1835
1836 /**
1837 * Provides access to plugin versions from {config_plugins}
1838 *
1839 * @param string $plugin plugin name
1840 * @param double $disablecache optional, defaults to false
1841 * @return int|false the stored value or false if not found
1842 */
1843 protected function get_version_from_config_plugins($plugin, $disablecache=false) {
1844 global $DB;
1845 static $pluginversions = null;
1846
1847 if (is_null($pluginversions) or $disablecache) {
f433088d
PS
1848 try {
1849 $pluginversions = $DB->get_records_menu('config_plugins', array('name' => 'version'), 'plugin', 'plugin,value');
1850 } catch (dml_exception $e) {
1851 // before install
1852 $pluginversions = array();
1853 }
b9934a17
DM
1854 }
1855
1856 if (!array_key_exists($plugin, $pluginversions)) {
1857 return false;
1858 }
1859
1860 return $pluginversions[$plugin];
1861 }
1862}
1863
b6ad8594 1864
b9934a17
DM
1865/**
1866 * General class for all plugin types that do not have their own class
1867 */
b6ad8594 1868class plugininfo_general extends plugininfo_base {
b9934a17
DM
1869}
1870
b6ad8594 1871
b9934a17
DM
1872/**
1873 * Class for page side blocks
1874 */
b6ad8594 1875class plugininfo_block extends plugininfo_base {
b9934a17 1876
b9934a17
DM
1877 public static function get_plugins($type, $typerootdir, $typeclass) {
1878
1879 // get the information about blocks at the disk
1880 $blocks = parent::get_plugins($type, $typerootdir, $typeclass);
1881
1882 // add blocks missing from disk
1883 $blocksinfo = self::get_blocks_info();
1884 foreach ($blocksinfo as $blockname => $blockinfo) {
1885 if (isset($blocks[$blockname])) {
1886 continue;
1887 }
1888 $plugin = new $typeclass();
1889 $plugin->type = $type;
1890 $plugin->typerootdir = $typerootdir;
1891 $plugin->name = $blockname;
1892 $plugin->rootdir = null;
1893 $plugin->displayname = $blockname;
1894 $plugin->versiondb = $blockinfo->version;
b8343e68 1895 $plugin->init_is_standard();
b9934a17
DM
1896
1897 $blocks[$blockname] = $plugin;
1898 }
1899
1900 return $blocks;
1901 }
1902
b8343e68 1903 public function init_display_name() {
b9934a17
DM
1904
1905 if (get_string_manager()->string_exists('pluginname', 'block_' . $this->name)) {
1906 $this->displayname = get_string('pluginname', 'block_' . $this->name);
1907
1908 } else if (($block = block_instance($this->name)) !== false) {
1909 $this->displayname = $block->get_title();
1910
1911 } else {
b8343e68 1912 parent::init_display_name();
b9934a17
DM
1913 }
1914 }
1915
b8343e68 1916 public function load_db_version() {
b9934a17
DM
1917 global $DB;
1918
1919 $blocksinfo = self::get_blocks_info();
1920 if (isset($blocksinfo[$this->name]->version)) {
1921 $this->versiondb = $blocksinfo[$this->name]->version;
1922 }
1923 }
1924
b9934a17
DM
1925 public function is_enabled() {
1926
1927 $blocksinfo = self::get_blocks_info();
1928 if (isset($blocksinfo[$this->name]->visible)) {
1929 if ($blocksinfo[$this->name]->visible) {
1930 return true;
1931 } else {
1932 return false;
1933 }
1934 } else {
1935 return parent::is_enabled();
1936 }
1937 }
1938
b9934a17
DM
1939 public function get_settings_url() {
1940
1941 if (($block = block_instance($this->name)) === false) {
1942 return parent::get_settings_url();
1943
1944 } else if ($block->has_config()) {
6740c605 1945 if (file_exists($this->full_path('settings.php'))) {
b9934a17
DM
1946 return new moodle_url('/admin/settings.php', array('section' => 'blocksetting' . $this->name));
1947 } else {
1948 $blocksinfo = self::get_blocks_info();
1949 return new moodle_url('/admin/block.php', array('block' => $blocksinfo[$this->name]->id));
1950 }
1951
1952 } else {
1953 return parent::get_settings_url();
1954 }
1955 }
1956
b9934a17
DM
1957 public function get_uninstall_url() {
1958
1959 $blocksinfo = self::get_blocks_info();
1960 return new moodle_url('/admin/blocks.php', array('delete' => $blocksinfo[$this->name]->id, 'sesskey' => sesskey()));
1961 }
1962
1963 /**
1964 * Provides access to the records in {block} table
1965 *
1966 * @param bool $disablecache do not use internal static cache
1967 * @return array array of stdClasses
1968 */
1969 protected static function get_blocks_info($disablecache=false) {
1970 global $DB;
1971 static $blocksinfocache = null;
1972
1973 if (is_null($blocksinfocache) or $disablecache) {
f433088d
PS
1974 try {
1975 $blocksinfocache = $DB->get_records('block', null, 'name', 'name,id,version,visible');
1976 } catch (dml_exception $e) {
1977 // before install
1978 $blocksinfocache = array();
1979 }
b9934a17
DM
1980 }
1981
1982 return $blocksinfocache;
1983 }
1984}
1985
b6ad8594 1986
b9934a17
DM
1987/**
1988 * Class for text filters
1989 */
b6ad8594 1990class plugininfo_filter extends plugininfo_base {
b9934a17 1991
b9934a17 1992 public static function get_plugins($type, $typerootdir, $typeclass) {
7c9b837e 1993 global $CFG, $DB;
b9934a17
DM
1994
1995 $filters = array();
1996
1997 // get the list of filters from both /filter and /mod location
1998 $installed = filter_get_all_installed();
1999
2000 foreach ($installed as $filterlegacyname => $displayname) {
2001 $plugin = new $typeclass();
2002 $plugin->type = $type;
2003 $plugin->typerootdir = $typerootdir;
2004 $plugin->name = self::normalize_legacy_name($filterlegacyname);
2005 $plugin->rootdir = $CFG->dirroot . '/' . $filterlegacyname;
2006 $plugin->displayname = $displayname;
2007
b8343e68
TH
2008 $plugin->load_disk_version();
2009 $plugin->load_db_version();
2010 $plugin->load_required_main_version();
2011 $plugin->init_is_standard();
b9934a17
DM
2012
2013 $filters[$plugin->name] = $plugin;
2014 }
2015
b9934a17 2016 $globalstates = self::get_global_states();
7c9b837e
DM
2017
2018 if ($DB->get_manager()->table_exists('filter_active')) {
2019 // if we're upgrading from 1.9, the table does not exist yet
2020 // if it does, make sure that all installed filters are registered
2021 $needsreload = false;
2022 foreach (array_keys($installed) as $filterlegacyname) {
2023 if (!isset($globalstates[self::normalize_legacy_name($filterlegacyname)])) {
2024 filter_set_global_state($filterlegacyname, TEXTFILTER_DISABLED);
2025 $needsreload = true;
2026 }
2027 }
2028 if ($needsreload) {
2029 $globalstates = self::get_global_states(true);
b9934a17 2030 }
b9934a17
DM
2031 }
2032
2033 // make sure that all registered filters are installed, just in case
2034 foreach ($globalstates as $name => $info) {
2035 if (!isset($filters[$name])) {
2036 // oops, there is a record in filter_active but the filter is not installed
2037 $plugin = new $typeclass();
2038 $plugin->type = $type;
2039 $plugin->typerootdir = $typerootdir;
2040 $plugin->name = $name;
2041 $plugin->rootdir = $CFG->dirroot . '/' . $info->legacyname;
2042 $plugin->displayname = $info->legacyname;
2043
b8343e68 2044 $plugin->load_db_version();
b9934a17
DM
2045
2046 if (is_null($plugin->versiondb)) {
2047 // this is a hack to stimulate 'Missing from disk' error
2048 // because $plugin->versiondisk will be null !== false
2049 $plugin->versiondb = false;
2050 }
2051
2052 $filters[$plugin->name] = $plugin;
2053 }
2054 }
2055
2056 return $filters;
2057 }
2058
b8343e68 2059 public function init_display_name() {
b9934a17
DM
2060 // do nothing, the name is set in self::get_plugins()
2061 }
2062
2063 /**
b6ad8594 2064 * @see load_version_php()
b9934a17 2065 */
473289a0 2066 protected function load_version_php() {
b9934a17 2067 if (strpos($this->name, 'mod_') === 0) {
473289a0
TH
2068 // filters bundled with modules do not have a version.php and so
2069 // do not provide their own versioning information.
2070 return new stdClass();
b9934a17 2071 }
473289a0 2072 return parent::load_version_php();
b9934a17
DM
2073 }
2074
b9934a17
DM
2075 public function is_enabled() {
2076
2077 $globalstates = self::get_global_states();
2078
2079 foreach ($globalstates as $filterlegacyname => $info) {
2080 $name = self::normalize_legacy_name($filterlegacyname);
2081 if ($name === $this->name) {
2082 if ($info->active == TEXTFILTER_DISABLED) {
2083 return false;
2084 } else {
2085 // it may be 'On' or 'Off, but available'
2086 return null;
2087 }
2088 }
2089 }
2090
2091 return null;
2092 }
2093
b9934a17
DM
2094 public function get_settings_url() {
2095
2096 $globalstates = self::get_global_states();
2097 $legacyname = $globalstates[$this->name]->legacyname;
2098 if (filter_has_global_settings($legacyname)) {
2099 return new moodle_url('/admin/settings.php', array('section' => 'filtersetting' . str_replace('/', '', $legacyname)));
2100 } else {
2101 return null;
2102 }
2103 }
2104
b9934a17
DM
2105 public function get_uninstall_url() {
2106
2107 if (strpos($this->name, 'mod_') === 0) {
2108 return null;
2109 } else {
2110 $globalstates = self::get_global_states();
2111 $legacyname = $globalstates[$this->name]->legacyname;
2112 return new moodle_url('/admin/filters.php', array('sesskey' => sesskey(), 'filterpath' => $legacyname, 'action' => 'delete'));
2113 }
2114 }
2115
2116 /**
2117 * Convert legacy filter names like 'filter/foo' or 'mod/bar' into frankenstyle
2118 *
2119 * @param string $legacyfiltername legacy filter name
2120 * @return string frankenstyle-like name
2121 */
2122 protected static function normalize_legacy_name($legacyfiltername) {
2123
2124 $name = str_replace('/', '_', $legacyfiltername);
2125 if (strpos($name, 'filter_') === 0) {
2126 $name = substr($name, 7);
2127 if (empty($name)) {
2128 throw new coding_exception('Unable to determine filter name: ' . $legacyfiltername);
2129 }
2130 }
2131
2132 return $name;
2133 }
2134
2135 /**
2136 * Provides access to the results of {@link filter_get_global_states()}
2137 * but indexed by the normalized filter name
2138 *
2139 * The legacy filter name is available as ->legacyname property.
2140 *
2141 * @param bool $disablecache
2142 * @return array
2143 */
2144 protected static function get_global_states($disablecache=false) {
2145 global $DB;
2146 static $globalstatescache = null;
2147
2148 if ($disablecache or is_null($globalstatescache)) {
2149
2150 if (!$DB->get_manager()->table_exists('filter_active')) {
2151 // we're upgrading from 1.9 and the table used by {@link filter_get_global_states()}
2152 // does not exist yet
2153 $globalstatescache = array();
2154
2155 } else {
2156 foreach (filter_get_global_states() as $legacyname => $info) {
2157 $name = self::normalize_legacy_name($legacyname);
2158 $filterinfo = new stdClass();
2159 $filterinfo->legacyname = $legacyname;
2160 $filterinfo->active = $info->active;
2161 $filterinfo->sortorder = $info->sortorder;
2162 $globalstatescache[$name] = $filterinfo;
2163 }
2164 }
2165 }
2166
2167 return $globalstatescache;
2168 }
2169}
2170
b6ad8594 2171
b9934a17
DM
2172/**
2173 * Class for activity modules
2174 */
b6ad8594 2175class plugininfo_mod extends plugininfo_base {
b9934a17 2176
b9934a17
DM
2177 public static function get_plugins($type, $typerootdir, $typeclass) {
2178
2179 // get the information about plugins at the disk
2180 $modules = parent::get_plugins($type, $typerootdir, $typeclass);
2181
2182 // add modules missing from disk
2183 $modulesinfo = self::get_modules_info();
2184 foreach ($modulesinfo as $modulename => $moduleinfo) {
2185 if (isset($modules[$modulename])) {
2186 continue;
2187 }
2188 $plugin = new $typeclass();
2189 $plugin->type = $type;
2190 $plugin->typerootdir = $typerootdir;
2191 $plugin->name = $modulename;
2192 $plugin->rootdir = null;
2193 $plugin->displayname = $modulename;
2194 $plugin->versiondb = $moduleinfo->version;
b8343e68 2195 $plugin->init_is_standard();
b9934a17
DM
2196
2197 $modules[$modulename] = $plugin;
2198 }
2199
2200 return $modules;
2201 }
2202
fde6f79f
MG
2203 /**
2204 * Magic method getter, redirects to read only values.
2205 *
2206 * For module plugins we pretend the object has 'visible' property for compatibility
2207 * with plugins developed for Moodle version below 2.4
2208 *
2209 * @param string $name
2210 * @return mixed
2211 */
2212 public function __get($name) {
2213 if ($name === 'visible') {
2214 debugging('This is now an instance of plugininfo_mod, please use $module->is_enabled() instead of $module->visible', DEBUG_DEVELOPER);
2215 return ($this->is_enabled() !== false);
2216 }
2217 return parent::__get($name);
2218 }
2219
b8343e68 2220 public function init_display_name() {
828788f0
TH
2221 if (get_string_manager()->string_exists('pluginname', $this->component)) {
2222 $this->displayname = get_string('pluginname', $this->component);
b9934a17 2223 } else {
828788f0 2224 $this->displayname = get_string('modulename', $this->component);
b9934a17
DM
2225 }
2226 }
2227
2228 /**
473289a0
TH
2229 * Load the data from version.php.
2230 * @return object the data object defined in version.php.
b9934a17 2231 */
473289a0
TH
2232 protected function load_version_php() {
2233 $versionfile = $this->full_path('version.php');
b9934a17 2234
473289a0 2235 $module = new stdClass();
b9934a17
DM
2236 if (is_readable($versionfile)) {
2237 include($versionfile);
b9934a17 2238 }
473289a0 2239 return $module;
b9934a17
DM
2240 }
2241
b8343e68 2242 public function load_db_version() {
b9934a17
DM
2243 global $DB;
2244
2245 $modulesinfo = self::get_modules_info();
2246 if (isset($modulesinfo[$this->name]->version)) {
2247 $this->versiondb = $modulesinfo[$this->name]->version;
2248 }
2249 }
2250
b9934a17
DM
2251 public function is_enabled() {
2252
2253 $modulesinfo = self::get_modules_info();
2254 if (isset($modulesinfo[$this->name]->visible)) {
2255 if ($modulesinfo[$this->name]->visible) {
2256 return true;
2257 } else {
2258 return false;
2259 }
2260 } else {
2261 return parent::is_enabled();
2262 }
2263 }
2264
fde6f79f
MG
2265 public function get_settings_section_name() {
2266 return 'modsetting' . $this->name;
2267 }
b9934a17 2268
fde6f79f
MG
2269 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
2270 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
2271 $ADMIN = $adminroot; // may be used in settings.php
2272 $module = $this; // also can be used inside settings.php
2273 $section = $this->get_settings_section_name();
2274
2275 $settings = null;
2276 if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
2277 $settings = new admin_settingpage($section, $this->displayname,
2278 'moodle/site:config', $this->is_enabled() === false);
2279 include($this->full_path('settings.php')); // this may also set $settings to null
2280 }
2281 if ($settings) {
2282 $ADMIN->add($parentnodename, $settings);
b9934a17
DM
2283 }
2284 }
2285
b9934a17
DM
2286 public function get_uninstall_url() {
2287
2288 if ($this->name !== 'forum') {
2289 return new moodle_url('/admin/modules.php', array('delete' => $this->name, 'sesskey' => sesskey()));
2290 } else {
2291 return null;
2292 }
2293 }
2294
2295 /**
2296 * Provides access to the records in {modules} table
2297 *
2298 * @param bool $disablecache do not use internal static cache
2299 * @return array array of stdClasses
2300 */
2301 protected static function get_modules_info($disablecache=false) {
2302 global $DB;
2303 static $modulesinfocache = null;
2304
2305 if (is_null($modulesinfocache) or $disablecache) {
f433088d
PS
2306 try {
2307 $modulesinfocache = $DB->get_records('modules', null, 'name', 'name,id,version,visible');
2308 } catch (dml_exception $e) {
2309 // before install
2310 $modulesinfocache = array();
2311 }
b9934a17
DM
2312 }
2313
2314 return $modulesinfocache;
2315 }
2316}
2317
0242bdc7
TH
2318
2319/**
2320 * Class for question behaviours.
2321 */
b6ad8594
DM
2322class plugininfo_qbehaviour extends plugininfo_base {
2323
828788f0
TH
2324 public function get_uninstall_url() {
2325 return new moodle_url('/admin/qbehaviours.php',
2326 array('delete' => $this->name, 'sesskey' => sesskey()));
2327 }
0242bdc7
TH
2328}
2329
2330
b9934a17
DM
2331/**
2332 * Class for question types
2333 */
b6ad8594
DM
2334class plugininfo_qtype extends plugininfo_base {
2335
828788f0
TH
2336 public function get_uninstall_url() {
2337 return new moodle_url('/admin/qtypes.php',
2338 array('delete' => $this->name, 'sesskey' => sesskey()));
2339 }
b9934a17
DM
2340}
2341
b9934a17
DM
2342
2343/**
2344 * Class for authentication plugins
2345 */
b6ad8594 2346class plugininfo_auth extends plugininfo_base {
b9934a17 2347
b9934a17
DM
2348 public function is_enabled() {
2349 global $CFG;
2350 /** @var null|array list of enabled authentication plugins */
2351 static $enabled = null;
2352
2353 if (in_array($this->name, array('nologin', 'manual'))) {
2354 // these two are always enabled and can't be disabled
2355 return null;
2356 }
2357
2358 if (is_null($enabled)) {
d5d181f5 2359 $enabled = array_flip(explode(',', $CFG->auth));
b9934a17
DM
2360 }
2361
2362 return isset($enabled[$this->name]);
2363 }
2364
b9934a17 2365 public function get_settings_url() {
6740c605 2366 if (file_exists($this->full_path('settings.php'))) {
b9934a17
DM
2367 return new moodle_url('/admin/settings.php', array('section' => 'authsetting' . $this->name));
2368 } else {
2369 return new moodle_url('/admin/auth_config.php', array('auth' => $this->name));
2370 }
2371 }
2372}
2373
b6ad8594 2374
b9934a17
DM
2375/**
2376 * Class for enrolment plugins
2377 */
b6ad8594 2378class plugininfo_enrol extends plugininfo_base {
b9934a17 2379
b9934a17
DM
2380 public function is_enabled() {
2381 global $CFG;
2382 /** @var null|array list of enabled enrolment plugins */
2383 static $enabled = null;
2384
b6ad8594
DM
2385 // We do not actually need whole enrolment classes here so we do not call
2386 // {@link enrol_get_plugins()}. Note that this may produce slightly different
2387 // results, for example if the enrolment plugin does not contain lib.php
2388 // but it is listed in $CFG->enrol_plugins_enabled
2389
b9934a17 2390 if (is_null($enabled)) {
d5d181f5 2391 $enabled = array_flip(explode(',', $CFG->enrol_plugins_enabled));
b9934a17
DM
2392 }
2393
2394 return isset($enabled[$this->name]);
2395 }
2396
b9934a17
DM
2397 public function get_settings_url() {
2398
6740c605 2399 if ($this->is_enabled() or file_exists($this->full_path('settings.php'))) {
b9934a17
DM
2400 return new moodle_url('/admin/settings.php', array('section' => 'enrolsettings' . $this->name));
2401 } else {
2402 return parent::get_settings_url();
2403 }
2404 }
2405
b9934a17
DM
2406 public function get_uninstall_url() {
2407 return new moodle_url('/admin/enrol.php', array('action' => 'uninstall', 'enrol' => $this->name, 'sesskey' => sesskey()));
2408 }
2409}
2410
b6ad8594 2411
b9934a17
DM
2412/**
2413 * Class for messaging processors
2414 */
b6ad8594 2415class plugininfo_message extends plugininfo_base {
b9934a17 2416
b9934a17 2417 public function get_settings_url() {
bc795b98
RK
2418 $processors = get_message_processors();
2419 if (isset($processors[$this->name])) {
2420 $processor = $processors[$this->name];
2421 if ($processor->available && $processor->hassettings) {
2422 return new moodle_url('settings.php', array('section' => 'messagesetting'.$processor->name));
2423 }
0210ce10 2424 }
bc795b98 2425 return parent::get_settings_url();
b9934a17 2426 }
b9934a17 2427
bede23f7
RK
2428 /**
2429 * @see plugintype_interface::is_enabled()
2430 */
2431 public function is_enabled() {
2432 $processors = get_message_processors();
2433 if (isset($processors[$this->name])) {
2434 return $processors[$this->name]->configured && $processors[$this->name]->enabled;
0210ce10 2435 } else {
bede23f7
RK
2436 return parent::is_enabled();
2437 }
2438 }
3f9d9e28
RK
2439
2440 /**
2441 * @see plugintype_interface::get_uninstall_url()
2442 */
2443 public function get_uninstall_url() {
2444 $processors = get_message_processors();
2445 if (isset($processors[$this->name])) {
2446 return new moodle_url('message.php', array('uninstall' => $processors[$this->name]->id, 'sesskey' => sesskey()));
2447 } else {
2448 return parent::get_uninstall_url();
0210ce10 2449 }
b9934a17
DM
2450 }
2451}
2452
b6ad8594 2453
b9934a17
DM
2454/**
2455 * Class for repositories
2456 */
b6ad8594 2457class plugininfo_repository extends plugininfo_base {
b9934a17 2458
b9934a17
DM
2459 public function is_enabled() {
2460
2461 $enabled = self::get_enabled_repositories();
2462
2463 return isset($enabled[$this->name]);
2464 }
2465
b9934a17
DM
2466 public function get_settings_url() {
2467
2468 if ($this->is_enabled()) {
2469 return new moodle_url('/admin/repository.php', array('sesskey' => sesskey(), 'action' => 'edit', 'repos' => $this->name));
2470 } else {
2471 return parent::get_settings_url();
2472 }
2473 }
2474
2475 /**
2476 * Provides access to the records in {repository} table
2477 *
2478 * @param bool $disablecache do not use internal static cache
2479 * @return array array of stdClasses
2480 */
2481 protected static function get_enabled_repositories($disablecache=false) {
2482 global $DB;
2483 static $repositories = null;
2484
2485 if (is_null($repositories) or $disablecache) {
2486 $repositories = $DB->get_records('repository', null, 'type', 'type,visible,sortorder');
2487 }
2488
2489 return $repositories;
2490 }
2491}
2492
b6ad8594 2493
b9934a17
DM
2494/**
2495 * Class for portfolios
2496 */
b6ad8594 2497class plugininfo_portfolio extends plugininfo_base {
b9934a17 2498
b9934a17
DM
2499 public function is_enabled() {
2500
2501 $enabled = self::get_enabled_portfolios();
2502
2503 return isset($enabled[$this->name]);
2504 }
2505
2506 /**
2507 * Provides access to the records in {portfolio_instance} table
2508 *
2509 * @param bool $disablecache do not use internal static cache
2510 * @return array array of stdClasses
2511 */
2512 protected static function get_enabled_portfolios($disablecache=false) {
2513 global $DB;
2514 static $portfolios = null;
2515
2516 if (is_null($portfolios) or $disablecache) {
2517 $portfolios = array();
2518 $instances = $DB->get_recordset('portfolio_instance', null, 'plugin');
2519 foreach ($instances as $instance) {
2520 if (isset($portfolios[$instance->plugin])) {
2521 if ($instance->visible) {
2522 $portfolios[$instance->plugin]->visible = $instance->visible;
2523 }
2524 } else {
2525 $portfolios[$instance->plugin] = $instance;
2526 }
2527 }
2528 }
2529
2530 return $portfolios;
2531 }
2532}
2533
b6ad8594 2534
b9934a17
DM
2535/**
2536 * Class for themes
2537 */
b6ad8594 2538class plugininfo_theme extends plugininfo_base {
b9934a17 2539
b9934a17
DM
2540 public function is_enabled() {
2541 global $CFG;
2542
2543 if ((!empty($CFG->theme) and $CFG->theme === $this->name) or
2544 (!empty($CFG->themelegacy) and $CFG->themelegacy === $this->name)) {
2545 return true;
2546 } else {
2547 return parent::is_enabled();
2548 }
2549 }
2550}
2551
b6ad8594 2552
b9934a17
DM
2553/**
2554 * Class representing an MNet service
2555 */
b6ad8594 2556class plugininfo_mnetservice extends plugininfo_base {
b9934a17 2557
b9934a17
DM
2558 public function is_enabled() {
2559 global $CFG;
2560
2561 if (empty($CFG->mnet_dispatcher_mode) || $CFG->mnet_dispatcher_mode !== 'strict') {
2562 return false;
2563 } else {
2564 return parent::is_enabled();
2565 }
2566 }
2567}
3cdfaeef 2568
b6ad8594 2569
3cdfaeef
PS
2570/**
2571 * Class for admin tool plugins
2572 */
b6ad8594 2573class plugininfo_tool extends plugininfo_base {
3cdfaeef
PS
2574
2575 public function get_uninstall_url() {
2576 return new moodle_url('/admin/tools.php', array('delete' => $this->name, 'sesskey' => sesskey()));
2577 }
2578}
4f6bba20 2579
b6ad8594 2580
4f6bba20
PS
2581/**
2582 * Class for admin tool plugins
2583 */
b6ad8594 2584class plugininfo_report extends plugininfo_base {
4f6bba20
PS
2585
2586 public function get_uninstall_url() {
2587 return new moodle_url('/admin/reports.php', array('delete' => $this->name, 'sesskey' => sesskey()));
2588 }
2589}
888ce02a
RK
2590
2591
2592/**
2593 * Class for local plugins
2594 */
2595class plugininfo_local extends plugininfo_base {
2596
2597 public function get_uninstall_url() {
2598 return new moodle_url('/admin/localplugins.php', array('delete' => $this->name, 'sesskey' => sesskey()));
2599 }
2600
2601 public function get_settings_url() {
2602 if (file_exists($this->full_path('settings.php'))) {
2603 return new moodle_url('/admin/settings.php', array('section' => 'local_' . $this->name));
2604 } else {
2605 return parent::get_settings_url();
2606 }
2607 }
2608}