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