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