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