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