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