MDL-35109 Fix available_update_checker::cron_has_fresh_fetch()
[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(
381 'comments', 'file'
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 *
7d8de6d8 864 * This implementation uses the config_plugins table as the permanent storage.
b9934a17 865 *
7d8de6d8 866 * @param bool $forcereload reload even if it was already loaded
b9934a17 867 */
7d8de6d8
DM
868 protected function restore_response($forcereload = false) {
869
870 if (!$forcereload and !is_null($this->recentresponse)) {
871 // we already have it, nothing to do
872 return;
cd0bb55f
DM
873 }
874
7d8de6d8
DM
875 $config = get_config('core_plugin');
876
877 if (!empty($config->recentresponse) and !empty($config->recentfetch)) {
878 try {
879 $this->validate_response($config->recentresponse);
880 $this->recentfetch = $config->recentfetch;
881 $this->recentresponse = $this->decode_response($config->recentresponse);
660c4d46 882 } catch (available_update_checker_exception $e) {
7d8de6d8
DM
883 // do not set recentresponse if the validation fails
884 }
885
cd0bb55f 886 } else {
7d8de6d8 887 $this->recentresponse = array();
cd0bb55f
DM
888 }
889 }
890
7b35553b
DM
891 /**
892 * Compares two raw {@link $recentresponse} records and returns the list of changed updates
893 *
894 * This method is used to populate potential update info to be sent to site admins.
895 *
19d11b3b
DM
896 * @param array $old
897 * @param array $new
7b35553b
DM
898 * @throws available_update_checker_exception
899 * @return array parts of $new['updates'] that have changed
900 */
19d11b3b 901 protected function compare_responses(array $old, array $new) {
7b35553b 902
19d11b3b 903 if (empty($new)) {
7b35553b
DM
904 return array();
905 }
906
907 if (!array_key_exists('updates', $new)) {
908 throw new available_update_checker_exception('err_response_format');
909 }
910
19d11b3b 911 if (empty($old)) {
7b35553b
DM
912 return $new['updates'];
913 }
914
915 if (!array_key_exists('updates', $old)) {
916 throw new available_update_checker_exception('err_response_format');
917 }
918
919 $changes = array();
920
921 foreach ($new['updates'] as $newcomponent => $newcomponentupdates) {
922 if (empty($old['updates'][$newcomponent])) {
923 $changes[$newcomponent] = $newcomponentupdates;
924 continue;
925 }
926 foreach ($newcomponentupdates as $newcomponentupdate) {
927 $inold = false;
928 foreach ($old['updates'][$newcomponent] as $oldcomponentupdate) {
929 if ($newcomponentupdate['version'] == $oldcomponentupdate['version']) {
930 $inold = true;
931 }
932 }
933 if (!$inold) {
934 if (!isset($changes[$newcomponent])) {
935 $changes[$newcomponent] = array();
936 }
937 $changes[$newcomponent][] = $newcomponentupdate;
938 }
939 }
940 }
941
942 return $changes;
943 }
944
cd0bb55f
DM
945 /**
946 * Returns the URL to send update requests to
947 *
948 * During the development or testing, you can set $CFG->alternativeupdateproviderurl
949 * to a custom URL that will be used. Otherwise the standard URL will be returned.
950 *
951 * @return string URL
952 */
953 protected function prepare_request_url() {
954 global $CFG;
955
956 if (!empty($CFG->alternativeupdateproviderurl)) {
957 return $CFG->alternativeupdateproviderurl;
958 } else {
959 return 'http://download.moodle.org/api/1.0/updates.php';
960 }
961 }
962
55585f3a 963 /**
4442cc80 964 * Sets the properties currentversion, currentrelease, currentbranch and currentplugins
55585f3a
DM
965 *
966 * @param bool $forcereload
967 */
968 protected function load_current_environment($forcereload=false) {
969 global $CFG;
970
971 if (!is_null($this->currentversion) and !$forcereload) {
972 // nothing to do
973 return;
974 }
975
975311d3
PS
976 $version = null;
977 $release = null;
978
55585f3a
DM
979 require($CFG->dirroot.'/version.php');
980 $this->currentversion = $version;
4442cc80 981 $this->currentrelease = $release;
55585f3a
DM
982 $this->currentbranch = moodle_major_version(true);
983
984 $pluginman = plugin_manager::instance();
985 foreach ($pluginman->get_plugins() as $type => $plugins) {
986 foreach ($plugins as $plugin) {
987 if (!$plugin->is_standard()) {
988 $this->currentplugins[$plugin->component] = $plugin->versiondisk;
989 }
990 }
991 }
992 }
993
cd0bb55f
DM
994 /**
995 * Returns the list of HTTP params to be sent to the updates provider URL
996 *
997 * @return array of (string)param => (string)value
998 */
999 protected function prepare_request_params() {
1000 global $CFG;
1001
55585f3a 1002 $this->load_current_environment();
7d8de6d8
DM
1003 $this->restore_response();
1004
cd0bb55f
DM
1005 $params = array();
1006 $params['format'] = 'json';
1007
7d8de6d8
DM
1008 if (isset($this->recentresponse['ticket'])) {
1009 $params['ticket'] = $this->recentresponse['ticket'];
cd0bb55f
DM
1010 }
1011
55585f3a
DM
1012 if (isset($this->currentversion)) {
1013 $params['version'] = $this->currentversion;
1014 } else {
1015 throw new coding_exception('Main Moodle version must be already known here');
cd0bb55f
DM
1016 }
1017
55585f3a
DM
1018 if (isset($this->currentbranch)) {
1019 $params['branch'] = $this->currentbranch;
1020 } else {
1021 throw new coding_exception('Moodle release must be already known here');
1022 }
1023
1024 $plugins = array();
1025 foreach ($this->currentplugins as $plugin => $version) {
1026 $plugins[] = $plugin.'@'.$version;
1027 }
1028 if (!empty($plugins)) {
1029 $params['plugins'] = implode(',', $plugins);
cd0bb55f
DM
1030 }
1031
cd0bb55f
DM
1032 return $params;
1033 }
be378880
DM
1034
1035 /**
1036 * Returns the current timestamp
1037 *
1038 * @return int the timestamp
1039 */
1040 protected function cron_current_timestamp() {
1041 return time();
1042 }
1043
1044 /**
1045 * Output cron debugging info
1046 *
1047 * @see mtrace()
1048 * @param string $msg output message
1049 * @param string $eol end of line
1050 */
1051 protected function cron_mtrace($msg, $eol = PHP_EOL) {
1052 mtrace($msg, $eol);
1053 }
1054
1055 /**
1056 * Decide if the autocheck feature is disabled in the server setting
1057 *
1058 * @return bool true if autocheck enabled, false if disabled
1059 */
1060 protected function cron_autocheck_enabled() {
718eb2a5
DM
1061 global $CFG;
1062
be378880
DM
1063 if (empty($CFG->updateautocheck)) {
1064 return false;
1065 } else {
1066 return true;
1067 }
1068 }
1069
1070 /**
1071 * Decide if the recently fetched data are still fresh enough
1072 *
1073 * @param int $now current timestamp
1074 * @return bool true if no need to re-fetch, false otherwise
1075 */
1076 protected function cron_has_fresh_fetch($now) {
1077 $recent = $this->get_last_timefetched();
1078
1079 if (empty($recent)) {
1080 return false;
1081 }
1082
1083 if ($now < $recent) {
1084 $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1085 return true;
1086 }
1087
7092ea5d 1088 if ($now - $recent > 24 * HOURSECS) {
be378880
DM
1089 return false;
1090 }
1091
1092 return true;
1093 }
1094
1095 /**
1096 * Decide if the fetch is outadated or even missing
1097 *
1098 * @param int $now current timestamp
1099 * @return bool false if no need to re-fetch, true otherwise
1100 */
1101 protected function cron_has_outdated_fetch($now) {
1102 $recent = $this->get_last_timefetched();
1103
1104 if (empty($recent)) {
1105 return true;
1106 }
1107
1108 if ($now < $recent) {
1109 $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1110 return false;
1111 }
1112
1113 if ($now - $recent > 48 * HOURSECS) {
1114 return true;
1115 }
1116
1117 return false;
1118 }
1119
1120 /**
1121 * Returns the cron execution offset for this site
1122 *
1123 * The main {@link self::cron()} is supposed to run every night in some random time
1124 * between 01:00 and 06:00 AM (local time). The exact moment is defined by so called
1125 * execution offset, that is the amount of time after 01:00 AM. The offset value is
1126 * initially generated randomly and then used consistently at the site. This way, the
1127 * regular checks against the download.moodle.org server are spread in time.
1128 *
1129 * @return int the offset number of seconds from range 1 sec to 5 hours
1130 */
1131 protected function cron_execution_offset() {
1132 global $CFG;
1133
1134 if (empty($CFG->updatecronoffset)) {
1135 set_config('updatecronoffset', rand(1, 5 * HOURSECS));
1136 }
1137
1138 return $CFG->updatecronoffset;
1139 }
1140
1141 /**
1142 * Fetch available updates info and eventually send notification to site admins
1143 */
1144 protected function cron_execute() {
7b35553b 1145
19d11b3b 1146 try {
fd87d0bf
AB
1147 $this->restore_response();
1148 $previous = $this->recentresponse;
1149 $this->fetch();
1150 $this->restore_response(true);
1151 $current = $this->recentresponse;
19d11b3b
DM
1152 $changes = $this->compare_responses($previous, $current);
1153 $notifications = $this->cron_notifications($changes);
1154 $this->cron_notify($notifications);
a77141a7 1155 $this->cron_mtrace('done');
19d11b3b
DM
1156 } catch (available_update_checker_exception $e) {
1157 $this->cron_mtrace('FAILED!');
1158 }
1159 }
1160
1161 /**
1162 * Given the list of changes in available updates, pick those to send to site admins
1163 *
1164 * @param array $changes as returned by {@link self::compare_responses()}
1165 * @return array of available_update_info objects to send to site admins
1166 */
1167 protected function cron_notifications(array $changes) {
1168 global $CFG;
1169
1170 $notifications = array();
1171 $pluginman = plugin_manager::instance();
1172 $plugins = $pluginman->get_plugins(true);
1173
1174 foreach ($changes as $component => $componentchanges) {
718eb2a5
DM
1175 if (empty($componentchanges)) {
1176 continue;
1177 }
19d11b3b
DM
1178 $componentupdates = $this->get_update_info($component,
1179 array('minmaturity' => $CFG->updateminmaturity, 'notifybuilds' => $CFG->updatenotifybuilds));
718eb2a5
DM
1180 if (empty($componentupdates)) {
1181 continue;
1182 }
19d11b3b
DM
1183 // notify only about those $componentchanges that are present in $componentupdates
1184 // to respect the preferences
1185 foreach ($componentchanges as $componentchange) {
1186 foreach ($componentupdates as $componentupdate) {
1187 if ($componentupdate->version == $componentchange['version']) {
1188 if ($component == 'core') {
1189 // in case of 'core' this is enough, we already know that the
1190 // $componentupdate is a real update with higher version
1191 $notifications[] = $componentupdate;
1192 } else {
1193 // use the plugin_manager to check if the reported $componentchange
1194 // is a real update with higher version. such a real update must be
1195 // present in the 'availableupdates' property of one of the component's
1196 // available_update_info object
1197 list($plugintype, $pluginname) = normalize_component($component);
1198 if (!empty($plugins[$plugintype][$pluginname]->availableupdates)) {
1199 foreach ($plugins[$plugintype][$pluginname]->availableupdates as $availableupdate) {
1200 if ($availableupdate->version == $componentchange['version']) {
1201 $notifications[] = $componentupdate;
1202 }
1203 }
1204 }
1205 }
1206 }
1207 }
1208 }
1209 }
1210
1211 return $notifications;
be378880 1212 }
a77141a7
DM
1213
1214 /**
1215 * Sends the given notifications to site admins via messaging API
1216 *
1217 * @param array $notifications array of available_update_info objects to send
1218 */
1219 protected function cron_notify(array $notifications) {
1220 global $CFG;
1221
1222 if (empty($notifications)) {
1223 return;
1224 }
1225
1226 $admins = get_admins();
1227
1228 if (empty($admins)) {
1229 return;
1230 }
1231
1232 $this->cron_mtrace('sending notifications ... ', '');
1233
1234 $text = get_string('updatenotifications', 'core_admin') . PHP_EOL;
1235 $html = html_writer::tag('h1', get_string('updatenotifications', 'core_admin')) . PHP_EOL;
1236
1237 $coreupdates = array();
1238 $pluginupdates = array();
1239
660c4d46 1240 foreach ($notifications as $notification) {
a77141a7
DM
1241 if ($notification->component == 'core') {
1242 $coreupdates[] = $notification;
1243 } else {
1244 $pluginupdates[] = $notification;
1245 }
1246 }
1247
1248 if (!empty($coreupdates)) {
1249 $text .= PHP_EOL . get_string('updateavailable', 'core_admin') . PHP_EOL;
1250 $html .= html_writer::tag('h2', get_string('updateavailable', 'core_admin')) . PHP_EOL;
1251 $html .= html_writer::start_tag('ul') . PHP_EOL;
1252 foreach ($coreupdates as $coreupdate) {
1253 $html .= html_writer::start_tag('li');
1254 if (isset($coreupdate->release)) {
1255 $text .= get_string('updateavailable_release', 'core_admin', $coreupdate->release);
1256 $html .= html_writer::tag('strong', get_string('updateavailable_release', 'core_admin', $coreupdate->release));
1257 }
1258 if (isset($coreupdate->version)) {
1259 $text .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
1260 $html .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
1261 }
1262 if (isset($coreupdate->maturity)) {
1263 $text .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
1264 $html .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
1265 }
1266 $text .= PHP_EOL;
1267 $html .= html_writer::end_tag('li') . PHP_EOL;
1268 }
1269 $text .= PHP_EOL;
1270 $html .= html_writer::end_tag('ul') . PHP_EOL;
1271
1272 $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/index.php');
1273 $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
1274 $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/index.php', $CFG->wwwroot.'/'.$CFG->admin.'/index.php'));
1275 $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
1276 }
1277
1278 if (!empty($pluginupdates)) {
1279 $text .= PHP_EOL . get_string('updateavailableforplugin', 'core_admin') . PHP_EOL;
1280 $html .= html_writer::tag('h2', get_string('updateavailableforplugin', 'core_admin')) . PHP_EOL;
1281
1282 $html .= html_writer::start_tag('ul') . PHP_EOL;
1283 foreach ($pluginupdates as $pluginupdate) {
1284 $html .= html_writer::start_tag('li');
1285 $text .= get_string('pluginname', $pluginupdate->component);
1286 $html .= html_writer::tag('strong', get_string('pluginname', $pluginupdate->component));
1287
1288 $text .= ' ('.$pluginupdate->component.')';
1289 $html .= ' ('.$pluginupdate->component.')';
1290
1291 $text .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
1292 $html .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
1293
1294 $text .= PHP_EOL;
1295 $html .= html_writer::end_tag('li') . PHP_EOL;
1296 }
1297 $text .= PHP_EOL;
1298 $html .= html_writer::end_tag('ul') . PHP_EOL;
b9934a17 1299
a77141a7
DM
1300 $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php');
1301 $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
1302 $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/plugins.php', $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php'));
1303 $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
1304 }
1305
1306 $a = array('siteurl' => $CFG->wwwroot);
1307 $text .= get_string('updatenotificationfooter', 'core_admin', $a) . PHP_EOL;
1308 $a = array('siteurl' => html_writer::link($CFG->wwwroot, $CFG->wwwroot));
1309 $html .= html_writer::tag('footer', html_writer::tag('p', get_string('updatenotificationfooter', 'core_admin', $a),
1310 array('style' => 'font-size:smaller; color:#333;')));
1311
1312 $mainadmin = reset($admins);
1313
1314 foreach ($admins as $admin) {
1315 $message = new stdClass();
1316 $message->component = 'moodle';
1317 $message->name = 'availableupdate';
1318 $message->userfrom = $mainadmin;
1319 $message->userto = $admin;
1320 $message->subject = get_string('updatenotifications', 'core_admin');
1321 $message->fullmessage = $text;
1322 $message->fullmessageformat = FORMAT_PLAIN;
1323 $message->fullmessagehtml = $html;
cd89994d
DM
1324 $message->smallmessage = get_string('updatenotifications', 'core_admin');
1325 $message->notification = 1;
a77141a7
DM
1326 message_send($message);
1327 }
1328 }
b9934a17
DM
1329
1330 /**
4442cc80 1331 * Compare two release labels and decide if they are the same
b9934a17 1332 *
4442cc80
DM
1333 * @param string $remote release info of the available update
1334 * @param null|string $local release info of the local code, defaults to $release defined in version.php
1335 * @return boolean true if the releases declare the same minor+major version
b9934a17 1336 */
4442cc80 1337 protected function is_same_release($remote, $local=null) {
b9934a17 1338
4442cc80
DM
1339 if (is_null($local)) {
1340 $this->load_current_environment();
1341 $local = $this->currentrelease;
1342 }
0242bdc7 1343
4442cc80 1344 $pattern = '/^([0-9\.\+]+)([^(]*)/';
b9934a17 1345
4442cc80
DM
1346 preg_match($pattern, $remote, $remotematches);
1347 preg_match($pattern, $local, $localmatches);
b9934a17 1348
4442cc80
DM
1349 $remotematches[1] = str_replace('+', '', $remotematches[1]);
1350 $localmatches[1] = str_replace('+', '', $localmatches[1]);
1351
1352 if ($remotematches[1] === $localmatches[1] and rtrim($remotematches[2]) === rtrim($localmatches[2])) {
1353 return true;
1354 } else {
1355 return false;
1356 }
1357 }
cd0bb55f
DM
1358}
1359
1360
7d8de6d8
DM
1361/**
1362 * Defines the structure of objects returned by {@link available_update_checker::get_update_info()}
1363 */
1364class available_update_info {
1365
1366 /** @var string frankenstyle component name */
1367 public $component;
1368 /** @var int the available version of the component */
1369 public $version;
1370 /** @var string|null optional release name */
1371 public $release = null;
1372 /** @var int|null optional maturity info, eg {@link MATURITY_STABLE} */
1373 public $maturity = null;
1374 /** @var string|null optional URL of a page with more info about the update */
1375 public $url = null;
1376 /** @var string|null optional URL of a ZIP package that can be downloaded and installed */
1377 public $download = null;
1378
1379 /**
1380 * Creates new instance of the class
b9934a17 1381 *
7d8de6d8
DM
1382 * The $info array must provide at least the 'version' value and optionally all other
1383 * values to populate the object's properties.
b9934a17 1384 *
7d8de6d8
DM
1385 * @param string $name the frankenstyle component name
1386 * @param array $info associative array with other properties
1387 */
1388 public function __construct($name, array $info) {
1389 $this->component = $name;
1390 foreach ($info as $k => $v) {
1391 if (property_exists('available_update_info', $k) and $k != 'component') {
1392 $this->$k = $v;
1393 }
1394 }
1395 }
1396}
1397
1398
00ef3c3e
DM
1399/**
1400 * Factory class producing required subclasses of {@link plugininfo_base}
1401 */
1402class plugininfo_default_factory {
b9934a17
DM
1403
1404 /**
00ef3c3e 1405 * Makes a new instance of the plugininfo class
b9934a17 1406 *
00ef3c3e
DM
1407 * @param string $type the plugin type, eg. 'mod'
1408 * @param string $typerootdir full path to the location of all the plugins of this type
1409 * @param string $name the plugin name, eg. 'workshop'
1410 * @param string $namerootdir full path to the location of the plugin
1411 * @param string $typeclass the name of class that holds the info about the plugin
1412 * @return plugininfo_base the instance of $typeclass
1413 */
1414 public static function make($type, $typerootdir, $name, $namerootdir, $typeclass) {
1415 $plugin = new $typeclass();
1416 $plugin->type = $type;
1417 $plugin->typerootdir = $typerootdir;
1418 $plugin->name = $name;
1419 $plugin->rootdir = $namerootdir;
1420
1421 $plugin->init_display_name();
1422 $plugin->load_disk_version();
1423 $plugin->load_db_version();
1424 $plugin->load_required_main_version();
1425 $plugin->init_is_standard();
473289a0 1426
00ef3c3e
DM
1427 return $plugin;
1428 }
b9934a17
DM
1429}
1430
00ef3c3e 1431
b9934a17 1432/**
b6ad8594 1433 * Base class providing access to the information about a plugin
828788f0
TH
1434 *
1435 * @property-read string component the component name, type_name
b9934a17 1436 */
b6ad8594 1437abstract class plugininfo_base {
b9934a17
DM
1438
1439 /** @var string the plugintype name, eg. mod, auth or workshopform */
1440 public $type;
1441 /** @var string full path to the location of all the plugins of this type */
1442 public $typerootdir;
1443 /** @var string the plugin name, eg. assignment, ldap */
1444 public $name;
1445 /** @var string the localized plugin name */
1446 public $displayname;
1447 /** @var string the plugin source, one of plugin_manager::PLUGIN_SOURCE_xxx constants */
1448 public $source;
1449 /** @var fullpath to the location of this plugin */
1450 public $rootdir;
1451 /** @var int|string the version of the plugin's source code */
1452 public $versiondisk;
1453 /** @var int|string the version of the installed plugin */
1454 public $versiondb;
1455 /** @var int|float|string required version of Moodle core */
1456 public $versionrequires;
b6ad8594
DM
1457 /** @var array other plugins that this one depends on, lazy-loaded by {@link get_other_required_plugins()} */
1458 public $dependencies;
b9934a17
DM
1459 /** @var int number of instances of the plugin - not supported yet */
1460 public $instances;
1461 /** @var int order of the plugin among other plugins of the same type - not supported yet */
1462 public $sortorder;
7d8de6d8
DM
1463 /** @var array|null array of {@link available_update_info} for this plugin */
1464 public $availableupdates;
b9934a17
DM
1465
1466 /**
b6ad8594
DM
1467 * Gathers and returns the information about all plugins of the given type
1468 *
b6ad8594
DM
1469 * @param string $type the name of the plugintype, eg. mod, auth or workshopform
1470 * @param string $typerootdir full path to the location of the plugin dir
1471 * @param string $typeclass the name of the actually called class
1472 * @return array of plugintype classes, indexed by the plugin name
b9934a17
DM
1473 */
1474 public static function get_plugins($type, $typerootdir, $typeclass) {
1475
1476 // get the information about plugins at the disk
1477 $plugins = get_plugin_list($type);
1478 $ondisk = array();
1479 foreach ($plugins as $pluginname => $pluginrootdir) {
00ef3c3e
DM
1480 $ondisk[$pluginname] = plugininfo_default_factory::make($type, $typerootdir,
1481 $pluginname, $pluginrootdir, $typeclass);
b9934a17
DM
1482 }
1483 return $ondisk;
1484 }
1485
1486 /**
b6ad8594 1487 * Sets {@link $displayname} property to a localized name of the plugin
b9934a17 1488 */
b8343e68 1489 public function init_display_name() {
828788f0
TH
1490 if (!get_string_manager()->string_exists('pluginname', $this->component)) {
1491 $this->displayname = '[pluginname,' . $this->component . ']';
b9934a17 1492 } else {
828788f0
TH
1493 $this->displayname = get_string('pluginname', $this->component);
1494 }
1495 }
1496
1497 /**
1498 * Magic method getter, redirects to read only values.
b6ad8594 1499 *
828788f0
TH
1500 * @param string $name
1501 * @return mixed
1502 */
1503 public function __get($name) {
1504 switch ($name) {
1505 case 'component': return $this->type . '_' . $this->name;
1506
1507 default:
1508 debugging('Invalid plugin property accessed! '.$name);
1509 return null;
b9934a17
DM
1510 }
1511 }
1512
1513 /**
b6ad8594
DM
1514 * Return the full path name of a file within the plugin.
1515 *
1516 * No check is made to see if the file exists.
1517 *
1518 * @param string $relativepath e.g. 'version.php'.
1519 * @return string e.g. $CFG->dirroot . '/mod/quiz/version.php'.
b9934a17 1520 */
473289a0 1521 public function full_path($relativepath) {
b9934a17 1522 if (empty($this->rootdir)) {
473289a0 1523 return '';
b9934a17 1524 }
473289a0
TH
1525 return $this->rootdir . '/' . $relativepath;
1526 }
b9934a17 1527
473289a0
TH
1528 /**
1529 * Load the data from version.php.
b6ad8594
DM
1530 *
1531 * @return stdClass the object called $plugin defined in version.php
473289a0
TH
1532 */
1533 protected function load_version_php() {
1534 $versionfile = $this->full_path('version.php');
b9934a17 1535
473289a0 1536 $plugin = new stdClass();
b9934a17
DM
1537 if (is_readable($versionfile)) {
1538 include($versionfile);
b9934a17 1539 }
473289a0 1540 return $plugin;
b9934a17
DM
1541 }
1542
1543 /**
b6ad8594
DM
1544 * Sets {@link $versiondisk} property to a numerical value representing the
1545 * version of the plugin's source code.
1546 *
1547 * If the value is null after calling this method, either the plugin
1548 * does not use versioning (typically does not have any database
1549 * data) or is missing from disk.
b9934a17 1550 */
473289a0
TH
1551 public function load_disk_version() {
1552 $plugin = $this->load_version_php();
1553 if (isset($plugin->version)) {
1554 $this->versiondisk = $plugin->version;
b9934a17
DM
1555 }
1556 }
1557
1558 /**
b6ad8594
DM
1559 * Sets {@link $versionrequires} property to a numerical value representing
1560 * the version of Moodle core that this plugin requires.
b9934a17 1561 */
b8343e68 1562 public function load_required_main_version() {
473289a0
TH
1563 $plugin = $this->load_version_php();
1564 if (isset($plugin->requires)) {
1565 $this->versionrequires = $plugin->requires;
b9934a17 1566 }
473289a0 1567 }
b9934a17 1568
0242bdc7 1569 /**
777781d1 1570 * Initialise {@link $dependencies} to the list of other plugins (in any)
0242bdc7
TH
1571 * that this one requires to be installed.
1572 */
1573 protected function load_other_required_plugins() {
1574 $plugin = $this->load_version_php();
777781d1
TH
1575 if (!empty($plugin->dependencies)) {
1576 $this->dependencies = $plugin->dependencies;
0242bdc7 1577 } else {
777781d1 1578 $this->dependencies = array(); // By default, no dependencies.
0242bdc7
TH
1579 }
1580 }
1581
1582 /**
b6ad8594
DM
1583 * Get the list of other plugins that this plugin requires to be installed.
1584 *
1585 * @return array with keys the frankenstyle plugin name, and values either
1586 * a version string (like '2011101700') or the constant ANY_VERSION.
0242bdc7
TH
1587 */
1588 public function get_other_required_plugins() {
777781d1 1589 if (is_null($this->dependencies)) {
0242bdc7
TH
1590 $this->load_other_required_plugins();
1591 }
777781d1 1592 return $this->dependencies;
0242bdc7
TH
1593 }
1594
473289a0 1595 /**
b6ad8594
DM
1596 * Sets {@link $versiondb} property to a numerical value representing the
1597 * currently installed version of the plugin.
1598 *
1599 * If the value is null after calling this method, either the plugin
1600 * does not use versioning (typically does not have any database
1601 * data) or has not been installed yet.
473289a0
TH
1602 */
1603 public function load_db_version() {
828788f0 1604 if ($ver = self::get_version_from_config_plugins($this->component)) {
473289a0 1605 $this->versiondb = $ver;
b9934a17
DM
1606 }
1607 }
1608
1609 /**
b6ad8594
DM
1610 * Sets {@link $source} property to one of plugin_manager::PLUGIN_SOURCE_xxx
1611 * constants.
1612 *
1613 * If the property's value is null after calling this method, then
1614 * the type of the plugin has not been recognized and you should throw
1615 * an exception.
b9934a17 1616 */
b8343e68 1617 public function init_is_standard() {
b9934a17
DM
1618
1619 $standard = plugin_manager::standard_plugins_list($this->type);
1620
1621 if ($standard !== false) {
1622 $standard = array_flip($standard);
1623 if (isset($standard[$this->name])) {
1624 $this->source = plugin_manager::PLUGIN_SOURCE_STANDARD;
ec8935f5
PS
1625 } else if (!is_null($this->versiondb) and is_null($this->versiondisk)
1626 and plugin_manager::is_deleted_standard_plugin($this->type, $this->name)) {
1627 $this->source = plugin_manager::PLUGIN_SOURCE_STANDARD; // to be deleted
b9934a17
DM
1628 } else {
1629 $this->source = plugin_manager::PLUGIN_SOURCE_EXTENSION;
1630 }
1631 }
1632 }
1633
1634 /**
b6ad8594
DM
1635 * Returns true if the plugin is shipped with the official distribution
1636 * of the current Moodle version, false otherwise.
1637 *
1638 * @return bool
b9934a17
DM
1639 */
1640 public function is_standard() {
1641 return $this->source === plugin_manager::PLUGIN_SOURCE_STANDARD;
1642 }
1643
3a2300f5
DM
1644 /**
1645 * Returns true if the the given Moodle version is enough to run this plugin
1646 *
1647 * @param string|int|double $moodleversion
1648 * @return bool
1649 */
1650 public function is_core_dependency_satisfied($moodleversion) {
1651
1652 if (empty($this->versionrequires)) {
1653 return true;
1654
1655 } else {
1656 return (double)$this->versionrequires <= (double)$moodleversion;
1657 }
1658 }
1659
b9934a17 1660 /**
b6ad8594
DM
1661 * Returns the status of the plugin
1662 *
1663 * @return string one of plugin_manager::PLUGIN_STATUS_xxx constants
b9934a17
DM
1664 */
1665 public function get_status() {
1666
1667 if (is_null($this->versiondb) and is_null($this->versiondisk)) {
1668 return plugin_manager::PLUGIN_STATUS_NODB;
1669
1670 } else if (is_null($this->versiondb) and !is_null($this->versiondisk)) {
1671 return plugin_manager::PLUGIN_STATUS_NEW;
1672
1673 } else if (!is_null($this->versiondb) and is_null($this->versiondisk)) {
ec8935f5
PS
1674 if (plugin_manager::is_deleted_standard_plugin($this->type, $this->name)) {
1675 return plugin_manager::PLUGIN_STATUS_DELETE;
1676 } else {
1677 return plugin_manager::PLUGIN_STATUS_MISSING;
1678 }
b9934a17
DM
1679
1680 } else if ((string)$this->versiondb === (string)$this->versiondisk) {
1681 return plugin_manager::PLUGIN_STATUS_UPTODATE;
1682
1683 } else if ($this->versiondb < $this->versiondisk) {
1684 return plugin_manager::PLUGIN_STATUS_UPGRADE;
1685
1686 } else if ($this->versiondb > $this->versiondisk) {
1687 return plugin_manager::PLUGIN_STATUS_DOWNGRADE;
1688
1689 } else {
1690 // $version = pi(); and similar funny jokes - hopefully Donald E. Knuth will never contribute to Moodle ;-)
1691 throw new coding_exception('Unable to determine plugin state, check the plugin versions');
1692 }
1693 }
1694
1695 /**
b6ad8594
DM
1696 * Returns the information about plugin availability
1697 *
1698 * True means that the plugin is enabled. False means that the plugin is
1699 * disabled. Null means that the information is not available, or the
1700 * plugin does not support configurable availability or the availability
1701 * can not be changed.
1702 *
1703 * @return null|bool
b9934a17
DM
1704 */
1705 public function is_enabled() {
1706 return null;
1707 }
1708
1709 /**
7d8de6d8 1710 * Populates the property {@link $availableupdates} with the information provided by
dd119e21
DM
1711 * available update checker
1712 *
1713 * @param available_update_checker $provider the class providing the available update info
1714 */
7d8de6d8 1715 public function check_available_updates(available_update_checker $provider) {
c6f008e7
DM
1716 global $CFG;
1717
1718 if (isset($CFG->updateminmaturity)) {
1719 $minmaturity = $CFG->updateminmaturity;
1720 } else {
1721 // this can happen during the very first upgrade to 2.3
1722 $minmaturity = MATURITY_STABLE;
1723 }
1724
1725 $this->availableupdates = $provider->get_update_info($this->component,
1726 array('minmaturity' => $minmaturity));
dd119e21
DM
1727 }
1728
d26f3ddd 1729 /**
7d8de6d8 1730 * If there are updates for this plugin available, returns them.
d26f3ddd 1731 *
7d8de6d8
DM
1732 * Returns array of {@link available_update_info} objects, if some update
1733 * is available. Returns null if there is no update available or if the update
1734 * availability is unknown.
d26f3ddd 1735 *
7d8de6d8 1736 * @return array|null
d26f3ddd 1737 */
7d8de6d8 1738 public function available_updates() {
dd119e21 1739
7d8de6d8 1740 if (empty($this->availableupdates) or !is_array($this->availableupdates)) {
dd119e21
DM
1741 return null;
1742 }
1743
7d8de6d8
DM
1744 $updates = array();
1745
1746 foreach ($this->availableupdates as $availableupdate) {
1747 if ($availableupdate->version > $this->versiondisk) {
1748 $updates[] = $availableupdate;
1749 }
1750 }
1751
1752 if (empty($updates)) {
1753 return null;
dd119e21
DM
1754 }
1755
7d8de6d8 1756 return $updates;
d26f3ddd
DM
1757 }
1758
b9934a17 1759 /**
b6ad8594
DM
1760 * Returns the URL of the plugin settings screen
1761 *
1762 * Null value means that the plugin either does not have the settings screen
1763 * or its location is not available via this library.
1764 *
1765 * @return null|moodle_url
b9934a17
DM
1766 */
1767 public function get_settings_url() {
1768 return null;
1769 }
1770
1771 /**
b6ad8594
DM
1772 * Returns the URL of the screen where this plugin can be uninstalled
1773 *
1774 * Visiting that URL must be safe, that is a manual confirmation is needed
1775 * for actual uninstallation of the plugin. Null value means that the
1776 * plugin either does not support uninstallation, or does not require any
1777 * database cleanup or the location of the screen is not available via this
1778 * library.
1779 *
1780 * @return null|moodle_url
b9934a17
DM
1781 */
1782 public function get_uninstall_url() {
1783 return null;
1784 }
1785
1786 /**
b6ad8594
DM
1787 * Returns relative directory of the plugin with heading '/'
1788 *
1789 * @return string
b9934a17
DM
1790 */
1791 public function get_dir() {
1792 global $CFG;
1793
1794 return substr($this->rootdir, strlen($CFG->dirroot));
1795 }
1796
1797 /**
1798 * Provides access to plugin versions from {config_plugins}
1799 *
1800 * @param string $plugin plugin name
1801 * @param double $disablecache optional, defaults to false
1802 * @return int|false the stored value or false if not found
1803 */
1804 protected function get_version_from_config_plugins($plugin, $disablecache=false) {
1805 global $DB;
1806 static $pluginversions = null;
1807
1808 if (is_null($pluginversions) or $disablecache) {
f433088d
PS
1809 try {
1810 $pluginversions = $DB->get_records_menu('config_plugins', array('name' => 'version'), 'plugin', 'plugin,value');
1811 } catch (dml_exception $e) {
1812 // before install
1813 $pluginversions = array();
1814 }
b9934a17
DM
1815 }
1816
1817 if (!array_key_exists($plugin, $pluginversions)) {
1818 return false;
1819 }
1820
1821 return $pluginversions[$plugin];
1822 }
1823}
1824
b6ad8594 1825
b9934a17
DM
1826/**
1827 * General class for all plugin types that do not have their own class
1828 */
b6ad8594 1829class plugininfo_general extends plugininfo_base {
b9934a17
DM
1830}
1831
b6ad8594 1832
b9934a17
DM
1833/**
1834 * Class for page side blocks
1835 */
b6ad8594 1836class plugininfo_block extends plugininfo_base {
b9934a17 1837
b9934a17
DM
1838 public static function get_plugins($type, $typerootdir, $typeclass) {
1839
1840 // get the information about blocks at the disk
1841 $blocks = parent::get_plugins($type, $typerootdir, $typeclass);
1842
1843 // add blocks missing from disk
1844 $blocksinfo = self::get_blocks_info();
1845 foreach ($blocksinfo as $blockname => $blockinfo) {
1846 if (isset($blocks[$blockname])) {
1847 continue;
1848 }
1849 $plugin = new $typeclass();
1850 $plugin->type = $type;
1851 $plugin->typerootdir = $typerootdir;
1852 $plugin->name = $blockname;
1853 $plugin->rootdir = null;
1854 $plugin->displayname = $blockname;
1855 $plugin->versiondb = $blockinfo->version;
b8343e68 1856 $plugin->init_is_standard();
b9934a17
DM
1857
1858 $blocks[$blockname] = $plugin;
1859 }
1860
1861 return $blocks;
1862 }
1863
b8343e68 1864 public function init_display_name() {
b9934a17
DM
1865
1866 if (get_string_manager()->string_exists('pluginname', 'block_' . $this->name)) {
1867 $this->displayname = get_string('pluginname', 'block_' . $this->name);
1868
1869 } else if (($block = block_instance($this->name)) !== false) {
1870 $this->displayname = $block->get_title();
1871
1872 } else {
b8343e68 1873 parent::init_display_name();
b9934a17
DM
1874 }
1875 }
1876
b8343e68 1877 public function load_db_version() {
b9934a17
DM
1878 global $DB;
1879
1880 $blocksinfo = self::get_blocks_info();
1881 if (isset($blocksinfo[$this->name]->version)) {
1882 $this->versiondb = $blocksinfo[$this->name]->version;
1883 }
1884 }
1885
b9934a17
DM
1886 public function is_enabled() {
1887
1888 $blocksinfo = self::get_blocks_info();
1889 if (isset($blocksinfo[$this->name]->visible)) {
1890 if ($blocksinfo[$this->name]->visible) {
1891 return true;
1892 } else {
1893 return false;
1894 }
1895 } else {
1896 return parent::is_enabled();
1897 }
1898 }
1899
b9934a17
DM
1900 public function get_settings_url() {
1901
1902 if (($block = block_instance($this->name)) === false) {
1903 return parent::get_settings_url();
1904
1905 } else if ($block->has_config()) {
6740c605 1906 if (file_exists($this->full_path('settings.php'))) {
b9934a17
DM
1907 return new moodle_url('/admin/settings.php', array('section' => 'blocksetting' . $this->name));
1908 } else {
1909 $blocksinfo = self::get_blocks_info();
1910 return new moodle_url('/admin/block.php', array('block' => $blocksinfo[$this->name]->id));
1911 }
1912
1913 } else {
1914 return parent::get_settings_url();
1915 }
1916 }
1917
b9934a17
DM
1918 public function get_uninstall_url() {
1919
1920 $blocksinfo = self::get_blocks_info();
1921 return new moodle_url('/admin/blocks.php', array('delete' => $blocksinfo[$this->name]->id, 'sesskey' => sesskey()));
1922 }
1923
1924 /**
1925 * Provides access to the records in {block} table
1926 *
1927 * @param bool $disablecache do not use internal static cache
1928 * @return array array of stdClasses
1929 */
1930 protected static function get_blocks_info($disablecache=false) {
1931 global $DB;
1932 static $blocksinfocache = null;
1933
1934 if (is_null($blocksinfocache) or $disablecache) {
f433088d
PS
1935 try {
1936 $blocksinfocache = $DB->get_records('block', null, 'name', 'name,id,version,visible');
1937 } catch (dml_exception $e) {
1938 // before install
1939 $blocksinfocache = array();
1940 }
b9934a17
DM
1941 }
1942
1943 return $blocksinfocache;
1944 }
1945}
1946
b6ad8594 1947
b9934a17
DM
1948/**
1949 * Class for text filters
1950 */
b6ad8594 1951class plugininfo_filter extends plugininfo_base {
b9934a17 1952
b9934a17 1953 public static function get_plugins($type, $typerootdir, $typeclass) {
7c9b837e 1954 global $CFG, $DB;
b9934a17
DM
1955
1956 $filters = array();
1957
1958 // get the list of filters from both /filter and /mod location
1959 $installed = filter_get_all_installed();
1960
1961 foreach ($installed as $filterlegacyname => $displayname) {
1962 $plugin = new $typeclass();
1963 $plugin->type = $type;
1964 $plugin->typerootdir = $typerootdir;
1965 $plugin->name = self::normalize_legacy_name($filterlegacyname);
1966 $plugin->rootdir = $CFG->dirroot . '/' . $filterlegacyname;
1967 $plugin->displayname = $displayname;
1968
b8343e68
TH
1969 $plugin->load_disk_version();
1970 $plugin->load_db_version();
1971 $plugin->load_required_main_version();
1972 $plugin->init_is_standard();
b9934a17
DM
1973
1974 $filters[$plugin->name] = $plugin;
1975 }
1976
b9934a17 1977 $globalstates = self::get_global_states();
7c9b837e
DM
1978
1979 if ($DB->get_manager()->table_exists('filter_active')) {
1980 // if we're upgrading from 1.9, the table does not exist yet
1981 // if it does, make sure that all installed filters are registered
1982 $needsreload = false;
1983 foreach (array_keys($installed) as $filterlegacyname) {
1984 if (!isset($globalstates[self::normalize_legacy_name($filterlegacyname)])) {
1985 filter_set_global_state($filterlegacyname, TEXTFILTER_DISABLED);
1986 $needsreload = true;
1987 }
1988 }
1989 if ($needsreload) {
1990 $globalstates = self::get_global_states(true);
b9934a17 1991 }
b9934a17
DM
1992 }
1993
1994 // make sure that all registered filters are installed, just in case
1995 foreach ($globalstates as $name => $info) {
1996 if (!isset($filters[$name])) {
1997 // oops, there is a record in filter_active but the filter is not installed
1998 $plugin = new $typeclass();
1999 $plugin->type = $type;
2000 $plugin->typerootdir = $typerootdir;
2001 $plugin->name = $name;
2002 $plugin->rootdir = $CFG->dirroot . '/' . $info->legacyname;
2003 $plugin->displayname = $info->legacyname;
2004
b8343e68 2005 $plugin->load_db_version();
b9934a17
DM
2006
2007 if (is_null($plugin->versiondb)) {
2008 // this is a hack to stimulate 'Missing from disk' error
2009 // because $plugin->versiondisk will be null !== false
2010 $plugin->versiondb = false;
2011 }
2012
2013 $filters[$plugin->name] = $plugin;
2014 }
2015 }
2016
2017 return $filters;
2018 }
2019
b8343e68 2020 public function init_display_name() {
b9934a17
DM
2021 // do nothing, the name is set in self::get_plugins()
2022 }
2023
2024 /**
b6ad8594 2025 * @see load_version_php()
b9934a17 2026 */
473289a0 2027 protected function load_version_php() {
b9934a17 2028 if (strpos($this->name, 'mod_') === 0) {
473289a0
TH
2029 // filters bundled with modules do not have a version.php and so
2030 // do not provide their own versioning information.
2031 return new stdClass();
b9934a17 2032 }
473289a0 2033 return parent::load_version_php();
b9934a17
DM
2034 }
2035
b9934a17
DM
2036 public function is_enabled() {
2037
2038 $globalstates = self::get_global_states();
2039
2040 foreach ($globalstates as $filterlegacyname => $info) {
2041 $name = self::normalize_legacy_name($filterlegacyname);
2042 if ($name === $this->name) {
2043 if ($info->active == TEXTFILTER_DISABLED) {
2044 return false;
2045 } else {
2046 // it may be 'On' or 'Off, but available'
2047 return null;
2048 }
2049 }
2050 }
2051
2052 return null;
2053 }
2054
b9934a17
DM
2055 public function get_settings_url() {
2056
2057 $globalstates = self::get_global_states();
2058 $legacyname = $globalstates[$this->name]->legacyname;
2059 if (filter_has_global_settings($legacyname)) {
2060 return new moodle_url('/admin/settings.php', array('section' => 'filtersetting' . str_replace('/', '', $legacyname)));
2061 } else {
2062 return null;
2063 }
2064 }
2065
b9934a17
DM
2066 public function get_uninstall_url() {
2067
2068 if (strpos($this->name, 'mod_') === 0) {
2069 return null;
2070 } else {
2071 $globalstates = self::get_global_states();
2072 $legacyname = $globalstates[$this->name]->legacyname;
2073 return new moodle_url('/admin/filters.php', array('sesskey' => sesskey(), 'filterpath' => $legacyname, 'action' => 'delete'));
2074 }
2075 }
2076
2077 /**
2078 * Convert legacy filter names like 'filter/foo' or 'mod/bar' into frankenstyle
2079 *
2080 * @param string $legacyfiltername legacy filter name
2081 * @return string frankenstyle-like name
2082 */
2083 protected static function normalize_legacy_name($legacyfiltername) {
2084
2085 $name = str_replace('/', '_', $legacyfiltername);
2086 if (strpos($name, 'filter_') === 0) {
2087 $name = substr($name, 7);
2088 if (empty($name)) {
2089 throw new coding_exception('Unable to determine filter name: ' . $legacyfiltername);
2090 }
2091 }
2092
2093 return $name;
2094 }
2095
2096 /**
2097 * Provides access to the results of {@link filter_get_global_states()}
2098 * but indexed by the normalized filter name
2099 *
2100 * The legacy filter name is available as ->legacyname property.
2101 *
2102 * @param bool $disablecache
2103 * @return array
2104 */
2105 protected static function get_global_states($disablecache=false) {
2106 global $DB;
2107 static $globalstatescache = null;
2108
2109 if ($disablecache or is_null($globalstatescache)) {
2110
2111 if (!$DB->get_manager()->table_exists('filter_active')) {
2112 // we're upgrading from 1.9 and the table used by {@link filter_get_global_states()}
2113 // does not exist yet
2114 $globalstatescache = array();
2115
2116 } else {
2117 foreach (filter_get_global_states() as $legacyname => $info) {
2118 $name = self::normalize_legacy_name($legacyname);
2119 $filterinfo = new stdClass();
2120 $filterinfo->legacyname = $legacyname;
2121 $filterinfo->active = $info->active;
2122 $filterinfo->sortorder = $info->sortorder;
2123 $globalstatescache[$name] = $filterinfo;
2124 }
2125 }
2126 }
2127
2128 return $globalstatescache;
2129 }
2130}
2131
b6ad8594 2132
b9934a17
DM
2133/**
2134 * Class for activity modules
2135 */
b6ad8594 2136class plugininfo_mod extends plugininfo_base {
b9934a17 2137
b9934a17
DM
2138 public static function get_plugins($type, $typerootdir, $typeclass) {
2139
2140 // get the information about plugins at the disk
2141 $modules = parent::get_plugins($type, $typerootdir, $typeclass);
2142
2143 // add modules missing from disk
2144 $modulesinfo = self::get_modules_info();
2145 foreach ($modulesinfo as $modulename => $moduleinfo) {
2146 if (isset($modules[$modulename])) {
2147 continue;
2148 }
2149 $plugin = new $typeclass();
2150 $plugin->type = $type;
2151 $plugin->typerootdir = $typerootdir;
2152 $plugin->name = $modulename;
2153 $plugin->rootdir = null;
2154 $plugin->displayname = $modulename;
2155 $plugin->versiondb = $moduleinfo->version;
b8343e68 2156 $plugin->init_is_standard();
b9934a17
DM
2157
2158 $modules[$modulename] = $plugin;
2159 }
2160
2161 return $modules;
2162 }
2163
b8343e68 2164 public function init_display_name() {
828788f0
TH
2165 if (get_string_manager()->string_exists('pluginname', $this->component)) {
2166 $this->displayname = get_string('pluginname', $this->component);
b9934a17 2167 } else {
828788f0 2168 $this->displayname = get_string('modulename', $this->component);
b9934a17
DM
2169 }
2170 }
2171
2172 /**
473289a0
TH
2173 * Load the data from version.php.
2174 * @return object the data object defined in version.php.
b9934a17 2175 */
473289a0
TH
2176 protected function load_version_php() {
2177 $versionfile = $this->full_path('version.php');
b9934a17 2178
473289a0 2179 $module = new stdClass();
b9934a17
DM
2180 if (is_readable($versionfile)) {
2181 include($versionfile);
b9934a17 2182 }
473289a0 2183 return $module;
b9934a17
DM
2184 }
2185
b8343e68 2186 public function load_db_version() {
b9934a17
DM
2187 global $DB;
2188
2189 $modulesinfo = self::get_modules_info();
2190 if (isset($modulesinfo[$this->name]->version)) {
2191 $this->versiondb = $modulesinfo[$this->name]->version;
2192 }
2193 }
2194
b9934a17
DM
2195 public function is_enabled() {
2196
2197 $modulesinfo = self::get_modules_info();
2198 if (isset($modulesinfo[$this->name]->visible)) {
2199 if ($modulesinfo[$this->name]->visible) {
2200 return true;
2201 } else {
2202 return false;
2203 }
2204 } else {
2205 return parent::is_enabled();
2206 }
2207 }
2208
b9934a17
DM
2209 public function get_settings_url() {
2210
6740c605 2211 if (file_exists($this->full_path('settings.php')) or file_exists($this->full_path('settingstree.php'))) {
b9934a17
DM
2212 return new moodle_url('/admin/settings.php', array('section' => 'modsetting' . $this->name));
2213 } else {
2214 return parent::get_settings_url();
2215 }
2216 }
2217
b9934a17
DM
2218 public function get_uninstall_url() {
2219
2220 if ($this->name !== 'forum') {
2221 return new moodle_url('/admin/modules.php', array('delete' => $this->name, 'sesskey' => sesskey()));
2222 } else {
2223 return null;
2224 }
2225 }
2226
2227 /**
2228 * Provides access to the records in {modules} table
2229 *
2230 * @param bool $disablecache do not use internal static cache
2231 * @return array array of stdClasses
2232 */
2233 protected static function get_modules_info($disablecache=false) {
2234 global $DB;
2235 static $modulesinfocache = null;
2236
2237 if (is_null($modulesinfocache) or $disablecache) {
f433088d
PS
2238 try {
2239 $modulesinfocache = $DB->get_records('modules', null, 'name', 'name,id,version,visible');
2240 } catch (dml_exception $e) {
2241 // before install
2242 $modulesinfocache = array();
2243 }
b9934a17
DM
2244 }
2245
2246 return $modulesinfocache;
2247 }
2248}
2249
0242bdc7
TH
2250
2251/**
2252 * Class for question behaviours.
2253 */
b6ad8594
DM
2254class plugininfo_qbehaviour extends plugininfo_base {
2255
828788f0
TH
2256 public function get_uninstall_url() {
2257 return new moodle_url('/admin/qbehaviours.php',
2258 array('delete' => $this->name, 'sesskey' => sesskey()));
2259 }
0242bdc7
TH
2260}
2261
2262
b9934a17
DM
2263/**
2264 * Class for question types
2265 */
b6ad8594
DM
2266class plugininfo_qtype extends plugininfo_base {
2267
828788f0
TH
2268 public function get_uninstall_url() {
2269 return new moodle_url('/admin/qtypes.php',
2270 array('delete' => $this->name, 'sesskey' => sesskey()));
2271 }
b9934a17
DM
2272}
2273
b9934a17
DM
2274
2275/**
2276 * Class for authentication plugins
2277 */
b6ad8594 2278class plugininfo_auth extends plugininfo_base {
b9934a17 2279
b9934a17
DM
2280 public function is_enabled() {
2281 global $CFG;
2282 /** @var null|array list of enabled authentication plugins */
2283 static $enabled = null;
2284
2285 if (in_array($this->name, array('nologin', 'manual'))) {
2286 // these two are always enabled and can't be disabled
2287 return null;
2288 }
2289
2290 if (is_null($enabled)) {
d5d181f5 2291 $enabled = array_flip(explode(',', $CFG->auth));
b9934a17
DM
2292 }
2293
2294 return isset($enabled[$this->name]);
2295 }
2296
b9934a17 2297 public function get_settings_url() {
6740c605 2298 if (file_exists($this->full_path('settings.php'))) {
b9934a17
DM
2299 return new moodle_url('/admin/settings.php', array('section' => 'authsetting' . $this->name));
2300 } else {
2301 return new moodle_url('/admin/auth_config.php', array('auth' => $this->name));
2302 }
2303 }
2304}
2305
b6ad8594 2306
b9934a17
DM
2307/**
2308 * Class for enrolment plugins
2309 */
b6ad8594 2310class plugininfo_enrol extends plugininfo_base {
b9934a17 2311
b9934a17
DM
2312 public function is_enabled() {
2313 global $CFG;
2314 /** @var null|array list of enabled enrolment plugins */
2315 static $enabled = null;
2316
b6ad8594
DM
2317 // We do not actually need whole enrolment classes here so we do not call
2318 // {@link enrol_get_plugins()}. Note that this may produce slightly different
2319 // results, for example if the enrolment plugin does not contain lib.php
2320 // but it is listed in $CFG->enrol_plugins_enabled
2321
b9934a17 2322 if (is_null($enabled)) {
d5d181f5 2323 $enabled = array_flip(explode(',', $CFG->enrol_plugins_enabled));
b9934a17
DM
2324 }
2325
2326 return isset($enabled[$this->name]);
2327 }
2328
b9934a17
DM
2329 public function get_settings_url() {
2330
6740c605 2331 if ($this->is_enabled() or file_exists($this->full_path('settings.php'))) {
b9934a17
DM
2332 return new moodle_url('/admin/settings.php', array('section' => 'enrolsettings' . $this->name));
2333 } else {
2334 return parent::get_settings_url();
2335 }
2336 }
2337
b9934a17
DM
2338 public function get_uninstall_url() {
2339 return new moodle_url('/admin/enrol.php', array('action' => 'uninstall', 'enrol' => $this->name, 'sesskey' => sesskey()));
2340 }
2341}
2342
b6ad8594 2343
b9934a17
DM
2344/**
2345 * Class for messaging processors
2346 */
b6ad8594 2347class plugininfo_message extends plugininfo_base {
b9934a17 2348
b9934a17 2349 public function get_settings_url() {
bc795b98
RK
2350 $processors = get_message_processors();
2351 if (isset($processors[$this->name])) {
2352 $processor = $processors[$this->name];
2353 if ($processor->available && $processor->hassettings) {
2354 return new moodle_url('settings.php', array('section' => 'messagesetting'.$processor->name));
2355 }
0210ce10 2356 }
bc795b98 2357 return parent::get_settings_url();
b9934a17 2358 }
b9934a17 2359
bede23f7
RK
2360 /**
2361 * @see plugintype_interface::is_enabled()
2362 */
2363 public function is_enabled() {
2364 $processors = get_message_processors();
2365 if (isset($processors[$this->name])) {
2366 return $processors[$this->name]->configured && $processors[$this->name]->enabled;
0210ce10 2367 } else {
bede23f7
RK
2368 return parent::is_enabled();
2369 }
2370 }
3f9d9e28
RK
2371
2372 /**
2373 * @see plugintype_interface::get_uninstall_url()
2374 */
2375 public function get_uninstall_url() {
2376 $processors = get_message_processors();
2377 if (isset($processors[$this->name])) {
2378 return new moodle_url('message.php', array('uninstall' => $processors[$this->name]->id, 'sesskey' => sesskey()));
2379 } else {
2380 return parent::get_uninstall_url();
0210ce10 2381 }
b9934a17
DM
2382 }
2383}
2384
b6ad8594 2385
b9934a17
DM
2386/**
2387 * Class for repositories
2388 */
b6ad8594 2389class plugininfo_repository extends plugininfo_base {
b9934a17 2390
b9934a17
DM
2391 public function is_enabled() {
2392
2393 $enabled = self::get_enabled_repositories();
2394
2395 return isset($enabled[$this->name]);
2396 }
2397
b9934a17
DM
2398 public function get_settings_url() {
2399
2400 if ($this->is_enabled()) {
2401 return new moodle_url('/admin/repository.php', array('sesskey' => sesskey(), 'action' => 'edit', 'repos' => $this->name));
2402 } else {
2403 return parent::get_settings_url();
2404 }
2405 }
2406
2407 /**
2408 * Provides access to the records in {repository} table
2409 *
2410 * @param bool $disablecache do not use internal static cache
2411 * @return array array of stdClasses
2412 */
2413 protected static function get_enabled_repositories($disablecache=false) {
2414 global $DB;
2415 static $repositories = null;
2416
2417 if (is_null($repositories) or $disablecache) {
2418 $repositories = $DB->get_records('repository', null, 'type', 'type,visible,sortorder');
2419 }
2420
2421 return $repositories;
2422 }
2423}
2424
b6ad8594 2425
b9934a17
DM
2426/**
2427 * Class for portfolios
2428 */
b6ad8594 2429class plugininfo_portfolio extends plugininfo_base {
b9934a17 2430
b9934a17
DM
2431 public function is_enabled() {
2432
2433 $enabled = self::get_enabled_portfolios();
2434
2435 return isset($enabled[$this->name]);
2436 }
2437
2438 /**
2439 * Provides access to the records in {portfolio_instance} table
2440 *
2441 * @param bool $disablecache do not use internal static cache
2442 * @return array array of stdClasses
2443 */
2444 protected static function get_enabled_portfolios($disablecache=false) {
2445 global $DB;
2446 static $portfolios = null;
2447
2448 if (is_null($portfolios) or $disablecache) {
2449 $portfolios = array();
2450 $instances = $DB->get_recordset('portfolio_instance', null, 'plugin');
2451 foreach ($instances as $instance) {
2452 if (isset($portfolios[$instance->plugin])) {
2453 if ($instance->visible) {
2454 $portfolios[$instance->plugin]->visible = $instance->visible;
2455 }
2456 } else {
2457 $portfolios[$instance->plugin] = $instance;
2458 }
2459 }
2460 }
2461
2462 return $portfolios;
2463 }
2464}
2465
b6ad8594 2466
b9934a17
DM
2467/**
2468 * Class for themes
2469 */
b6ad8594 2470class plugininfo_theme extends plugininfo_base {
b9934a17 2471
b9934a17
DM
2472 public function is_enabled() {
2473 global $CFG;
2474
2475 if ((!empty($CFG->theme) and $CFG->theme === $this->name) or
2476 (!empty($CFG->themelegacy) and $CFG->themelegacy === $this->name)) {
2477 return true;
2478 } else {
2479 return parent::is_enabled();
2480 }
2481 }
2482}
2483
b6ad8594 2484
b9934a17
DM
2485/**
2486 * Class representing an MNet service
2487 */
b6ad8594 2488class plugininfo_mnetservice extends plugininfo_base {
b9934a17 2489
b9934a17
DM
2490 public function is_enabled() {
2491 global $CFG;
2492
2493 if (empty($CFG->mnet_dispatcher_mode) || $CFG->mnet_dispatcher_mode !== 'strict') {
2494 return false;
2495 } else {
2496 return parent::is_enabled();
2497 }
2498 }
2499}
3cdfaeef 2500
b6ad8594 2501
3cdfaeef
PS
2502/**
2503 * Class for admin tool plugins
2504 */
b6ad8594 2505class plugininfo_tool extends plugininfo_base {
3cdfaeef
PS
2506
2507 public function get_uninstall_url() {
2508 return new moodle_url('/admin/tools.php', array('delete' => $this->name, 'sesskey' => sesskey()));
2509 }
2510}
4f6bba20 2511
b6ad8594 2512
4f6bba20
PS
2513/**
2514 * Class for admin tool plugins
2515 */
b6ad8594 2516class plugininfo_report extends plugininfo_base {
4f6bba20
PS
2517
2518 public function get_uninstall_url() {
2519 return new moodle_url('/admin/reports.php', array('delete' => $this->name, 'sesskey' => sesskey()));
2520 }
2521}