MDL-36950 pluginlib - add MDL to comment
[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 828 $curl = new curl(array('proxy' => true));
4785c45d
DM
829 $response = $curl->post($this->prepare_request_url(), $this->prepare_request_params(), $this->prepare_request_options());
830 $curlerrno = $curl->get_errno();
831 if (!empty($curlerrno)) {
832 throw new available_update_checker_exception('err_response_curl', 'cURL error '.$curlerrno.': '.$curl->error);
833 }
cd0bb55f
DM
834 $curlinfo = $curl->get_info();
835 if ($curlinfo['http_code'] != 200) {
836 throw new available_update_checker_exception('err_response_http_code', $curlinfo['http_code']);
837 }
cd0bb55f
DM
838 return $response;
839 }
840
841 /**
842 * Makes sure the response is valid, has correct API format etc.
843 *
7d8de6d8 844 * @param string $response raw response as returned by the {@link self::get_response()}
cd0bb55f
DM
845 * @throws available_update_checker_exception
846 */
7d8de6d8
DM
847 protected function validate_response($response) {
848
849 $response = $this->decode_response($response);
cd0bb55f
DM
850
851 if (empty($response)) {
852 throw new available_update_checker_exception('err_response_empty');
853 }
854
7d8de6d8
DM
855 if (empty($response['status']) or $response['status'] !== 'OK') {
856 throw new available_update_checker_exception('err_response_status', $response['status']);
857 }
858
85d75163 859 if (empty($response['apiver']) or $response['apiver'] !== '1.1') {
7d8de6d8 860 throw new available_update_checker_exception('err_response_format_version', $response['apiver']);
cd0bb55f
DM
861 }
862
7d8de6d8 863 if (empty($response['forbranch']) or $response['forbranch'] !== moodle_major_version(true)) {
d5d2e353 864 throw new available_update_checker_exception('err_response_target_version', $response['forbranch']);
cd0bb55f
DM
865 }
866 }
867
868 /**
7d8de6d8 869 * Decodes the raw string response from the update notifications provider
b9934a17 870 *
7d8de6d8
DM
871 * @param string $response as returned by {@link self::get_response()}
872 * @return array decoded response structure
b9934a17 873 */
7d8de6d8
DM
874 protected function decode_response($response) {
875 return json_decode($response, true);
cd0bb55f 876 }
b9934a17
DM
877
878 /**
7d8de6d8
DM
879 * Stores the valid fetched response for later usage
880 *
881 * This implementation uses the config_plugins table as the permanent storage.
b9934a17 882 *
7d8de6d8 883 * @param string $response raw valid data returned by {@link self::get_response()}
b9934a17 884 */
7d8de6d8
DM
885 protected function store_response($response) {
886
887 set_config('recentfetch', time(), 'core_plugin');
888 set_config('recentresponse', $response, 'core_plugin');
889
890 $this->restore_response(true);
cd0bb55f 891 }
b9934a17
DM
892
893 /**
7d8de6d8 894 * Loads the most recent raw response record we have fetched
b9934a17 895 *
c62580b9
DM
896 * After this method is called, $this->recentresponse is set to an array. If the
897 * array is empty, then either no data have been fetched yet or the fetched data
898 * do not have expected format (and thence they are ignored and a debugging
899 * message is displayed).
900 *
7d8de6d8 901 * This implementation uses the config_plugins table as the permanent storage.
b9934a17 902 *
7d8de6d8 903 * @param bool $forcereload reload even if it was already loaded
b9934a17 904 */
7d8de6d8
DM
905 protected function restore_response($forcereload = false) {
906
907 if (!$forcereload and !is_null($this->recentresponse)) {
908 // we already have it, nothing to do
909 return;
cd0bb55f
DM
910 }
911
7d8de6d8
DM
912 $config = get_config('core_plugin');
913
914 if (!empty($config->recentresponse) and !empty($config->recentfetch)) {
915 try {
916 $this->validate_response($config->recentresponse);
917 $this->recentfetch = $config->recentfetch;
918 $this->recentresponse = $this->decode_response($config->recentresponse);
660c4d46 919 } catch (available_update_checker_exception $e) {
a22de4ce
DM
920 // The server response is not valid. Behave as if no data were fetched yet.
921 // This may happen when the most recent update info (cached locally) has been
922 // fetched with the previous branch of Moodle (like during an upgrade from 2.x
923 // to 2.y) or when the API of the response has changed.
c62580b9 924 $this->recentresponse = array();
7d8de6d8
DM
925 }
926
cd0bb55f 927 } else {
7d8de6d8 928 $this->recentresponse = array();
cd0bb55f
DM
929 }
930 }
931
7b35553b
DM
932 /**
933 * Compares two raw {@link $recentresponse} records and returns the list of changed updates
934 *
935 * This method is used to populate potential update info to be sent to site admins.
936 *
19d11b3b
DM
937 * @param array $old
938 * @param array $new
7b35553b
DM
939 * @throws available_update_checker_exception
940 * @return array parts of $new['updates'] that have changed
941 */
19d11b3b 942 protected function compare_responses(array $old, array $new) {
7b35553b 943
19d11b3b 944 if (empty($new)) {
7b35553b
DM
945 return array();
946 }
947
948 if (!array_key_exists('updates', $new)) {
949 throw new available_update_checker_exception('err_response_format');
950 }
951
19d11b3b 952 if (empty($old)) {
7b35553b
DM
953 return $new['updates'];
954 }
955
956 if (!array_key_exists('updates', $old)) {
957 throw new available_update_checker_exception('err_response_format');
958 }
959
960 $changes = array();
961
962 foreach ($new['updates'] as $newcomponent => $newcomponentupdates) {
963 if (empty($old['updates'][$newcomponent])) {
964 $changes[$newcomponent] = $newcomponentupdates;
965 continue;
966 }
967 foreach ($newcomponentupdates as $newcomponentupdate) {
968 $inold = false;
969 foreach ($old['updates'][$newcomponent] as $oldcomponentupdate) {
970 if ($newcomponentupdate['version'] == $oldcomponentupdate['version']) {
971 $inold = true;
972 }
973 }
974 if (!$inold) {
975 if (!isset($changes[$newcomponent])) {
976 $changes[$newcomponent] = array();
977 }
978 $changes[$newcomponent][] = $newcomponentupdate;
979 }
980 }
981 }
982
983 return $changes;
984 }
985
cd0bb55f
DM
986 /**
987 * Returns the URL to send update requests to
988 *
989 * During the development or testing, you can set $CFG->alternativeupdateproviderurl
990 * to a custom URL that will be used. Otherwise the standard URL will be returned.
991 *
992 * @return string URL
993 */
994 protected function prepare_request_url() {
995 global $CFG;
996
56c05088
DM
997 if (!empty($CFG->config_php_settings['alternativeupdateproviderurl'])) {
998 return $CFG->config_php_settings['alternativeupdateproviderurl'];
cd0bb55f 999 } else {
40a9c2e5 1000 return 'https://download.moodle.org/api/1.1/updates.php';
cd0bb55f
DM
1001 }
1002 }
1003
55585f3a 1004 /**
4442cc80 1005 * Sets the properties currentversion, currentrelease, currentbranch and currentplugins
55585f3a
DM
1006 *
1007 * @param bool $forcereload
1008 */
1009 protected function load_current_environment($forcereload=false) {
1010 global $CFG;
1011
1012 if (!is_null($this->currentversion) and !$forcereload) {
1013 // nothing to do
1014 return;
1015 }
1016
975311d3
PS
1017 $version = null;
1018 $release = null;
1019
55585f3a
DM
1020 require($CFG->dirroot.'/version.php');
1021 $this->currentversion = $version;
4442cc80 1022 $this->currentrelease = $release;
55585f3a
DM
1023 $this->currentbranch = moodle_major_version(true);
1024
1025 $pluginman = plugin_manager::instance();
1026 foreach ($pluginman->get_plugins() as $type => $plugins) {
1027 foreach ($plugins as $plugin) {
1028 if (!$plugin->is_standard()) {
1029 $this->currentplugins[$plugin->component] = $plugin->versiondisk;
1030 }
1031 }
1032 }
1033 }
1034
cd0bb55f
DM
1035 /**
1036 * Returns the list of HTTP params to be sent to the updates provider URL
1037 *
1038 * @return array of (string)param => (string)value
1039 */
1040 protected function prepare_request_params() {
1041 global $CFG;
1042
55585f3a 1043 $this->load_current_environment();
7d8de6d8
DM
1044 $this->restore_response();
1045
cd0bb55f
DM
1046 $params = array();
1047 $params['format'] = 'json';
1048
7d8de6d8
DM
1049 if (isset($this->recentresponse['ticket'])) {
1050 $params['ticket'] = $this->recentresponse['ticket'];
cd0bb55f
DM
1051 }
1052
55585f3a
DM
1053 if (isset($this->currentversion)) {
1054 $params['version'] = $this->currentversion;
1055 } else {
1056 throw new coding_exception('Main Moodle version must be already known here');
cd0bb55f
DM
1057 }
1058
55585f3a
DM
1059 if (isset($this->currentbranch)) {
1060 $params['branch'] = $this->currentbranch;
1061 } else {
1062 throw new coding_exception('Moodle release must be already known here');
1063 }
1064
1065 $plugins = array();
1066 foreach ($this->currentplugins as $plugin => $version) {
1067 $plugins[] = $plugin.'@'.$version;
1068 }
1069 if (!empty($plugins)) {
1070 $params['plugins'] = implode(',', $plugins);
cd0bb55f
DM
1071 }
1072
cd0bb55f
DM
1073 return $params;
1074 }
be378880 1075
4785c45d
DM
1076 /**
1077 * Returns the list of cURL options to use when fetching available updates data
1078 *
1079 * @return array of (string)param => (string)value
1080 */
1081 protected function prepare_request_options() {
1082 global $CFG;
1083
1084 $options = array(
1085 'CURLOPT_SSL_VERIFYHOST' => 2, // this is the default in {@link curl} class but just in case
1086 'CURLOPT_SSL_VERIFYPEER' => true,
1087 );
1088
1089 $cacertfile = $CFG->dataroot.'/moodleorgca.crt';
1090 if (is_readable($cacertfile)) {
1091 // Do not use CA certs provided by the operating system. Instead,
1092 // use this CA cert to verify the updates provider.
1093 $options['CURLOPT_CAINFO'] = $cacertfile;
1094 }
1095
1096 return $options;
1097 }
1098
be378880
DM
1099 /**
1100 * Returns the current timestamp
1101 *
1102 * @return int the timestamp
1103 */
1104 protected function cron_current_timestamp() {
1105 return time();
1106 }
1107
1108 /**
1109 * Output cron debugging info
1110 *
1111 * @see mtrace()
1112 * @param string $msg output message
1113 * @param string $eol end of line
1114 */
1115 protected function cron_mtrace($msg, $eol = PHP_EOL) {
1116 mtrace($msg, $eol);
1117 }
1118
1119 /**
1120 * Decide if the autocheck feature is disabled in the server setting
1121 *
1122 * @return bool true if autocheck enabled, false if disabled
1123 */
1124 protected function cron_autocheck_enabled() {
718eb2a5
DM
1125 global $CFG;
1126
be378880
DM
1127 if (empty($CFG->updateautocheck)) {
1128 return false;
1129 } else {
1130 return true;
1131 }
1132 }
1133
1134 /**
1135 * Decide if the recently fetched data are still fresh enough
1136 *
1137 * @param int $now current timestamp
1138 * @return bool true if no need to re-fetch, false otherwise
1139 */
1140 protected function cron_has_fresh_fetch($now) {
1141 $recent = $this->get_last_timefetched();
1142
1143 if (empty($recent)) {
1144 return false;
1145 }
1146
1147 if ($now < $recent) {
1148 $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1149 return true;
1150 }
1151
7092ea5d 1152 if ($now - $recent > 24 * HOURSECS) {
be378880
DM
1153 return false;
1154 }
1155
1156 return true;
1157 }
1158
1159 /**
1160 * Decide if the fetch is outadated or even missing
1161 *
1162 * @param int $now current timestamp
1163 * @return bool false if no need to re-fetch, true otherwise
1164 */
1165 protected function cron_has_outdated_fetch($now) {
1166 $recent = $this->get_last_timefetched();
1167
1168 if (empty($recent)) {
1169 return true;
1170 }
1171
1172 if ($now < $recent) {
1173 $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1174 return false;
1175 }
1176
1177 if ($now - $recent > 48 * HOURSECS) {
1178 return true;
1179 }
1180
1181 return false;
1182 }
1183
1184 /**
1185 * Returns the cron execution offset for this site
1186 *
1187 * The main {@link self::cron()} is supposed to run every night in some random time
1188 * between 01:00 and 06:00 AM (local time). The exact moment is defined by so called
1189 * execution offset, that is the amount of time after 01:00 AM. The offset value is
1190 * initially generated randomly and then used consistently at the site. This way, the
1191 * regular checks against the download.moodle.org server are spread in time.
1192 *
1193 * @return int the offset number of seconds from range 1 sec to 5 hours
1194 */
1195 protected function cron_execution_offset() {
1196 global $CFG;
1197
1198 if (empty($CFG->updatecronoffset)) {
1199 set_config('updatecronoffset', rand(1, 5 * HOURSECS));
1200 }
1201
1202 return $CFG->updatecronoffset;
1203 }
1204
1205 /**
1206 * Fetch available updates info and eventually send notification to site admins
1207 */
1208 protected function cron_execute() {
7b35553b 1209
19d11b3b 1210 try {
fd87d0bf
AB
1211 $this->restore_response();
1212 $previous = $this->recentresponse;
1213 $this->fetch();
1214 $this->restore_response(true);
1215 $current = $this->recentresponse;
19d11b3b
DM
1216 $changes = $this->compare_responses($previous, $current);
1217 $notifications = $this->cron_notifications($changes);
1218 $this->cron_notify($notifications);
a77141a7 1219 $this->cron_mtrace('done');
19d11b3b
DM
1220 } catch (available_update_checker_exception $e) {
1221 $this->cron_mtrace('FAILED!');
1222 }
1223 }
1224
1225 /**
1226 * Given the list of changes in available updates, pick those to send to site admins
1227 *
1228 * @param array $changes as returned by {@link self::compare_responses()}
1229 * @return array of available_update_info objects to send to site admins
1230 */
1231 protected function cron_notifications(array $changes) {
1232 global $CFG;
1233
1234 $notifications = array();
1235 $pluginman = plugin_manager::instance();
1236 $plugins = $pluginman->get_plugins(true);
1237
1238 foreach ($changes as $component => $componentchanges) {
718eb2a5
DM
1239 if (empty($componentchanges)) {
1240 continue;
1241 }
19d11b3b
DM
1242 $componentupdates = $this->get_update_info($component,
1243 array('minmaturity' => $CFG->updateminmaturity, 'notifybuilds' => $CFG->updatenotifybuilds));
718eb2a5
DM
1244 if (empty($componentupdates)) {
1245 continue;
1246 }
19d11b3b
DM
1247 // notify only about those $componentchanges that are present in $componentupdates
1248 // to respect the preferences
1249 foreach ($componentchanges as $componentchange) {
1250 foreach ($componentupdates as $componentupdate) {
1251 if ($componentupdate->version == $componentchange['version']) {
1252 if ($component == 'core') {
1253 // in case of 'core' this is enough, we already know that the
1254 // $componentupdate is a real update with higher version
1255 $notifications[] = $componentupdate;
1256 } else {
1257 // use the plugin_manager to check if the reported $componentchange
1258 // is a real update with higher version. such a real update must be
1259 // present in the 'availableupdates' property of one of the component's
1260 // available_update_info object
1261 list($plugintype, $pluginname) = normalize_component($component);
1262 if (!empty($plugins[$plugintype][$pluginname]->availableupdates)) {
1263 foreach ($plugins[$plugintype][$pluginname]->availableupdates as $availableupdate) {
1264 if ($availableupdate->version == $componentchange['version']) {
1265 $notifications[] = $componentupdate;
1266 }
1267 }
1268 }
1269 }
1270 }
1271 }
1272 }
1273 }
1274
1275 return $notifications;
be378880 1276 }
a77141a7
DM
1277
1278 /**
1279 * Sends the given notifications to site admins via messaging API
1280 *
1281 * @param array $notifications array of available_update_info objects to send
1282 */
1283 protected function cron_notify(array $notifications) {
1284 global $CFG;
1285
1286 if (empty($notifications)) {
1287 return;
1288 }
1289
1290 $admins = get_admins();
1291
1292 if (empty($admins)) {
1293 return;
1294 }
1295
1296 $this->cron_mtrace('sending notifications ... ', '');
1297
1298 $text = get_string('updatenotifications', 'core_admin') . PHP_EOL;
1299 $html = html_writer::tag('h1', get_string('updatenotifications', 'core_admin')) . PHP_EOL;
1300
1301 $coreupdates = array();
1302 $pluginupdates = array();
1303
660c4d46 1304 foreach ($notifications as $notification) {
a77141a7
DM
1305 if ($notification->component == 'core') {
1306 $coreupdates[] = $notification;
1307 } else {
1308 $pluginupdates[] = $notification;
1309 }
1310 }
1311
1312 if (!empty($coreupdates)) {
1313 $text .= PHP_EOL . get_string('updateavailable', 'core_admin') . PHP_EOL;
1314 $html .= html_writer::tag('h2', get_string('updateavailable', 'core_admin')) . PHP_EOL;
1315 $html .= html_writer::start_tag('ul') . PHP_EOL;
1316 foreach ($coreupdates as $coreupdate) {
1317 $html .= html_writer::start_tag('li');
1318 if (isset($coreupdate->release)) {
1319 $text .= get_string('updateavailable_release', 'core_admin', $coreupdate->release);
1320 $html .= html_writer::tag('strong', get_string('updateavailable_release', 'core_admin', $coreupdate->release));
1321 }
1322 if (isset($coreupdate->version)) {
1323 $text .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
1324 $html .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
1325 }
1326 if (isset($coreupdate->maturity)) {
1327 $text .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
1328 $html .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
1329 }
1330 $text .= PHP_EOL;
1331 $html .= html_writer::end_tag('li') . PHP_EOL;
1332 }
1333 $text .= PHP_EOL;
1334 $html .= html_writer::end_tag('ul') . PHP_EOL;
1335
1336 $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/index.php');
1337 $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
1338 $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/index.php', $CFG->wwwroot.'/'.$CFG->admin.'/index.php'));
1339 $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
1340 }
1341
1342 if (!empty($pluginupdates)) {
1343 $text .= PHP_EOL . get_string('updateavailableforplugin', 'core_admin') . PHP_EOL;
1344 $html .= html_writer::tag('h2', get_string('updateavailableforplugin', 'core_admin')) . PHP_EOL;
1345
1346 $html .= html_writer::start_tag('ul') . PHP_EOL;
1347 foreach ($pluginupdates as $pluginupdate) {
1348 $html .= html_writer::start_tag('li');
1349 $text .= get_string('pluginname', $pluginupdate->component);
1350 $html .= html_writer::tag('strong', get_string('pluginname', $pluginupdate->component));
1351
1352 $text .= ' ('.$pluginupdate->component.')';
1353 $html .= ' ('.$pluginupdate->component.')';
1354
1355 $text .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
1356 $html .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
1357
1358 $text .= PHP_EOL;
1359 $html .= html_writer::end_tag('li') . PHP_EOL;
1360 }
1361 $text .= PHP_EOL;
1362 $html .= html_writer::end_tag('ul') . PHP_EOL;
b9934a17 1363
a77141a7
DM
1364 $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php');
1365 $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
1366 $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/plugins.php', $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php'));
1367 $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
1368 }
1369
1370 $a = array('siteurl' => $CFG->wwwroot);
1371 $text .= get_string('updatenotificationfooter', 'core_admin', $a) . PHP_EOL;
1372 $a = array('siteurl' => html_writer::link($CFG->wwwroot, $CFG->wwwroot));
1373 $html .= html_writer::tag('footer', html_writer::tag('p', get_string('updatenotificationfooter', 'core_admin', $a),
1374 array('style' => 'font-size:smaller; color:#333;')));
1375
a77141a7
DM
1376 foreach ($admins as $admin) {
1377 $message = new stdClass();
1378 $message->component = 'moodle';
1379 $message->name = 'availableupdate';
55079015 1380 $message->userfrom = get_admin();
a77141a7 1381 $message->userto = $admin;
2399585f 1382 $message->subject = get_string('updatenotificationsubject', 'core_admin', array('siteurl' => $CFG->wwwroot));
a77141a7
DM
1383 $message->fullmessage = $text;
1384 $message->fullmessageformat = FORMAT_PLAIN;
1385 $message->fullmessagehtml = $html;
cd89994d
DM
1386 $message->smallmessage = get_string('updatenotifications', 'core_admin');
1387 $message->notification = 1;
a77141a7
DM
1388 message_send($message);
1389 }
1390 }
b9934a17
DM
1391
1392 /**
4442cc80 1393 * Compare two release labels and decide if they are the same
b9934a17 1394 *
4442cc80
DM
1395 * @param string $remote release info of the available update
1396 * @param null|string $local release info of the local code, defaults to $release defined in version.php
1397 * @return boolean true if the releases declare the same minor+major version
b9934a17 1398 */
4442cc80 1399 protected function is_same_release($remote, $local=null) {
b9934a17 1400
4442cc80
DM
1401 if (is_null($local)) {
1402 $this->load_current_environment();
1403 $local = $this->currentrelease;
1404 }
0242bdc7 1405
4442cc80 1406 $pattern = '/^([0-9\.\+]+)([^(]*)/';
b9934a17 1407
4442cc80
DM
1408 preg_match($pattern, $remote, $remotematches);
1409 preg_match($pattern, $local, $localmatches);
b9934a17 1410
4442cc80
DM
1411 $remotematches[1] = str_replace('+', '', $remotematches[1]);
1412 $localmatches[1] = str_replace('+', '', $localmatches[1]);
1413
1414 if ($remotematches[1] === $localmatches[1] and rtrim($remotematches[2]) === rtrim($localmatches[2])) {
1415 return true;
1416 } else {
1417 return false;
1418 }
1419 }
cd0bb55f
DM
1420}
1421
1422
7d8de6d8
DM
1423/**
1424 * Defines the structure of objects returned by {@link available_update_checker::get_update_info()}
1425 */
1426class available_update_info {
1427
1428 /** @var string frankenstyle component name */
1429 public $component;
1430 /** @var int the available version of the component */
1431 public $version;
1432 /** @var string|null optional release name */
1433 public $release = null;
1434 /** @var int|null optional maturity info, eg {@link MATURITY_STABLE} */
1435 public $maturity = null;
1436 /** @var string|null optional URL of a page with more info about the update */
1437 public $url = null;
1438 /** @var string|null optional URL of a ZIP package that can be downloaded and installed */
1439 public $download = null;
6b75106a
DM
1440 /** @var string|null of self::download is set, then this must be the MD5 hash of the ZIP */
1441 public $downloadmd5 = null;
7d8de6d8
DM
1442
1443 /**
1444 * Creates new instance of the class
b9934a17 1445 *
7d8de6d8
DM
1446 * The $info array must provide at least the 'version' value and optionally all other
1447 * values to populate the object's properties.
b9934a17 1448 *
7d8de6d8
DM
1449 * @param string $name the frankenstyle component name
1450 * @param array $info associative array with other properties
1451 */
1452 public function __construct($name, array $info) {
1453 $this->component = $name;
1454 foreach ($info as $k => $v) {
1455 if (property_exists('available_update_info', $k) and $k != 'component') {
1456 $this->$k = $v;
1457 }
1458 }
1459 }
1460}
1461
1462
7683e550
DM
1463/**
1464 * Implements a communication bridge to the mdeploy.php utility
1465 */
1466class available_update_deployer {
1467
1468 const HTTP_PARAM_PREFIX = 'updteautodpldata_'; // Hey, even Google has not heard of such a prefix! So it MUST be safe :-p
1469 const HTTP_PARAM_CHECKER = 'datapackagesize'; // Name of the parameter that holds the number of items in the received data items
1470
1471 /** @var available_update_deployer holds the singleton instance */
1472 protected static $singletoninstance;
1473 /** @var moodle_url URL of a page that includes the deployer UI */
1474 protected $callerurl;
1475 /** @var moodle_url URL to return after the deployment */
1476 protected $returnurl;
1477
1478 /**
1479 * Direct instantiation not allowed, use the factory method {@link self::instance()}
1480 */
1481 protected function __construct() {
1482 }
1483
1484 /**
1485 * Sorry, this is singleton
1486 */
1487 protected function __clone() {
1488 }
1489
1490 /**
1491 * Factory method for this class
1492 *
1493 * @return available_update_deployer the singleton instance
1494 */
1495 public static function instance() {
1496 if (is_null(self::$singletoninstance)) {
1497 self::$singletoninstance = new self();
1498 }
1499 return self::$singletoninstance;
1500 }
1501
dc11af19
DM
1502 /**
1503 * Reset caches used by this script
1504 *
1505 * @param bool $phpunitreset is this called as a part of PHPUnit reset?
1506 */
1507 public static function reset_caches($phpunitreset = false) {
1508 if ($phpunitreset) {
1509 self::$singletoninstance = null;
1510 }
1511 }
1512
7683e550
DM
1513 /**
1514 * Is automatic deployment enabled?
1515 *
1516 * @return bool
1517 */
1518 public function enabled() {
1519 global $CFG;
1520
1521 if (!empty($CFG->disableupdateautodeploy)) {
1522 // The feature is prohibited via config.php
1523 return false;
1524 }
1525
1526 return get_config('updateautodeploy');
1527 }
1528
1529 /**
1530 * Sets some base properties of the class to make it usable.
1531 *
1532 * @param moodle_url $callerurl the base URL of a script that will handle the class'es form data
1533 * @param moodle_url $returnurl the final URL to return to when the deployment is finished
1534 */
1535 public function initialize(moodle_url $callerurl, moodle_url $returnurl) {
1536
1537 if (!$this->enabled()) {
1538 throw new coding_exception('Unable to initialize the deployer, the feature is not enabled.');
1539 }
1540
1541 $this->callerurl = $callerurl;
1542 $this->returnurl = $returnurl;
1543 }
1544
1545 /**
1546 * Has the deployer been initialized?
1547 *
1548 * Initialized deployer means that the following properties were set:
1549 * callerurl, returnurl
1550 *
1551 * @return bool
1552 */
1553 public function initialized() {
1554
1555 if (!$this->enabled()) {
1556 return false;
1557 }
1558
1559 if (empty($this->callerurl)) {
1560 return false;
1561 }
1562
1563 if (empty($this->returnurl)) {
1564 return false;
1565 }
1566
1567 return true;
1568 }
1569
1570 /**
0daa6428 1571 * Returns a list of reasons why the deployment can not happen
7683e550 1572 *
0daa6428
DM
1573 * If the returned array is empty, the deployment seems to be possible. The returned
1574 * structure is an associative array with keys representing individual impediments.
1575 * Possible keys are: missingdownloadurl, missingdownloadmd5, notwritable.
7683e550
DM
1576 *
1577 * @param available_update_info $info
0daa6428 1578 * @return array
7683e550 1579 */
0daa6428
DM
1580 public function deployment_impediments(available_update_info $info) {
1581
1582 $impediments = array();
7683e550
DM
1583
1584 if (empty($info->download)) {
0daa6428 1585 $impediments['missingdownloadurl'] = true;
7683e550
DM
1586 }
1587
6b75106a 1588 if (empty($info->downloadmd5)) {
0daa6428 1589 $impediments['missingdownloadmd5'] = true;
6b75106a
DM
1590 }
1591
30e26827
DM
1592 if (!empty($info->download) and !$this->update_downloadable($info->download)) {
1593 $impediments['notdownloadable'] = true;
1594 }
1595
0daa6428
DM
1596 if (!$this->component_writable($info->component)) {
1597 $impediments['notwritable'] = true;
1598 }
1599
1600 return $impediments;
7683e550
DM
1601 }
1602
08c3bc00
DM
1603 /**
1604 * Check to see if the current version of the plugin seems to be a checkout of an external repository.
1605 *
1606 * @param available_update_info $info
1607 * @return false|string
1608 */
1609 public function plugin_external_source(available_update_info $info) {
1610
1611 $paths = get_plugin_types(true);
1612 list($plugintype, $pluginname) = normalize_component($info->component);
1613 $pluginroot = $paths[$plugintype].'/'.$pluginname;
1614
1615 if (is_dir($pluginroot.'/.git')) {
1616 return 'git';
1617 }
1618
1619 if (is_dir($pluginroot.'/CVS')) {
1620 return 'cvs';
1621 }
1622
1623 if (is_dir($pluginroot.'/.svn')) {
1624 return 'svn';
1625 }
1626
1627 return false;
1628 }
1629
7683e550
DM
1630 /**
1631 * Prepares a renderable widget to confirm installation of an available update.
1632 *
1633 * @param available_update_info $info component version to deploy
1634 * @return renderable
1635 */
1636 public function make_confirm_widget(available_update_info $info) {
1637
1638 if (!$this->initialized()) {
1639 throw new coding_exception('Illegal method call - deployer not initialized.');
1640 }
1641
1642 $params = $this->data_to_params(array(
1643 'updateinfo' => (array)$info, // see http://www.php.net/manual/en/language.types.array.php#language.types.array.casting
1644 ));
1645
1646 $widget = new single_button(
1647 new moodle_url($this->callerurl, $params),
1648 get_string('updateavailableinstall', 'core_admin'),
1649 'post'
1650 );
1651
1652 return $widget;
1653 }
1654
1655 /**
1656 * Prepares a renderable widget to execute installation of an available update.
1657 *
1658 * @param available_update_info $info component version to deploy
1659 * @return renderable
1660 */
1661 public function make_execution_widget(available_update_info $info) {
1662 global $CFG;
1663
1664 if (!$this->initialized()) {
1665 throw new coding_exception('Illegal method call - deployer not initialized.');
1666 }
1667
1668 $pluginrootpaths = get_plugin_types(true);
1669
1670 list($plugintype, $pluginname) = normalize_component($info->component);
1671
1672 if (empty($pluginrootpaths[$plugintype])) {
1673 throw new coding_exception('Unknown plugin type root location', $plugintype);
1674 }
1675
3daedb5c
DM
1676 list($passfile, $password) = $this->prepare_authorization();
1677
23137c4a
DM
1678 $upgradeurl = new moodle_url('/admin');
1679
7683e550
DM
1680 $params = array(
1681 'upgrade' => true,
1682 'type' => $plugintype,
1683 'name' => $pluginname,
1684 'typeroot' => $pluginrootpaths[$plugintype],
4c72f555 1685 'package' => $info->download,
6b75106a 1686 'md5' => $info->downloadmd5,
7683e550
DM
1687 'dataroot' => $CFG->dataroot,
1688 'dirroot' => $CFG->dirroot,
3daedb5c
DM
1689 'passfile' => $passfile,
1690 'password' => $password,
23137c4a 1691 'returnurl' => $upgradeurl->out(true),
7683e550
DM
1692 );
1693
63def597 1694 if (!empty($CFG->proxyhost)) {
0dcae7cd
DP
1695 // MDL-36973 - Beware - we should call just !is_proxybypass() here. But currently, our
1696 // cURL wrapper class does not do it. So, to have consistent behaviour, we pass proxy
1697 // setting regardless the $CFG->proxybypass setting. Once the {@link curl} class is
1698 // fixed, the condition should be amended.
63def597
DM
1699 if (true or !is_proxybypass($info->download)) {
1700 if (empty($CFG->proxyport)) {
1701 $params['proxy'] = $CFG->proxyhost;
1702 } else {
1703 $params['proxy'] = $CFG->proxyhost.':'.$CFG->proxyport;
1704 }
1705
1706 if (!empty($CFG->proxyuser) and !empty($CFG->proxypassword)) {
1707 $params['proxyuserpwd'] = $CFG->proxyuser.':'.$CFG->proxypassword;
1708 }
1709
1710 if (!empty($CFG->proxytype)) {
1711 $params['proxytype'] = $CFG->proxytype;
1712 }
1713 }
1714 }
1715
7683e550
DM
1716 $widget = new single_button(
1717 new moodle_url('/mdeploy.php', $params),
1718 get_string('updateavailableinstall', 'core_admin'),
1719 'post'
1720 );
1721
1722 return $widget;
1723 }
1724
1725 /**
1726 * Returns array of data objects passed to this tool.
1727 *
1728 * @return array
1729 */
1730 public function submitted_data() {
1731
1732 $data = $this->params_to_data($_POST);
1733
1734 if (empty($data) or empty($data[self::HTTP_PARAM_CHECKER])) {
1735 return false;
1736 }
1737
1738 if (!empty($data['updateinfo']) and is_object($data['updateinfo'])) {
1739 $updateinfo = $data['updateinfo'];
1740 if (!empty($updateinfo->component) and !empty($updateinfo->version)) {
1741 $data['updateinfo'] = new available_update_info($updateinfo->component, (array)$updateinfo);
1742 }
1743 }
1744
1745 if (!empty($data['callerurl'])) {
1746 $data['callerurl'] = new moodle_url($data['callerurl']);
1747 }
1748
1749 if (!empty($data['returnurl'])) {
1750 $data['returnurl'] = new moodle_url($data['returnurl']);
1751 }
1752
1753 return $data;
1754 }
1755
1756 /**
1757 * Handles magic getters and setters for protected properties.
1758 *
1759 * @param string $name method name, e.g. set_returnurl()
1760 * @param array $arguments arguments to be passed to the array
1761 */
1762 public function __call($name, array $arguments = array()) {
1763
1764 if (substr($name, 0, 4) === 'set_') {
1765 $property = substr($name, 4);
1766 if (empty($property)) {
1767 throw new coding_exception('Invalid property name (empty)');
1768 }
1769 if (empty($arguments)) {
1770 $arguments = array(true); // Default value for flag-like properties.
1771 }
1772 // Make sure it is a protected property.
1773 $isprotected = false;
1774 $reflection = new ReflectionObject($this);
1775 foreach ($reflection->getProperties(ReflectionProperty::IS_PROTECTED) as $reflectionproperty) {
1776 if ($reflectionproperty->getName() === $property) {
1777 $isprotected = true;
1778 break;
1779 }
1780 }
1781 if (!$isprotected) {
1782 throw new coding_exception('Unable to set property - it does not exist or it is not protected');
1783 }
1784 $value = reset($arguments);
1785 $this->$property = $value;
1786 return;
1787 }
1788
1789 if (substr($name, 0, 4) === 'get_') {
1790 $property = substr($name, 4);
1791 if (empty($property)) {
1792 throw new coding_exception('Invalid property name (empty)');
1793 }
1794 if (!empty($arguments)) {
1795 throw new coding_exception('No parameter expected');
1796 }
1797 // Make sure it is a protected property.
1798 $isprotected = false;
1799 $reflection = new ReflectionObject($this);
1800 foreach ($reflection->getProperties(ReflectionProperty::IS_PROTECTED) as $reflectionproperty) {
1801 if ($reflectionproperty->getName() === $property) {
1802 $isprotected = true;
1803 break;
1804 }
1805 }
1806 if (!$isprotected) {
1807 throw new coding_exception('Unable to get property - it does not exist or it is not protected');
1808 }
1809 return $this->$property;
1810 }
1811 }
1812
3daedb5c
DM
1813 /**
1814 * Generates a random token and stores it in a file in moodledata directory.
1815 *
1816 * @return array of the (string)filename and (string)password in this order
1817 */
1818 public function prepare_authorization() {
1819 global $CFG;
1820
1821 make_upload_directory('mdeploy/auth/');
1822
1823 $attempts = 0;
1824 $success = false;
1825
1826 while (!$success and $attempts < 5) {
1827 $attempts++;
1828
1829 $passfile = $this->generate_passfile();
1830 $password = $this->generate_password();
1831 $now = time();
1832
1833 $filepath = $CFG->dataroot.'/mdeploy/auth/'.$passfile;
1834
1835 if (!file_exists($filepath)) {
1836 $success = file_put_contents($filepath, $password . PHP_EOL . $now . PHP_EOL, LOCK_EX);
1837 }
1838 }
1839
1840 if ($success) {
1841 return array($passfile, $password);
1842
1843 } else {
1844 throw new moodle_exception('unable_prepare_authorization', 'core_plugin');
1845 }
1846 }
1847
7683e550
DM
1848 // End of external API
1849
1850 /**
1851 * Prepares an array of HTTP parameters that can be passed to another page.
1852 *
1853 * @param array|object $data associative array or an object holding the data, data JSON-able
1854 * @return array suitable as a param for moodle_url
1855 */
1856 protected function data_to_params($data) {
1857
1858 // Append some our own data
1859 if (!empty($this->callerurl)) {
1860 $data['callerurl'] = $this->callerurl->out(false);
1861 }
1862 if (!empty($this->callerurl)) {
1863 $data['returnurl'] = $this->returnurl->out(false);
1864 }
1865
1866 // Finally append the count of items in the package.
1867 $data[self::HTTP_PARAM_CHECKER] = count($data);
1868
1869 // Generate params
1870 $params = array();
1871 foreach ($data as $name => $value) {
1872 $transname = self::HTTP_PARAM_PREFIX.$name;
1873 $transvalue = json_encode($value);
1874 $params[$transname] = $transvalue;
1875 }
1876
1877 return $params;
1878 }
1879
1880 /**
1881 * Converts HTTP parameters passed to the script into native PHP data
1882 *
1883 * @param array $params such as $_REQUEST or $_POST
1884 * @return array data passed for this class
1885 */
1886 protected function params_to_data(array $params) {
1887
1888 if (empty($params)) {
1889 return array();
1890 }
1891
1892 $data = array();
1893 foreach ($params as $name => $value) {
1894 if (strpos($name, self::HTTP_PARAM_PREFIX) === 0) {
1895 $realname = substr($name, strlen(self::HTTP_PARAM_PREFIX));
1896 $realvalue = json_decode($value);
1897 $data[$realname] = $realvalue;
1898 }
1899 }
1900
1901 return $data;
1902 }
3daedb5c
DM
1903
1904 /**
1905 * Returns a random string to be used as a filename of the password storage.
1906 *
1907 * @return string
1908 */
1909 protected function generate_passfile() {
1910 return clean_param(uniqid('mdeploy_', true), PARAM_FILE);
1911 }
1912
1913 /**
1914 * Returns a random string to be used as the authorization token
1915 *
1916 * @return string
1917 */
1918 protected function generate_password() {
1919 return complex_random_string();
1920 }
0daa6428
DM
1921
1922 /**
1923 * Checks if the given component's directory is writable
1924 *
1925 * For the purpose of the deployment, the web server process has to have
1926 * write access to all files in the component's directory (recursively) and for the
1927 * directory itself.
1928 *
1929 * @see worker::move_directory_source_precheck()
1930 * @param string $component normalized component name
1931 * @return boolean
1932 */
1933 protected function component_writable($component) {
1934
1935 list($plugintype, $pluginname) = normalize_component($component);
1936
1937 $directory = get_plugin_directory($plugintype, $pluginname);
1938
1939 if (is_null($directory)) {
1940 throw new coding_exception('Unknown component location', $component);
1941 }
1942
1943 return $this->directory_writable($directory);
1944 }
1945
30e26827
DM
1946 /**
1947 * Checks if the mdeploy.php will be able to fetch the ZIP from the given URL
1948 *
1949 * This is mainly supposed to check if the transmission over HTTPS would
1950 * work. That is, if the CA certificates are present at the server.
1951 *
1952 * @param string $downloadurl the URL of the ZIP package to download
1953 * @return bool
1954 */
1955 protected function update_downloadable($downloadurl) {
1956 global $CFG;
1957
1958 $curloptions = array(
1959 'CURLOPT_SSL_VERIFYHOST' => 2, // this is the default in {@link curl} class but just in case
1960 'CURLOPT_SSL_VERIFYPEER' => true,
1961 );
1962
1963 $cacertfile = $CFG->dataroot.'/moodleorgca.crt';
1964 if (is_readable($cacertfile)) {
1965 // Do not use CA certs provided by the operating system. Instead,
1966 // use this CA cert to verify the updates provider.
1967 $curloptions['CURLOPT_CAINFO'] = $cacertfile;
1968 }
1969
1970 $curl = new curl(array('proxy' => true));
1971 $result = $curl->head($downloadurl, $curloptions);
1972 $errno = $curl->get_errno();
1973 if (empty($errno)) {
1974 return true;
1975 } else {
1976 return false;
1977 }
1978 }
1979
0daa6428
DM
1980 /**
1981 * Checks if the directory and all its contents (recursively) is writable
1982 *
1983 * @param string $path full path to a directory
1984 * @return boolean
1985 */
1986 private function directory_writable($path) {
1987
1988 if (!is_writable($path)) {
1989 return false;
1990 }
1991
1992 if (is_dir($path)) {
1993 $handle = opendir($path);
1994 } else {
1995 return false;
1996 }
1997
1998 $result = true;
1999
2000 while ($filename = readdir($handle)) {
2001 $filepath = $path.'/'.$filename;
2002
2003 if ($filename === '.' or $filename === '..') {
2004 continue;
2005 }
2006
2007 if (is_dir($filepath)) {
2008 $result = $result && $this->directory_writable($filepath);
2009
2010 } else {
2011 $result = $result && is_writable($filepath);
2012 }
2013 }
2014
2015 closedir($handle);
2016
2017 return $result;
2018 }
7683e550
DM
2019}
2020
2021
00ef3c3e
DM
2022/**
2023 * Factory class producing required subclasses of {@link plugininfo_base}
2024 */
2025class plugininfo_default_factory {
b9934a17
DM
2026
2027 /**
00ef3c3e 2028 * Makes a new instance of the plugininfo class
b9934a17 2029 *
00ef3c3e
DM
2030 * @param string $type the plugin type, eg. 'mod'
2031 * @param string $typerootdir full path to the location of all the plugins of this type
2032 * @param string $name the plugin name, eg. 'workshop'
2033 * @param string $namerootdir full path to the location of the plugin
2034 * @param string $typeclass the name of class that holds the info about the plugin
2035 * @return plugininfo_base the instance of $typeclass
2036 */
2037 public static function make($type, $typerootdir, $name, $namerootdir, $typeclass) {
2038 $plugin = new $typeclass();
2039 $plugin->type = $type;
2040 $plugin->typerootdir = $typerootdir;
2041 $plugin->name = $name;
2042 $plugin->rootdir = $namerootdir;
2043
2044 $plugin->init_display_name();
2045 $plugin->load_disk_version();
2046 $plugin->load_db_version();
2047 $plugin->load_required_main_version();
2048 $plugin->init_is_standard();
473289a0 2049
00ef3c3e
DM
2050 return $plugin;
2051 }
b9934a17
DM
2052}
2053
00ef3c3e 2054
b9934a17 2055/**
b6ad8594 2056 * Base class providing access to the information about a plugin
828788f0
TH
2057 *
2058 * @property-read string component the component name, type_name
b9934a17 2059 */
b6ad8594 2060abstract class plugininfo_base {
b9934a17
DM
2061
2062 /** @var string the plugintype name, eg. mod, auth or workshopform */
2063 public $type;
2064 /** @var string full path to the location of all the plugins of this type */
2065 public $typerootdir;
2066 /** @var string the plugin name, eg. assignment, ldap */
2067 public $name;
2068 /** @var string the localized plugin name */
2069 public $displayname;
2070 /** @var string the plugin source, one of plugin_manager::PLUGIN_SOURCE_xxx constants */
2071 public $source;
2072 /** @var fullpath to the location of this plugin */
2073 public $rootdir;
2074 /** @var int|string the version of the plugin's source code */
2075 public $versiondisk;
2076 /** @var int|string the version of the installed plugin */
2077 public $versiondb;
2078 /** @var int|float|string required version of Moodle core */
2079 public $versionrequires;
b6ad8594
DM
2080 /** @var array other plugins that this one depends on, lazy-loaded by {@link get_other_required_plugins()} */
2081 public $dependencies;
b9934a17
DM
2082 /** @var int number of instances of the plugin - not supported yet */
2083 public $instances;
2084 /** @var int order of the plugin among other plugins of the same type - not supported yet */
2085 public $sortorder;
7d8de6d8
DM
2086 /** @var array|null array of {@link available_update_info} for this plugin */
2087 public $availableupdates;
b9934a17
DM
2088
2089 /**
b6ad8594
DM
2090 * Gathers and returns the information about all plugins of the given type
2091 *
b6ad8594
DM
2092 * @param string $type the name of the plugintype, eg. mod, auth or workshopform
2093 * @param string $typerootdir full path to the location of the plugin dir
2094 * @param string $typeclass the name of the actually called class
2095 * @return array of plugintype classes, indexed by the plugin name
b9934a17
DM
2096 */
2097 public static function get_plugins($type, $typerootdir, $typeclass) {
2098
2099 // get the information about plugins at the disk
2100 $plugins = get_plugin_list($type);
2101 $ondisk = array();
2102 foreach ($plugins as $pluginname => $pluginrootdir) {
00ef3c3e
DM
2103 $ondisk[$pluginname] = plugininfo_default_factory::make($type, $typerootdir,
2104 $pluginname, $pluginrootdir, $typeclass);
b9934a17
DM
2105 }
2106 return $ondisk;
2107 }
2108
2109 /**
b6ad8594 2110 * Sets {@link $displayname} property to a localized name of the plugin
b9934a17 2111 */
b8343e68 2112 public function init_display_name() {
828788f0
TH
2113 if (!get_string_manager()->string_exists('pluginname', $this->component)) {
2114 $this->displayname = '[pluginname,' . $this->component . ']';
b9934a17 2115 } else {
828788f0
TH
2116 $this->displayname = get_string('pluginname', $this->component);
2117 }
2118 }
2119
2120 /**
2121 * Magic method getter, redirects to read only values.
b6ad8594 2122 *
828788f0
TH
2123 * @param string $name
2124 * @return mixed
2125 */
2126 public function __get($name) {
2127 switch ($name) {
2128 case 'component': return $this->type . '_' . $this->name;
2129
2130 default:
2131 debugging('Invalid plugin property accessed! '.$name);
2132 return null;
b9934a17
DM
2133 }
2134 }
2135
2136 /**
b6ad8594
DM
2137 * Return the full path name of a file within the plugin.
2138 *
2139 * No check is made to see if the file exists.
2140 *
2141 * @param string $relativepath e.g. 'version.php'.
2142 * @return string e.g. $CFG->dirroot . '/mod/quiz/version.php'.
b9934a17 2143 */
473289a0 2144 public function full_path($relativepath) {
b9934a17 2145 if (empty($this->rootdir)) {
473289a0 2146 return '';
b9934a17 2147 }
473289a0
TH
2148 return $this->rootdir . '/' . $relativepath;
2149 }
b9934a17 2150
473289a0
TH
2151 /**
2152 * Load the data from version.php.
b6ad8594
DM
2153 *
2154 * @return stdClass the object called $plugin defined in version.php
473289a0
TH
2155 */
2156 protected function load_version_php() {
2157 $versionfile = $this->full_path('version.php');
b9934a17 2158
473289a0 2159 $plugin = new stdClass();
b9934a17
DM
2160 if (is_readable($versionfile)) {
2161 include($versionfile);
b9934a17 2162 }
473289a0 2163 return $plugin;
b9934a17
DM
2164 }
2165
2166 /**
b6ad8594
DM
2167 * Sets {@link $versiondisk} property to a numerical value representing the
2168 * version of the plugin's source code.
2169 *
2170 * If the value is null after calling this method, either the plugin
2171 * does not use versioning (typically does not have any database
2172 * data) or is missing from disk.
b9934a17 2173 */
473289a0
TH
2174 public function load_disk_version() {
2175 $plugin = $this->load_version_php();
2176 if (isset($plugin->version)) {
2177 $this->versiondisk = $plugin->version;
b9934a17
DM
2178 }
2179 }
2180
2181 /**
b6ad8594
DM
2182 * Sets {@link $versionrequires} property to a numerical value representing
2183 * the version of Moodle core that this plugin requires.
b9934a17 2184 */
b8343e68 2185 public function load_required_main_version() {
473289a0
TH
2186 $plugin = $this->load_version_php();
2187 if (isset($plugin->requires)) {
2188 $this->versionrequires = $plugin->requires;
b9934a17 2189 }
473289a0 2190 }
b9934a17 2191
0242bdc7 2192 /**
777781d1 2193 * Initialise {@link $dependencies} to the list of other plugins (in any)
0242bdc7
TH
2194 * that this one requires to be installed.
2195 */
2196 protected function load_other_required_plugins() {
2197 $plugin = $this->load_version_php();
777781d1
TH
2198 if (!empty($plugin->dependencies)) {
2199 $this->dependencies = $plugin->dependencies;
0242bdc7 2200 } else {
777781d1 2201 $this->dependencies = array(); // By default, no dependencies.
0242bdc7
TH
2202 }
2203 }
2204
2205 /**
b6ad8594
DM
2206 * Get the list of other plugins that this plugin requires to be installed.
2207 *
2208 * @return array with keys the frankenstyle plugin name, and values either
2209 * a version string (like '2011101700') or the constant ANY_VERSION.
0242bdc7
TH
2210 */
2211 public function get_other_required_plugins() {
777781d1 2212 if (is_null($this->dependencies)) {
0242bdc7
TH
2213 $this->load_other_required_plugins();
2214 }
777781d1 2215 return $this->dependencies;
0242bdc7
TH
2216 }
2217
473289a0 2218 /**
b6ad8594
DM
2219 * Sets {@link $versiondb} property to a numerical value representing the
2220 * currently installed version of the plugin.
2221 *
2222 * If the value is null after calling this method, either the plugin
2223 * does not use versioning (typically does not have any database
2224 * data) or has not been installed yet.
473289a0
TH
2225 */
2226 public function load_db_version() {
828788f0 2227 if ($ver = self::get_version_from_config_plugins($this->component)) {
473289a0 2228 $this->versiondb = $ver;
b9934a17
DM
2229 }
2230 }
2231
2232 /**
b6ad8594
DM
2233 * Sets {@link $source} property to one of plugin_manager::PLUGIN_SOURCE_xxx
2234 * constants.
2235 *
2236 * If the property's value is null after calling this method, then
2237 * the type of the plugin has not been recognized and you should throw
2238 * an exception.
b9934a17 2239 */
b8343e68 2240 public function init_is_standard() {
b9934a17
DM
2241
2242 $standard = plugin_manager::standard_plugins_list($this->type);
2243
2244 if ($standard !== false) {
2245 $standard = array_flip($standard);
2246 if (isset($standard[$this->name])) {
2247 $this->source = plugin_manager::PLUGIN_SOURCE_STANDARD;
ec8935f5
PS
2248 } else if (!is_null($this->versiondb) and is_null($this->versiondisk)
2249 and plugin_manager::is_deleted_standard_plugin($this->type, $this->name)) {
2250 $this->source = plugin_manager::PLUGIN_SOURCE_STANDARD; // to be deleted
b9934a17
DM
2251 } else {
2252 $this->source = plugin_manager::PLUGIN_SOURCE_EXTENSION;
2253 }
2254 }
2255 }
2256
2257 /**
b6ad8594
DM
2258 * Returns true if the plugin is shipped with the official distribution
2259 * of the current Moodle version, false otherwise.
2260 *
2261 * @return bool
b9934a17
DM
2262 */
2263 public function is_standard() {
2264 return $this->source === plugin_manager::PLUGIN_SOURCE_STANDARD;
2265 }
2266
3a2300f5
DM
2267 /**
2268 * Returns true if the the given Moodle version is enough to run this plugin
2269 *
2270 * @param string|int|double $moodleversion
2271 * @return bool
2272 */
2273 public function is_core_dependency_satisfied($moodleversion) {
2274
2275 if (empty($this->versionrequires)) {
2276 return true;
2277
2278 } else {
2279 return (double)$this->versionrequires <= (double)$moodleversion;
2280 }
2281 }
2282
b9934a17 2283 /**
b6ad8594
DM
2284 * Returns the status of the plugin
2285 *
2286 * @return string one of plugin_manager::PLUGIN_STATUS_xxx constants
b9934a17
DM
2287 */
2288 public function get_status() {
2289
2290 if (is_null($this->versiondb) and is_null($this->versiondisk)) {
2291 return plugin_manager::PLUGIN_STATUS_NODB;
2292
2293 } else if (is_null($this->versiondb) and !is_null($this->versiondisk)) {
2294 return plugin_manager::PLUGIN_STATUS_NEW;
2295
2296 } else if (!is_null($this->versiondb) and is_null($this->versiondisk)) {
ec8935f5
PS
2297 if (plugin_manager::is_deleted_standard_plugin($this->type, $this->name)) {
2298 return plugin_manager::PLUGIN_STATUS_DELETE;
2299 } else {
2300 return plugin_manager::PLUGIN_STATUS_MISSING;
2301 }
b9934a17
DM
2302
2303 } else if ((string)$this->versiondb === (string)$this->versiondisk) {
2304 return plugin_manager::PLUGIN_STATUS_UPTODATE;
2305
2306 } else if ($this->versiondb < $this->versiondisk) {
2307 return plugin_manager::PLUGIN_STATUS_UPGRADE;
2308
2309 } else if ($this->versiondb > $this->versiondisk) {
2310 return plugin_manager::PLUGIN_STATUS_DOWNGRADE;
2311
2312 } else {
2313 // $version = pi(); and similar funny jokes - hopefully Donald E. Knuth will never contribute to Moodle ;-)
2314 throw new coding_exception('Unable to determine plugin state, check the plugin versions');
2315 }
2316 }
2317
2318 /**
b6ad8594
DM
2319 * Returns the information about plugin availability
2320 *
2321 * True means that the plugin is enabled. False means that the plugin is
2322 * disabled. Null means that the information is not available, or the
2323 * plugin does not support configurable availability or the availability
2324 * can not be changed.
2325 *
2326 * @return null|bool
b9934a17
DM
2327 */
2328 public function is_enabled() {
2329 return null;
2330 }
2331
2332 /**
7d8de6d8 2333 * Populates the property {@link $availableupdates} with the information provided by
dd119e21
DM
2334 * available update checker
2335 *
2336 * @param available_update_checker $provider the class providing the available update info
2337 */
7d8de6d8 2338 public function check_available_updates(available_update_checker $provider) {
c6f008e7
DM
2339 global $CFG;
2340
2341 if (isset($CFG->updateminmaturity)) {
2342 $minmaturity = $CFG->updateminmaturity;
2343 } else {
2344 // this can happen during the very first upgrade to 2.3
2345 $minmaturity = MATURITY_STABLE;
2346 }
2347
2348 $this->availableupdates = $provider->get_update_info($this->component,
2349 array('minmaturity' => $minmaturity));
dd119e21
DM
2350 }
2351
d26f3ddd 2352 /**
7d8de6d8 2353 * If there are updates for this plugin available, returns them.
d26f3ddd 2354 *
7d8de6d8
DM
2355 * Returns array of {@link available_update_info} objects, if some update
2356 * is available. Returns null if there is no update available or if the update
2357 * availability is unknown.
d26f3ddd 2358 *
7d8de6d8 2359 * @return array|null
d26f3ddd 2360 */
7d8de6d8 2361 public function available_updates() {
dd119e21 2362
7d8de6d8 2363 if (empty($this->availableupdates) or !is_array($this->availableupdates)) {
dd119e21
DM
2364 return null;
2365 }
2366
7d8de6d8
DM
2367 $updates = array();
2368
2369 foreach ($this->availableupdates as $availableupdate) {
2370 if ($availableupdate->version > $this->versiondisk) {
2371 $updates[] = $availableupdate;
2372 }
2373 }
2374
2375 if (empty($updates)) {
2376 return null;
dd119e21
DM
2377 }
2378
7d8de6d8 2379 return $updates;
d26f3ddd
DM
2380 }
2381
5cdb1893
MG
2382 /**
2383 * Returns the node name used in admin settings menu for this plugin settings (if applicable)
2384 *
2385 * @return null|string node name or null if plugin does not create settings node (default)
2386 */
2387 public function get_settings_section_name() {
2388 return null;
2389 }
2390
b9934a17 2391 /**
b6ad8594
DM
2392 * Returns the URL of the plugin settings screen
2393 *
2394 * Null value means that the plugin either does not have the settings screen
2395 * or its location is not available via this library.
2396 *
2397 * @return null|moodle_url
b9934a17
DM
2398 */
2399 public function get_settings_url() {
5cdb1893
MG
2400 $section = $this->get_settings_section_name();
2401 if ($section === null) {
2402 return null;
2403 }
2404 $settings = admin_get_root()->locate($section);
2405 if ($settings && $settings instanceof admin_settingpage) {
2406 return new moodle_url('/admin/settings.php', array('section' => $section));
2407 } else if ($settings && $settings instanceof admin_externalpage) {
2408 return new moodle_url($settings->url);
2409 } else {
2410 return null;
2411 }
2412 }
2413
2414 /**
2415 * Loads plugin settings to the settings tree
2416 *
2417 * This function usually includes settings.php file in plugins folder.
2418 * Alternatively it can create a link to some settings page (instance of admin_externalpage)
2419 *
2420 * @param part_of_admin_tree $adminroot
2421 * @param string $parentnodename
2422 * @param bool $hassiteconfig whether the current user has moodle/site:config capability
2423 */
2424 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
b9934a17
DM
2425 }
2426
2427 /**
b6ad8594
DM
2428 * Returns the URL of the screen where this plugin can be uninstalled
2429 *
2430 * Visiting that URL must be safe, that is a manual confirmation is needed
2431 * for actual uninstallation of the plugin. Null value means that the
2432 * plugin either does not support uninstallation, or does not require any
2433 * database cleanup or the location of the screen is not available via this
2434 * library.
2435 *
2436 * @return null|moodle_url
b9934a17
DM
2437 */
2438 public function get_uninstall_url() {
2439 return null;
2440 }
2441
2442 /**
b6ad8594
DM
2443 * Returns relative directory of the plugin with heading '/'
2444 *
2445 * @return string
b9934a17
DM
2446 */
2447 public function get_dir() {
2448 global $CFG;
2449
2450 return substr($this->rootdir, strlen($CFG->dirroot));
2451 }
2452
2453 /**
2454 * Provides access to plugin versions from {config_plugins}
2455 *
2456 * @param string $plugin plugin name
2457 * @param double $disablecache optional, defaults to false
2458 * @return int|false the stored value or false if not found
2459 */
2460 protected function get_version_from_config_plugins($plugin, $disablecache=false) {
2461 global $DB;
2462 static $pluginversions = null;
2463
2464 if (is_null($pluginversions) or $disablecache) {
f433088d
PS
2465 try {
2466 $pluginversions = $DB->get_records_menu('config_plugins', array('name' => 'version'), 'plugin', 'plugin,value');
2467 } catch (dml_exception $e) {
2468 // before install
2469 $pluginversions = array();
2470 }
b9934a17
DM
2471 }
2472
2473 if (!array_key_exists($plugin, $pluginversions)) {
2474 return false;
2475 }
2476
2477 return $pluginversions[$plugin];
2478 }
2479}
2480
b6ad8594 2481
b9934a17
DM
2482/**
2483 * General class for all plugin types that do not have their own class
2484 */
b6ad8594 2485class plugininfo_general extends plugininfo_base {
b9934a17
DM
2486}
2487
b6ad8594 2488
b9934a17
DM
2489/**
2490 * Class for page side blocks
2491 */
b6ad8594 2492class plugininfo_block extends plugininfo_base {
b9934a17 2493
b9934a17
DM
2494 public static function get_plugins($type, $typerootdir, $typeclass) {
2495
2496 // get the information about blocks at the disk
2497 $blocks = parent::get_plugins($type, $typerootdir, $typeclass);
2498
2499 // add blocks missing from disk
2500 $blocksinfo = self::get_blocks_info();
2501 foreach ($blocksinfo as $blockname => $blockinfo) {
2502 if (isset($blocks[$blockname])) {
2503 continue;
2504 }
2505 $plugin = new $typeclass();
2506 $plugin->type = $type;
2507 $plugin->typerootdir = $typerootdir;
2508 $plugin->name = $blockname;
2509 $plugin->rootdir = null;
2510 $plugin->displayname = $blockname;
2511 $plugin->versiondb = $blockinfo->version;
b8343e68 2512 $plugin->init_is_standard();
b9934a17
DM
2513
2514 $blocks[$blockname] = $plugin;
2515 }
2516
2517 return $blocks;
2518 }
2519
870d4280
MG
2520 /**
2521 * Magic method getter, redirects to read only values.
2522 *
2523 * For block plugins pretends the object has 'visible' property for compatibility
2524 * with plugins developed for Moodle version below 2.4
2525 *
2526 * @param string $name
2527 * @return mixed
2528 */
2529 public function __get($name) {
2530 if ($name === 'visible') {
2531 debugging('This is now an instance of plugininfo_block, please use $block->is_enabled() instead of $block->visible', DEBUG_DEVELOPER);
2532 return ($this->is_enabled() !== false);
2533 }
2534 return parent::__get($name);
2535 }
2536
b8343e68 2537 public function init_display_name() {
b9934a17
DM
2538
2539 if (get_string_manager()->string_exists('pluginname', 'block_' . $this->name)) {
2540 $this->displayname = get_string('pluginname', 'block_' . $this->name);
2541
2542 } else if (($block = block_instance($this->name)) !== false) {
2543 $this->displayname = $block->get_title();
2544
2545 } else {
b8343e68 2546 parent::init_display_name();
b9934a17
DM
2547 }
2548 }
2549
b8343e68 2550 public function load_db_version() {
b9934a17
DM
2551 global $DB;
2552
2553 $blocksinfo = self::get_blocks_info();
2554 if (isset($blocksinfo[$this->name]->version)) {
2555 $this->versiondb = $blocksinfo[$this->name]->version;
2556 }
2557 }
2558
b9934a17
DM
2559 public function is_enabled() {
2560
2561 $blocksinfo = self::get_blocks_info();
2562 if (isset($blocksinfo[$this->name]->visible)) {
2563 if ($blocksinfo[$this->name]->visible) {
2564 return true;
2565 } else {
2566 return false;
2567 }
2568 } else {
2569 return parent::is_enabled();
2570 }
2571 }
2572
870d4280
MG
2573 public function get_settings_section_name() {
2574 return 'blocksetting' . $this->name;
2575 }
b9934a17 2576
870d4280
MG
2577 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
2578 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
2579 $ADMIN = $adminroot; // may be used in settings.php
2580 $block = $this; // also can be used inside settings.php
2581 $section = $this->get_settings_section_name();
b9934a17 2582
870d4280
MG
2583 if (!$hassiteconfig || (($blockinstance = block_instance($this->name)) === false)) {
2584 return;
2585 }
b9934a17 2586
870d4280
MG
2587 $settings = null;
2588 if ($blockinstance->has_config()) {
6740c605 2589 if (file_exists($this->full_path('settings.php'))) {
870d4280
MG
2590 $settings = new admin_settingpage($section, $this->displayname,
2591 'moodle/site:config', $this->is_enabled() === false);
2592 include($this->full_path('settings.php')); // this may also set $settings to null
b9934a17
DM
2593 } else {
2594 $blocksinfo = self::get_blocks_info();
870d4280
MG
2595 $settingsurl = new moodle_url('/admin/block.php', array('block' => $blocksinfo[$this->name]->id));
2596 $settings = new admin_externalpage($section, $this->displayname,
2597 $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
b9934a17 2598 }
870d4280
MG
2599 }
2600 if ($settings) {
2601 $ADMIN->add($parentnodename, $settings);
b9934a17
DM
2602 }
2603 }
2604
b9934a17
DM
2605 public function get_uninstall_url() {
2606
2607 $blocksinfo = self::get_blocks_info();
2608 return new moodle_url('/admin/blocks.php', array('delete' => $blocksinfo[$this->name]->id, 'sesskey' => sesskey()));
2609 }
2610
2611 /**
2612 * Provides access to the records in {block} table
2613 *
2614 * @param bool $disablecache do not use internal static cache
2615 * @return array array of stdClasses
2616 */
2617 protected static function get_blocks_info($disablecache=false) {
2618 global $DB;
2619 static $blocksinfocache = null;
2620
2621 if (is_null($blocksinfocache) or $disablecache) {
f433088d
PS
2622 try {
2623 $blocksinfocache = $DB->get_records('block', null, 'name', 'name,id,version,visible');
2624 } catch (dml_exception $e) {
2625 // before install
2626 $blocksinfocache = array();
2627 }
b9934a17
DM
2628 }
2629
2630 return $blocksinfocache;
2631 }
2632}
2633
b6ad8594 2634
b9934a17
DM
2635/**
2636 * Class for text filters
2637 */
b6ad8594 2638class plugininfo_filter extends plugininfo_base {
b9934a17 2639
b9934a17 2640 public static function get_plugins($type, $typerootdir, $typeclass) {
7c9b837e 2641 global $CFG, $DB;
b9934a17
DM
2642
2643 $filters = array();
2644
2645 // get the list of filters from both /filter and /mod location
2646 $installed = filter_get_all_installed();
2647
2648 foreach ($installed as $filterlegacyname => $displayname) {
2649 $plugin = new $typeclass();
2650 $plugin->type = $type;
2651 $plugin->typerootdir = $typerootdir;
2652 $plugin->name = self::normalize_legacy_name($filterlegacyname);
2653 $plugin->rootdir = $CFG->dirroot . '/' . $filterlegacyname;
2654 $plugin->displayname = $displayname;
2655
b8343e68
TH
2656 $plugin->load_disk_version();
2657 $plugin->load_db_version();
2658 $plugin->load_required_main_version();
2659 $plugin->init_is_standard();
b9934a17
DM
2660
2661 $filters[$plugin->name] = $plugin;
2662 }
2663
b9934a17 2664 $globalstates = self::get_global_states();
7c9b837e
DM
2665
2666 if ($DB->get_manager()->table_exists('filter_active')) {
2667 // if we're upgrading from 1.9, the table does not exist yet
2668 // if it does, make sure that all installed filters are registered
2669 $needsreload = false;
2670 foreach (array_keys($installed) as $filterlegacyname) {
2671 if (!isset($globalstates[self::normalize_legacy_name($filterlegacyname)])) {
2672 filter_set_global_state($filterlegacyname, TEXTFILTER_DISABLED);
2673 $needsreload = true;
2674 }
2675 }
2676 if ($needsreload) {
2677 $globalstates = self::get_global_states(true);
b9934a17 2678 }
b9934a17
DM
2679 }
2680
2681 // make sure that all registered filters are installed, just in case
2682 foreach ($globalstates as $name => $info) {
2683 if (!isset($filters[$name])) {
2684 // oops, there is a record in filter_active but the filter is not installed
2685 $plugin = new $typeclass();
2686 $plugin->type = $type;
2687 $plugin->typerootdir = $typerootdir;
2688 $plugin->name = $name;
2689 $plugin->rootdir = $CFG->dirroot . '/' . $info->legacyname;
2690 $plugin->displayname = $info->legacyname;
2691
b8343e68 2692 $plugin->load_db_version();
b9934a17
DM
2693
2694 if (is_null($plugin->versiondb)) {
2695 // this is a hack to stimulate 'Missing from disk' error
2696 // because $plugin->versiondisk will be null !== false
2697 $plugin->versiondb = false;
2698 }
2699
2700 $filters[$plugin->name] = $plugin;
2701 }
2702 }
2703
2704 return $filters;
2705 }
2706
b8343e68 2707 public function init_display_name() {
b9934a17
DM
2708 // do nothing, the name is set in self::get_plugins()
2709 }
2710
2711 /**
b6ad8594 2712 * @see load_version_php()
b9934a17 2713 */
473289a0 2714 protected function load_version_php() {
b9934a17 2715 if (strpos($this->name, 'mod_') === 0) {
473289a0
TH
2716 // filters bundled with modules do not have a version.php and so
2717 // do not provide their own versioning information.
2718 return new stdClass();
b9934a17 2719 }
473289a0 2720 return parent::load_version_php();
b9934a17
DM
2721 }
2722
b9934a17
DM
2723 public function is_enabled() {
2724
2725 $globalstates = self::get_global_states();
2726
2727 foreach ($globalstates as $filterlegacyname => $info) {
2728 $name = self::normalize_legacy_name($filterlegacyname);
2729 if ($name === $this->name) {
2730 if ($info->active == TEXTFILTER_DISABLED) {
2731 return false;
2732 } else {
2733 // it may be 'On' or 'Off, but available'
2734 return null;
2735 }
2736 }
2737 }
2738
2739 return null;
2740 }
2741
1de1a666 2742 public function get_settings_section_name() {
b9934a17 2743 $globalstates = self::get_global_states();
dddbbac3
MG
2744 if (!isset($globalstates[$this->name])) {
2745 return parent::get_settings_section_name();
2746 }
b9934a17 2747 $legacyname = $globalstates[$this->name]->legacyname;
1de1a666
MG
2748 return 'filtersetting' . str_replace('/', '', $legacyname);
2749 }
2750
2751 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
2752 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
2753 $ADMIN = $adminroot; // may be used in settings.php
2754 $filter = $this; // also can be used inside settings.php
2755
dddbbac3 2756 $globalstates = self::get_global_states();
1de1a666 2757 $settings = null;
dddbbac3 2758 if ($hassiteconfig && isset($globalstates[$this->name]) && file_exists($this->full_path('filtersettings.php'))) {
1de1a666
MG
2759 $section = $this->get_settings_section_name();
2760 $settings = new admin_settingpage($section, $this->displayname,
2761 'moodle/site:config', $this->is_enabled() === false);
2762 include($this->full_path('filtersettings.php')); // this may also set $settings to null
2763 }
2764 if ($settings) {
2765 $ADMIN->add($parentnodename, $settings);
b9934a17
DM
2766 }
2767 }
2768
b9934a17
DM
2769 public function get_uninstall_url() {
2770
2771 if (strpos($this->name, 'mod_') === 0) {
2772 return null;
2773 } else {
2774 $globalstates = self::get_global_states();
2775 $legacyname = $globalstates[$this->name]->legacyname;
2776 return new moodle_url('/admin/filters.php', array('sesskey' => sesskey(), 'filterpath' => $legacyname, 'action' => 'delete'));
2777 }
2778 }
2779
2780 /**
2781 * Convert legacy filter names like 'filter/foo' or 'mod/bar' into frankenstyle
2782 *
2783 * @param string $legacyfiltername legacy filter name
2784 * @return string frankenstyle-like name
2785 */
2786 protected static function normalize_legacy_name($legacyfiltername) {
2787
2788 $name = str_replace('/', '_', $legacyfiltername);
2789 if (strpos($name, 'filter_') === 0) {
2790 $name = substr($name, 7);
2791 if (empty($name)) {
2792 throw new coding_exception('Unable to determine filter name: ' . $legacyfiltername);
2793 }
2794 }
2795
2796 return $name;
2797 }
2798
2799 /**
2800 * Provides access to the results of {@link filter_get_global_states()}
2801 * but indexed by the normalized filter name
2802 *
2803 * The legacy filter name is available as ->legacyname property.
2804 *
2805 * @param bool $disablecache
2806 * @return array
2807 */
2808 protected static function get_global_states($disablecache=false) {
2809 global $DB;
2810 static $globalstatescache = null;
2811
2812 if ($disablecache or is_null($globalstatescache)) {
2813
2814 if (!$DB->get_manager()->table_exists('filter_active')) {
2815 // we're upgrading from 1.9 and the table used by {@link filter_get_global_states()}
2816 // does not exist yet
2817 $globalstatescache = array();
2818
2819 } else {
2820 foreach (filter_get_global_states() as $legacyname => $info) {
2821 $name = self::normalize_legacy_name($legacyname);
2822 $filterinfo = new stdClass();
2823 $filterinfo->legacyname = $legacyname;
2824 $filterinfo->active = $info->active;
2825 $filterinfo->sortorder = $info->sortorder;
2826 $globalstatescache[$name] = $filterinfo;
2827 }
2828 }
2829 }
2830
2831 return $globalstatescache;
2832 }
2833}
2834
b6ad8594 2835
b9934a17
DM
2836/**
2837 * Class for activity modules
2838 */
b6ad8594 2839class plugininfo_mod extends plugininfo_base {
b9934a17 2840
b9934a17
DM
2841 public static function get_plugins($type, $typerootdir, $typeclass) {
2842
2843 // get the information about plugins at the disk
2844 $modules = parent::get_plugins($type, $typerootdir, $typeclass);
2845
2846 // add modules missing from disk
2847 $modulesinfo = self::get_modules_info();
2848 foreach ($modulesinfo as $modulename => $moduleinfo) {
2849 if (isset($modules[$modulename])) {
2850 continue;
2851 }
2852 $plugin = new $typeclass();
2853 $plugin->type = $type;
2854 $plugin->typerootdir = $typerootdir;
2855 $plugin->name = $modulename;
2856 $plugin->rootdir = null;
2857 $plugin->displayname = $modulename;
2858 $plugin->versiondb = $moduleinfo->version;
b8343e68 2859 $plugin->init_is_standard();
b9934a17
DM
2860
2861 $modules[$modulename] = $plugin;
2862 }
2863
2864 return $modules;
2865 }
2866
fde6f79f
MG
2867 /**
2868 * Magic method getter, redirects to read only values.
2869 *
2870 * For module plugins we pretend the object has 'visible' property for compatibility
2871 * with plugins developed for Moodle version below 2.4
2872 *
2873 * @param string $name
2874 * @return mixed
2875 */
2876 public function __get($name) {
2877 if ($name === 'visible') {
2878 debugging('This is now an instance of plugininfo_mod, please use $module->is_enabled() instead of $module->visible', DEBUG_DEVELOPER);
2879 return ($this->is_enabled() !== false);
2880 }
2881 return parent::__get($name);
2882 }
2883
b8343e68 2884 public function init_display_name() {
828788f0
TH
2885 if (get_string_manager()->string_exists('pluginname', $this->component)) {
2886 $this->displayname = get_string('pluginname', $this->component);
b9934a17 2887 } else {
828788f0 2888 $this->displayname = get_string('modulename', $this->component);
b9934a17
DM
2889 }
2890 }
2891
2892 /**
473289a0
TH
2893 * Load the data from version.php.
2894 * @return object the data object defined in version.php.
b9934a17 2895 */
473289a0
TH
2896 protected function load_version_php() {
2897 $versionfile = $this->full_path('version.php');
b9934a17 2898
473289a0 2899 $module = new stdClass();
b9934a17
DM
2900 if (is_readable($versionfile)) {
2901 include($versionfile);
b9934a17 2902 }
473289a0 2903 return $module;
b9934a17
DM
2904 }
2905
b8343e68 2906 public function load_db_version() {
b9934a17
DM
2907 global $DB;
2908
2909 $modulesinfo = self::get_modules_info();
2910 if (isset($modulesinfo[$this->name]->version)) {
2911 $this->versiondb = $modulesinfo[$this->name]->version;
2912 }
2913 }
2914
b9934a17
DM
2915 public function is_enabled() {
2916
2917 $modulesinfo = self::get_modules_info();
2918 if (isset($modulesinfo[$this->name]->visible)) {
2919 if ($modulesinfo[$this->name]->visible) {
2920 return true;
2921 } else {
2922 return false;
2923 }
2924 } else {
2925 return parent::is_enabled();
2926 }
2927 }
2928
fde6f79f
MG
2929 public function get_settings_section_name() {
2930 return 'modsetting' . $this->name;
2931 }
b9934a17 2932
fde6f79f
MG
2933 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
2934 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
2935 $ADMIN = $adminroot; // may be used in settings.php
2936 $module = $this; // also can be used inside settings.php
2937 $section = $this->get_settings_section_name();
2938
dddbbac3 2939 $modulesinfo = self::get_modules_info();
fde6f79f 2940 $settings = null;
dddbbac3 2941 if ($hassiteconfig && isset($modulesinfo[$this->name]) && file_exists($this->full_path('settings.php'))) {
fde6f79f
MG
2942 $settings = new admin_settingpage($section, $this->displayname,
2943 'moodle/site:config', $this->is_enabled() === false);
2944 include($this->full_path('settings.php')); // this may also set $settings to null
2945 }
2946 if ($settings) {
2947 $ADMIN->add($parentnodename, $settings);
b9934a17
DM
2948 }
2949 }
2950
b9934a17
DM
2951 public function get_uninstall_url() {
2952
2953 if ($this->name !== 'forum') {
2954 return new moodle_url('/admin/modules.php', array('delete' => $this->name, 'sesskey' => sesskey()));
2955 } else {
2956 return null;
2957 }
2958 }
2959
2960 /**
2961 * Provides access to the records in {modules} table
2962 *
2963 * @param bool $disablecache do not use internal static cache
2964 * @return array array of stdClasses
2965 */
2966 protected static function get_modules_info($disablecache=false) {
2967 global $DB;
2968 static $modulesinfocache = null;
2969
2970 if (is_null($modulesinfocache) or $disablecache) {
f433088d
PS
2971 try {
2972 $modulesinfocache = $DB->get_records('modules', null, 'name', 'name,id,version,visible');
2973 } catch (dml_exception $e) {
2974 // before install
2975 $modulesinfocache = array();
2976 }
b9934a17
DM
2977 }
2978
2979 return $modulesinfocache;
2980 }
2981}
2982
0242bdc7
TH
2983
2984/**
2985 * Class for question behaviours.
2986 */
b6ad8594
DM
2987class plugininfo_qbehaviour extends plugininfo_base {
2988
828788f0
TH
2989 public function get_uninstall_url() {
2990 return new moodle_url('/admin/qbehaviours.php',
2991 array('delete' => $this->name, 'sesskey' => sesskey()));
2992 }
0242bdc7
TH
2993}
2994
2995
b9934a17
DM
2996/**
2997 * Class for question types
2998 */
b6ad8594
DM
2999class plugininfo_qtype extends plugininfo_base {
3000
828788f0
TH
3001 public function get_uninstall_url() {
3002 return new moodle_url('/admin/qtypes.php',
3003 array('delete' => $this->name, 'sesskey' => sesskey()));
3004 }
66f3684a
MG
3005
3006 public function get_settings_section_name() {
3007 return 'qtypesetting' . $this->name;
3008 }
3009
3010 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3011 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3012 $ADMIN = $adminroot; // may be used in settings.php
3013 $qtype = $this; // also can be used inside settings.php
3014 $section = $this->get_settings_section_name();
3015
3016 $settings = null;
837e1812
TH
3017 $systemcontext = context_system::instance();
3018 if (($hassiteconfig || has_capability('moodle/question:config', $systemcontext)) &&
3019 file_exists($this->full_path('settings.php'))) {
66f3684a 3020 $settings = new admin_settingpage($section, $this->displayname,
837e1812 3021 'moodle/question:config', $this->is_enabled() === false);
66f3684a
MG
3022 include($this->full_path('settings.php')); // this may also set $settings to null
3023 }
3024 if ($settings) {
3025 $ADMIN->add($parentnodename, $settings);
3026 }
3027 }
b9934a17
DM
3028}
3029
b9934a17
DM
3030
3031/**
3032 * Class for authentication plugins
3033 */
b6ad8594 3034class plugininfo_auth extends plugininfo_base {
b9934a17 3035
b9934a17
DM
3036 public function is_enabled() {
3037 global $CFG;
3038 /** @var null|array list of enabled authentication plugins */
3039 static $enabled = null;
3040
3041 if (in_array($this->name, array('nologin', 'manual'))) {
3042 // these two are always enabled and can't be disabled
3043 return null;
3044 }
3045
3046 if (is_null($enabled)) {
d5d181f5 3047 $enabled = array_flip(explode(',', $CFG->auth));
b9934a17
DM
3048 }
3049
3050 return isset($enabled[$this->name]);
3051 }
3052
cbe9f609
MG
3053 public function get_settings_section_name() {
3054 return 'authsetting' . $this->name;
3055 }
3056
3057 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3058 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3059 $ADMIN = $adminroot; // may be used in settings.php
3060 $auth = $this; // also to be used inside settings.php
3061 $section = $this->get_settings_section_name();
3062
3063 $settings = null;
3064 if ($hassiteconfig) {
3065 if (file_exists($this->full_path('settings.php'))) {
3066 // TODO: finish implementation of common settings - locking, etc.
3067 $settings = new admin_settingpage($section, $this->displayname,
3068 'moodle/site:config', $this->is_enabled() === false);
3069 include($this->full_path('settings.php')); // this may also set $settings to null
3070 } else {
3071 $settingsurl = new moodle_url('/admin/auth_config.php', array('auth' => $this->name));
3072 $settings = new admin_externalpage($section, $this->displayname,
3073 $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
3074 }
3075 }
3076 if ($settings) {
3077 $ADMIN->add($parentnodename, $settings);
b9934a17
DM
3078 }
3079 }
3080}
3081
b6ad8594 3082
b9934a17
DM
3083/**
3084 * Class for enrolment plugins
3085 */
b6ad8594 3086class plugininfo_enrol extends plugininfo_base {
b9934a17 3087
b9934a17
DM
3088 public function is_enabled() {
3089 global $CFG;
3090 /** @var null|array list of enabled enrolment plugins */
3091 static $enabled = null;
3092
b6ad8594
DM
3093 // We do not actually need whole enrolment classes here so we do not call
3094 // {@link enrol_get_plugins()}. Note that this may produce slightly different
3095 // results, for example if the enrolment plugin does not contain lib.php
3096 // but it is listed in $CFG->enrol_plugins_enabled
3097
b9934a17 3098 if (is_null($enabled)) {
d5d181f5 3099 $enabled = array_flip(explode(',', $CFG->enrol_plugins_enabled));
b9934a17
DM
3100 }
3101
3102 return isset($enabled[$this->name]);
3103 }
3104
79c5c3fa
MG
3105 public function get_settings_section_name() {
3106 return 'enrolsettings' . $this->name;
3107 }
b9934a17 3108
79c5c3fa
MG
3109 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3110 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3111 $ADMIN = $adminroot; // may be used in settings.php
3112 $enrol = $this; // also can be used inside settings.php
3113 $section = $this->get_settings_section_name();
3114
3115 $settings = null;
3116 if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3117 $settings = new admin_settingpage($section, $this->displayname,
3118 'moodle/site:config', $this->is_enabled() === false);
3119 include($this->full_path('settings.php')); // this may also set $settings to null
3120 }
3121 if ($settings) {
3122 $ADMIN->add($parentnodename, $settings);
b9934a17
DM
3123 }
3124 }
3125
b9934a17
DM
3126 public function get_uninstall_url() {
3127 return new moodle_url('/admin/enrol.php', array('action' => 'uninstall', 'enrol' => $this->name, 'sesskey' => sesskey()));
3128 }
3129}
3130
b6ad8594 3131
b9934a17
DM
3132/**
3133 * Class for messaging processors
3134 */
b6ad8594 3135class plugininfo_message extends plugininfo_base {
b9934a17 3136
e8d16932
MG
3137 public function get_settings_section_name() {
3138 return 'messagesetting' . $this->name;
3139 }
3140
3141 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3142 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3143 $ADMIN = $adminroot; // may be used in settings.php
3144 if (!$hassiteconfig) {
3145 return;
3146 }
3147 $section = $this->get_settings_section_name();
3148
3149 $settings = null;
bc795b98
RK
3150 $processors = get_message_processors();
3151 if (isset($processors[$this->name])) {
3152 $processor = $processors[$this->name];
3153 if ($processor->available && $processor->hassettings) {
e8d16932
MG
3154 $settings = new admin_settingpage($section, $this->displayname,
3155 'moodle/site:config', $this->is_enabled() === false);
3156 include($this->full_path('settings.php')); // this may also set $settings to null
bc795b98 3157 }
0210ce10 3158 }
e8d16932
MG
3159 if ($settings) {
3160 $ADMIN->add($parentnodename, $settings);
3161 }
b9934a17 3162 }
b9934a17 3163
bede23f7
RK
3164 /**
3165 * @see plugintype_interface::is_enabled()
3166 */
3167 public function is_enabled() {
3168 $processors = get_message_processors();
3169 if (isset($processors[$this->name])) {
3170 return $processors[$this->name]->configured && $processors[$this->name]->enabled;
0210ce10 3171 } else {
bede23f7
RK
3172 return parent::is_enabled();
3173 }
3174 }
3f9d9e28
RK
3175
3176 /**
3177 * @see plugintype_interface::get_uninstall_url()
3178 */
3179 public function get_uninstall_url() {
3180 $processors = get_message_processors();
3181 if (isset($processors[$this->name])) {
e8d16932 3182 return new moodle_url('/admin/message.php', array('uninstall' => $processors[$this->name]->id, 'sesskey' => sesskey()));
3f9d9e28
RK
3183 } else {
3184 return parent::get_uninstall_url();
0210ce10 3185 }
b9934a17
DM
3186 }
3187}
3188
b6ad8594 3189
b9934a17
DM
3190/**
3191 * Class for repositories
3192 */
b6ad8594 3193class plugininfo_repository extends plugininfo_base {
b9934a17 3194
b9934a17
DM
3195 public function is_enabled() {
3196
3197 $enabled = self::get_enabled_repositories();
3198
3199 return isset($enabled[$this->name]);
3200 }
3201
c517dd68
MG
3202 public function get_settings_section_name() {
3203 return 'repositorysettings'.$this->name;
3204 }
b9934a17 3205
c517dd68
MG
3206 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3207 if ($hassiteconfig && $this->is_enabled()) {
3208 // completely no access to repository setting when it is not enabled
3209 $sectionname = $this->get_settings_section_name();
3210 $settingsurl = new moodle_url('/admin/repository.php',
3211 array('sesskey' => sesskey(), 'action' => 'edit', 'repos' => $this->name));
3212 $settings = new admin_externalpage($sectionname, $this->displayname,
3213 $settingsurl, 'moodle/site:config', false);
3214 $adminroot->add($parentnodename, $settings);
b9934a17
DM
3215 }
3216 }
3217
3218 /**
3219 * Provides access to the records in {repository} table
3220 *
3221 * @param bool $disablecache do not use internal static cache
3222 * @return array array of stdClasses
3223 */
3224 protected static function get_enabled_repositories($disablecache=false) {
3225 global $DB;
3226 static $repositories = null;
3227
3228 if (is_null($repositories) or $disablecache) {
3229 $repositories = $DB->get_records('repository', null, 'type', 'type,visible,sortorder');
3230 }
3231
3232 return $repositories;
3233 }
3234}
3235
b6ad8594 3236
b9934a17
DM
3237/**
3238 * Class for portfolios
3239 */
b6ad8594 3240class plugininfo_portfolio extends plugininfo_base {
b9934a17 3241
b9934a17
DM
3242 public function is_enabled() {
3243
3244 $enabled = self::get_enabled_portfolios();
3245
3246 return isset($enabled[$this->name]);
3247 }
3248
3249 /**
3250 * Provides access to the records in {portfolio_instance} table
3251 *
3252 * @param bool $disablecache do not use internal static cache
3253 * @return array array of stdClasses
3254 */
3255 protected static function get_enabled_portfolios($disablecache=false) {
3256 global $DB;
3257 static $portfolios = null;
3258
3259 if (is_null($portfolios) or $disablecache) {
3260 $portfolios = array();
3261 $instances = $DB->get_recordset('portfolio_instance', null, 'plugin');
3262 foreach ($instances as $instance) {
3263 if (isset($portfolios[$instance->plugin])) {
3264 if ($instance->visible) {
3265 $portfolios[$instance->plugin]->visible = $instance->visible;
3266 }
3267 } else {
3268 $portfolios[$instance->plugin] = $instance;
3269 }
3270 }
3271 }
3272
3273 return $portfolios;
3274 }
3275}
3276
b6ad8594 3277
b9934a17
DM
3278/**
3279 * Class for themes
3280 */
b6ad8594 3281class plugininfo_theme extends plugininfo_base {
b9934a17 3282
b9934a17
DM
3283 public function is_enabled() {
3284 global $CFG;
3285
3286 if ((!empty($CFG->theme) and $CFG->theme === $this->name) or
3287 (!empty($CFG->themelegacy) and $CFG->themelegacy === $this->name)) {
3288 return true;
3289 } else {
3290 return parent::is_enabled();
3291 }
3292 }
3293}
3294
b6ad8594 3295
b9934a17
DM
3296/**
3297 * Class representing an MNet service
3298 */
b6ad8594 3299class plugininfo_mnetservice extends plugininfo_base {
b9934a17 3300
b9934a17
DM
3301 public function is_enabled() {
3302 global $CFG;
3303
3304 if (empty($CFG->mnet_dispatcher_mode) || $CFG->mnet_dispatcher_mode !== 'strict') {
3305 return false;
3306 } else {
3307 return parent::is_enabled();
3308 }
3309 }
3310}
3cdfaeef 3311
b6ad8594 3312
3cdfaeef
PS
3313/**
3314 * Class for admin tool plugins
3315 */
b6ad8594 3316class plugininfo_tool extends plugininfo_base {
3cdfaeef
PS
3317
3318 public function get_uninstall_url() {
3319 return new moodle_url('/admin/tools.php', array('delete' => $this->name, 'sesskey' => sesskey()));
3320 }
3321}
4f6bba20 3322
b6ad8594 3323
4f6bba20
PS
3324/**
3325 * Class for admin tool plugins
3326 */
b6ad8594 3327class plugininfo_report extends plugininfo_base {
4f6bba20
PS
3328
3329 public function get_uninstall_url() {
3330 return new moodle_url('/admin/reports.php', array('delete' => $this->name, 'sesskey' => sesskey()));
3331 }
3332}
888ce02a
RK
3333
3334
3335/**
3336 * Class for local plugins
3337 */
3338class plugininfo_local extends plugininfo_base {
3339
3340 public function get_uninstall_url() {
3341 return new moodle_url('/admin/localplugins.php', array('delete' => $this->name, 'sesskey' => sesskey()));
3342 }
888ce02a 3343}
888ce02a 3344
087001ee
MG
3345/**
3346 * Class for HTML editors
3347 */
3348class plugininfo_editor extends plugininfo_base {
3349
3350 public function get_settings_section_name() {
3351 return 'editorsettings' . $this->name;
3352 }
3353
3354 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3355 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3356 $ADMIN = $adminroot; // may be used in settings.php
3357 $editor = $this; // also can be used inside settings.php
3358 $section = $this->get_settings_section_name();
3359
3360 $settings = null;
3361 if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3362 $settings = new admin_settingpage($section, $this->displayname,
3363 'moodle/site:config', $this->is_enabled() === false);
3364 include($this->full_path('settings.php')); // this may also set $settings to null
3365 }
3366 if ($settings) {
3367 $ADMIN->add($parentnodename, $settings);
3368 }
3369 }
3370
3371 /**
3372 * Returns the information about plugin availability
3373 *
3374 * True means that the plugin is enabled. False means that the plugin is
3375 * disabled. Null means that the information is not available, or the
3376 * plugin does not support configurable availability or the availability
3377 * can not be changed.
3378 *
3379 * @return null|bool
3380 */
3381 public function is_enabled() {
3382 global $CFG;
3383 if (empty($CFG->texteditors)) {
3384 $CFG->texteditors = 'tinymce,textarea';
3385 }
3386 if (in_array($this->name, explode(',', $CFG->texteditors))) {
3387 return true;
3388 }
3389 return false;
3390 }
3391}
d98305bd
MG
3392
3393/**
3394 * Class for plagiarism plugins
3395 */
3396class plugininfo_plagiarism extends plugininfo_base {
3397
3398 public function get_settings_section_name() {
3399 return 'plagiarism'. $this->name;
3400 }
3401
3402 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3403 // plagiarism plugin just redirect to settings.php in the plugins directory
3404 if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3405 $section = $this->get_settings_section_name();
3406 $settingsurl = new moodle_url($this->get_dir().'/settings.php');
3407 $settings = new admin_externalpage($section, $this->displayname,
3408 $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
3409 $adminroot->add($parentnodename, $settings);
3410 }
3411 }
3412}
2567584d
MG
3413
3414/**
3415 * Class for webservice protocols
3416 */
3417class plugininfo_webservice extends plugininfo_base {
3418
3419 public function get_settings_section_name() {
3420 return 'webservicesetting' . $this->name;
3421 }
3422
3423 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3424 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3425 $ADMIN = $adminroot; // may be used in settings.php
3426 $webservice = $this; // also can be used inside settings.php
3427 $section = $this->get_settings_section_name();
3428
3429 $settings = null;
3430 if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3431 $settings = new admin_settingpage($section, $this->displayname,
3432 'moodle/site:config', $this->is_enabled() === false);
3433 include($this->full_path('settings.php')); // this may also set $settings to null
3434 }
3435 if ($settings) {
3436 $ADMIN->add($parentnodename, $settings);
888ce02a
RK
3437 }
3438 }
2567584d
MG
3439
3440 public function is_enabled() {
3441 global $CFG;
3442 if (empty($CFG->enablewebservices)) {
3443 return false;
3444 }
3445 $active_webservices = empty($CFG->webserviceprotocols) ? array() : explode(',', $CFG->webserviceprotocols);
3446 if (in_array($this->name, $active_webservices)) {
3447 return true;
3448 }
3449 return false;
3450 }
3451
3452 public function get_uninstall_url() {
3453 return new moodle_url('/admin/webservice/protocols.php',
3454 array('sesskey' => sesskey(), 'action' => 'uninstall', 'webservice' => $this->name));
3455 }
888ce02a 3456}
3776335c
MG
3457
3458/**
3459 * Class for course formats
3460 */
3461class plugininfo_format extends plugininfo_base {
3462
3463 /**
3464 * Gathers and returns the information about all plugins of the given type
3465 *
3466 * @param string $type the name of the plugintype, eg. mod, auth or workshopform
3467 * @param string $typerootdir full path to the location of the plugin dir
3468 * @param string $typeclass the name of the actually called class
3469 * @return array of plugintype classes, indexed by the plugin name
3470 */
3471 public static function get_plugins($type, $typerootdir, $typeclass) {
3472 global $CFG;
3473 $formats = parent::get_plugins($type, $typerootdir, $typeclass);
3474 require_once($CFG->dirroot.'/course/lib.php');
3475 $order = get_sorted_course_formats();
3476 $sortedformats = array();
3477 foreach ($order as $formatname) {
3478 $sortedformats[$formatname] = $formats[$formatname];
3479 }
3480 return $sortedformats;
3481 }
3482
3483 public function get_settings_section_name() {
3484 return 'formatsetting' . $this->name;
3485 }
3486
3487 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3488 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3489 $ADMIN = $adminroot; // also may be used in settings.php
3490 $section = $this->get_settings_section_name();
3491
3492 $settings = null;
3493 if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3494 $settings = new admin_settingpage($section, $this->displayname,
3495 'moodle/site:config', $this->is_enabled() === false);
3496 include($this->full_path('settings.php')); // this may also set $settings to null
3497 }
3498 if ($settings) {
3499 $ADMIN->add($parentnodename, $settings);
3500 }
3501 }
3502
3503 public function is_enabled() {
3504 return !get_config($this->component, 'disabled');
3505 }
3506
3507 public function get_uninstall_url() {
3508 if ($this->name !== get_config('moodlecourse', 'format') && $this->name !== 'site') {
3509 return new moodle_url('/admin/courseformats.php',
3510 array('sesskey' => sesskey(), 'action' => 'uninstall', 'format' => $this->name));
3511 }
3512 return parent::get_uninstall_url();
3513 }
3514}