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