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