MDL-39056 Use new version of API when fetching available updates info
[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
2504 * plugin either does not support uninstallation, or does not require any
2505 * database cleanup or the location of the screen is not available via this
2506 * library.
2507 *
2508 * @return null|moodle_url
b9934a17
DM
2509 */
2510 public function get_uninstall_url() {
2511 return null;
2512 }
2513
2514 /**
b6ad8594
DM
2515 * Returns relative directory of the plugin with heading '/'
2516 *
2517 * @return string
b9934a17
DM
2518 */
2519 public function get_dir() {
2520 global $CFG;
2521
2522 return substr($this->rootdir, strlen($CFG->dirroot));
2523 }
2524
2525 /**
b8a6f26e 2526 * Provides access to plugin versions from the {config_plugins} table
b9934a17
DM
2527 *
2528 * @param string $plugin plugin name
b8a6f26e
DM
2529 * @param bool $disablecache do not attempt to obtain data from the cache
2530 * @return int|bool the stored value or false if not found
b9934a17
DM
2531 */
2532 protected function get_version_from_config_plugins($plugin, $disablecache=false) {
2533 global $DB;
b9934a17 2534
ad3ed98b 2535 $cache = cache::make('core', 'plugininfo_base');
b8a6f26e
DM
2536
2537 $pluginversions = $cache->get('versions_db');
2538
2539 if ($pluginversions === false or $disablecache) {
f433088d
PS
2540 try {
2541 $pluginversions = $DB->get_records_menu('config_plugins', array('name' => 'version'), 'plugin', 'plugin,value');
2542 } catch (dml_exception $e) {
2543 // before install
2544 $pluginversions = array();
2545 }
b8a6f26e 2546 $cache->set('versions_db', $pluginversions);
b9934a17
DM
2547 }
2548
b8a6f26e
DM
2549 if (isset($pluginversions[$plugin])) {
2550 return $pluginversions[$plugin];
2551 } else {
b9934a17
DM
2552 return false;
2553 }
b9934a17
DM
2554 }
2555}
2556
b6ad8594 2557
b9934a17
DM
2558/**
2559 * General class for all plugin types that do not have their own class
2560 */
b6ad8594 2561class plugininfo_general extends plugininfo_base {
b9934a17
DM
2562}
2563
b6ad8594 2564
b9934a17
DM
2565/**
2566 * Class for page side blocks
2567 */
b6ad8594 2568class plugininfo_block extends plugininfo_base {
b9934a17 2569
b9934a17
DM
2570 public static function get_plugins($type, $typerootdir, $typeclass) {
2571
2572 // get the information about blocks at the disk
2573 $blocks = parent::get_plugins($type, $typerootdir, $typeclass);
2574
2575 // add blocks missing from disk
2576 $blocksinfo = self::get_blocks_info();
2577 foreach ($blocksinfo as $blockname => $blockinfo) {
2578 if (isset($blocks[$blockname])) {
2579 continue;
2580 }
2581 $plugin = new $typeclass();
2582 $plugin->type = $type;
2583 $plugin->typerootdir = $typerootdir;
2584 $plugin->name = $blockname;
2585 $plugin->rootdir = null;
2586 $plugin->displayname = $blockname;
2587 $plugin->versiondb = $blockinfo->version;
b8343e68 2588 $plugin->init_is_standard();
b9934a17
DM
2589
2590 $blocks[$blockname] = $plugin;
2591 }
2592
2593 return $blocks;
2594 }
2595
870d4280
MG
2596 /**
2597 * Magic method getter, redirects to read only values.
2598 *
2599 * For block plugins pretends the object has 'visible' property for compatibility
2600 * with plugins developed for Moodle version below 2.4
2601 *
2602 * @param string $name
2603 * @return mixed
2604 */
2605 public function __get($name) {
2606 if ($name === 'visible') {
2607 debugging('This is now an instance of plugininfo_block, please use $block->is_enabled() instead of $block->visible', DEBUG_DEVELOPER);
2608 return ($this->is_enabled() !== false);
2609 }
2610 return parent::__get($name);
2611 }
2612
b8343e68 2613 public function init_display_name() {
b9934a17
DM
2614
2615 if (get_string_manager()->string_exists('pluginname', 'block_' . $this->name)) {
2616 $this->displayname = get_string('pluginname', 'block_' . $this->name);
2617
2618 } else if (($block = block_instance($this->name)) !== false) {
2619 $this->displayname = $block->get_title();
2620
2621 } else {
b8343e68 2622 parent::init_display_name();
b9934a17
DM
2623 }
2624 }
2625
b8343e68 2626 public function load_db_version() {
b9934a17
DM
2627 global $DB;
2628
2629 $blocksinfo = self::get_blocks_info();
2630 if (isset($blocksinfo[$this->name]->version)) {
2631 $this->versiondb = $blocksinfo[$this->name]->version;
2632 }
2633 }
2634
b9934a17
DM
2635 public function is_enabled() {
2636
2637 $blocksinfo = self::get_blocks_info();
2638 if (isset($blocksinfo[$this->name]->visible)) {
2639 if ($blocksinfo[$this->name]->visible) {
2640 return true;
2641 } else {
2642 return false;
2643 }
2644 } else {
2645 return parent::is_enabled();
2646 }
2647 }
2648
870d4280
MG
2649 public function get_settings_section_name() {
2650 return 'blocksetting' . $this->name;
2651 }
b9934a17 2652
870d4280
MG
2653 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
2654 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
2655 $ADMIN = $adminroot; // may be used in settings.php
2656 $block = $this; // also can be used inside settings.php
2657 $section = $this->get_settings_section_name();
b9934a17 2658
870d4280
MG
2659 if (!$hassiteconfig || (($blockinstance = block_instance($this->name)) === false)) {
2660 return;
2661 }
b9934a17 2662
870d4280
MG
2663 $settings = null;
2664 if ($blockinstance->has_config()) {
6740c605 2665 if (file_exists($this->full_path('settings.php'))) {
870d4280
MG
2666 $settings = new admin_settingpage($section, $this->displayname,
2667 'moodle/site:config', $this->is_enabled() === false);
2668 include($this->full_path('settings.php')); // this may also set $settings to null
b9934a17
DM
2669 } else {
2670 $blocksinfo = self::get_blocks_info();
870d4280
MG
2671 $settingsurl = new moodle_url('/admin/block.php', array('block' => $blocksinfo[$this->name]->id));
2672 $settings = new admin_externalpage($section, $this->displayname,
2673 $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
b9934a17 2674 }
870d4280
MG
2675 }
2676 if ($settings) {
2677 $ADMIN->add($parentnodename, $settings);
b9934a17
DM
2678 }
2679 }
2680
b9934a17
DM
2681 public function get_uninstall_url() {
2682
2683 $blocksinfo = self::get_blocks_info();
2684 return new moodle_url('/admin/blocks.php', array('delete' => $blocksinfo[$this->name]->id, 'sesskey' => sesskey()));
2685 }
2686
2687 /**
2688 * Provides access to the records in {block} table
2689 *
b8a6f26e 2690 * @param bool $disablecache do not attempt to obtain data from the cache
b9934a17
DM
2691 * @return array array of stdClasses
2692 */
2693 protected static function get_blocks_info($disablecache=false) {
2694 global $DB;
b9934a17 2695
ad3ed98b 2696 $cache = cache::make('core', 'plugininfo_block');
b8a6f26e
DM
2697
2698 $blocktypes = $cache->get('blocktypes');
2699
2700 if ($blocktypes === false or $disablecache) {
f433088d 2701 try {
b8a6f26e 2702 $blocktypes = $DB->get_records('block', null, 'name', 'name,id,version,visible');
f433088d
PS
2703 } catch (dml_exception $e) {
2704 // before install
b8a6f26e 2705 $blocktypes = array();
f433088d 2706 }
b8a6f26e 2707 $cache->set('blocktypes', $blocktypes);
b9934a17
DM
2708 }
2709
b8a6f26e 2710 return $blocktypes;
b9934a17
DM
2711 }
2712}
2713
b6ad8594 2714
b9934a17
DM
2715/**
2716 * Class for text filters
2717 */
b6ad8594 2718class plugininfo_filter extends plugininfo_base {
b9934a17 2719
b9934a17 2720 public static function get_plugins($type, $typerootdir, $typeclass) {
7c9b837e 2721 global $CFG, $DB;
b9934a17
DM
2722
2723 $filters = array();
2724
8d211302 2725 // get the list of filters in /filter location
b9934a17
DM
2726 $installed = filter_get_all_installed();
2727
0662bd67 2728 foreach ($installed as $name => $displayname) {
b9934a17
DM
2729 $plugin = new $typeclass();
2730 $plugin->type = $type;
2731 $plugin->typerootdir = $typerootdir;
0662bd67
PS
2732 $plugin->name = $name;
2733 $plugin->rootdir = "$CFG->dirroot/filter/$name";
b9934a17
DM
2734 $plugin->displayname = $displayname;
2735
b8343e68
TH
2736 $plugin->load_disk_version();
2737 $plugin->load_db_version();
2738 $plugin->load_required_main_version();
2739 $plugin->init_is_standard();
b9934a17
DM
2740
2741 $filters[$plugin->name] = $plugin;
2742 }
2743
8d211302 2744 // Do not mess with filter registration here!
7c9b837e 2745
8d211302 2746 $globalstates = self::get_global_states();
b9934a17
DM
2747
2748 // make sure that all registered filters are installed, just in case
2749 foreach ($globalstates as $name => $info) {
2750 if (!isset($filters[$name])) {
2751 // oops, there is a record in filter_active but the filter is not installed
2752 $plugin = new $typeclass();
2753 $plugin->type = $type;
2754 $plugin->typerootdir = $typerootdir;
2755 $plugin->name = $name;
0662bd67
PS
2756 $plugin->rootdir = "$CFG->dirroot/filter/$name";
2757 $plugin->displayname = $name;
b9934a17 2758
b8343e68 2759 $plugin->load_db_version();
b9934a17
DM
2760
2761 if (is_null($plugin->versiondb)) {
2762 // this is a hack to stimulate 'Missing from disk' error
2763 // because $plugin->versiondisk will be null !== false
2764 $plugin->versiondb = false;
2765 }
2766
2767 $filters[$plugin->name] = $plugin;
2768 }
2769 }
2770
2771 return $filters;
2772 }
2773
b8343e68 2774 public function init_display_name() {
b9934a17
DM
2775 // do nothing, the name is set in self::get_plugins()
2776 }
2777
b9934a17
DM
2778 public function is_enabled() {
2779
2780 $globalstates = self::get_global_states();
2781
0662bd67 2782 foreach ($globalstates as $name => $info) {
b9934a17
DM
2783 if ($name === $this->name) {
2784 if ($info->active == TEXTFILTER_DISABLED) {
2785 return false;
2786 } else {
2787 // it may be 'On' or 'Off, but available'
2788 return null;
2789 }
2790 }
2791 }
2792
2793 return null;
2794 }
2795
1de1a666 2796 public function get_settings_section_name() {
0662bd67 2797 return 'filtersetting' . $this->name;
1de1a666
MG
2798 }
2799
2800 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
2801 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
2802 $ADMIN = $adminroot; // may be used in settings.php
2803 $filter = $this; // also can be used inside settings.php
2804
2805 $settings = null;
8d211302 2806 if ($hassiteconfig && file_exists($this->full_path('filtersettings.php'))) {
1de1a666
MG
2807 $section = $this->get_settings_section_name();
2808 $settings = new admin_settingpage($section, $this->displayname,
2809 'moodle/site:config', $this->is_enabled() === false);
2810 include($this->full_path('filtersettings.php')); // this may also set $settings to null
2811 }
2812 if ($settings) {
2813 $ADMIN->add($parentnodename, $settings);
b9934a17
DM
2814 }
2815 }
2816
b9934a17 2817 public function get_uninstall_url() {
0662bd67 2818 return new moodle_url('/admin/filters.php', array('sesskey' => sesskey(), 'filterpath' => $this->name, 'action' => 'delete'));
b9934a17
DM
2819 }
2820
2821 /**
2822 * Provides access to the results of {@link filter_get_global_states()}
2823 * but indexed by the normalized filter name
2824 *
2825 * The legacy filter name is available as ->legacyname property.
2826 *
b8a6f26e 2827 * @param bool $disablecache do not attempt to obtain data from the cache
b9934a17
DM
2828 * @return array
2829 */
2830 protected static function get_global_states($disablecache=false) {
2831 global $DB;
b9934a17 2832
ad3ed98b 2833 $cache = cache::make('core', 'plugininfo_filter');
b8a6f26e
DM
2834
2835 $globalstates = $cache->get('globalstates');
2836
2837 if ($globalstates === false or $disablecache) {
b9934a17
DM
2838
2839 if (!$DB->get_manager()->table_exists('filter_active')) {
8d211302 2840 // Not installed yet.
b8a6f26e
DM
2841 $cache->set('globalstates', array());
2842 return array();
8d211302 2843 }
b9934a17 2844
b8a6f26e
DM
2845 $globalstates = array();
2846
8d211302
PS
2847 foreach (filter_get_global_states() as $name => $info) {
2848 if (strpos($name, '/') !== false) {
2849 // Skip existing before upgrade to new names.
2850 continue;
b9934a17 2851 }
8d211302 2852
b8a6f26e
DM
2853 $filterinfo = new stdClass();
2854 $filterinfo->active = $info->active;
2855 $filterinfo->sortorder = $info->sortorder;
2856 $globalstates[$name] = $filterinfo;
b9934a17 2857 }
b8a6f26e
DM
2858
2859 $cache->set('globalstates', $globalstates);
b9934a17
DM
2860 }
2861
b8a6f26e 2862 return $globalstates;
b9934a17
DM
2863 }
2864}
2865
b6ad8594 2866
b9934a17
DM
2867/**
2868 * Class for activity modules
2869 */
b6ad8594 2870class plugininfo_mod extends plugininfo_base {
b9934a17 2871
b9934a17
DM
2872 public static function get_plugins($type, $typerootdir, $typeclass) {
2873
2874 // get the information about plugins at the disk
2875 $modules = parent::get_plugins($type, $typerootdir, $typeclass);
2876
2877 // add modules missing from disk
2878 $modulesinfo = self::get_modules_info();
2879 foreach ($modulesinfo as $modulename => $moduleinfo) {
2880 if (isset($modules[$modulename])) {
2881 continue;
2882 }
2883 $plugin = new $typeclass();
2884 $plugin->type = $type;
2885 $plugin->typerootdir = $typerootdir;
2886 $plugin->name = $modulename;
2887 $plugin->rootdir = null;
2888 $plugin->displayname = $modulename;
2889 $plugin->versiondb = $moduleinfo->version;
b8343e68 2890 $plugin->init_is_standard();
b9934a17
DM
2891
2892 $modules[$modulename] = $plugin;
2893 }
2894
2895 return $modules;
2896 }
2897
fde6f79f
MG
2898 /**
2899 * Magic method getter, redirects to read only values.
2900 *
2901 * For module plugins we pretend the object has 'visible' property for compatibility
2902 * with plugins developed for Moodle version below 2.4
2903 *
2904 * @param string $name
2905 * @return mixed
2906 */
2907 public function __get($name) {
2908 if ($name === 'visible') {
2909 debugging('This is now an instance of plugininfo_mod, please use $module->is_enabled() instead of $module->visible', DEBUG_DEVELOPER);
2910 return ($this->is_enabled() !== false);
2911 }
2912 return parent::__get($name);
2913 }
2914
b8343e68 2915 public function init_display_name() {
828788f0
TH
2916 if (get_string_manager()->string_exists('pluginname', $this->component)) {
2917 $this->displayname = get_string('pluginname', $this->component);
b9934a17 2918 } else {
828788f0 2919 $this->displayname = get_string('modulename', $this->component);
b9934a17
DM
2920 }
2921 }
2922
2923 /**
473289a0 2924 * Load the data from version.php.
9d6eb027
DM
2925 *
2926 * @param bool $disablecache do not attempt to obtain data from the cache
473289a0 2927 * @return object the data object defined in version.php.
b9934a17 2928 */
9d6eb027
DM
2929 protected function load_version_php($disablecache=false) {
2930
2931 $cache = cache::make('core', 'plugininfo_base');
2932
2933 $versionsphp = $cache->get('versions_php');
2934
2935 if (!$disablecache and $versionsphp !== false and isset($versionsphp[$this->component])) {
2936 return $versionsphp[$this->component];
2937 }
2938
473289a0 2939 $versionfile = $this->full_path('version.php');
b9934a17 2940
473289a0 2941 $module = new stdClass();
bdbcb6d7 2942 $plugin = new stdClass();
b9934a17
DM
2943 if (is_readable($versionfile)) {
2944 include($versionfile);
b9934a17 2945 }
bdbcb6d7
PS
2946 if (!isset($module->version) and isset($plugin->version)) {
2947 $module = $plugin;
2948 }
9d6eb027
DM
2949 $versionsphp[$this->component] = $module;
2950 $cache->set('versions_php', $versionsphp);
2951
473289a0 2952 return $module;
b9934a17
DM
2953 }
2954
b8343e68 2955 public function load_db_version() {
b9934a17
DM
2956 global $DB;
2957
2958 $modulesinfo = self::get_modules_info();
2959 if (isset($modulesinfo[$this->name]->version)) {
2960 $this->versiondb = $modulesinfo[$this->name]->version;
2961 }
2962 }
2963
b9934a17
DM
2964 public function is_enabled() {
2965
2966 $modulesinfo = self::get_modules_info();
2967 if (isset($modulesinfo[$this->name]->visible)) {
2968 if ($modulesinfo[$this->name]->visible) {
2969 return true;
2970 } else {
2971 return false;
2972 }
2973 } else {
2974 return parent::is_enabled();
2975 }
2976 }
2977
fde6f79f
MG
2978 public function get_settings_section_name() {
2979 return 'modsetting' . $this->name;
2980 }
b9934a17 2981
fde6f79f
MG
2982 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
2983 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
2984 $ADMIN = $adminroot; // may be used in settings.php
2985 $module = $this; // also can be used inside settings.php
2986 $section = $this->get_settings_section_name();
2987
dddbbac3 2988 $modulesinfo = self::get_modules_info();
fde6f79f 2989 $settings = null;
dddbbac3 2990 if ($hassiteconfig && isset($modulesinfo[$this->name]) && file_exists($this->full_path('settings.php'))) {
fde6f79f
MG
2991 $settings = new admin_settingpage($section, $this->displayname,
2992 'moodle/site:config', $this->is_enabled() === false);
2993 include($this->full_path('settings.php')); // this may also set $settings to null
2994 }
2995 if ($settings) {
2996 $ADMIN->add($parentnodename, $settings);
b9934a17
DM
2997 }
2998 }
2999
b9934a17
DM
3000 public function get_uninstall_url() {
3001
3002 if ($this->name !== 'forum') {
3003 return new moodle_url('/admin/modules.php', array('delete' => $this->name, 'sesskey' => sesskey()));
3004 } else {
3005 return null;
3006 }
3007 }
3008
3009 /**
3010 * Provides access to the records in {modules} table
3011 *
b8a6f26e 3012 * @param bool $disablecache do not attempt to obtain data from the cache
b9934a17
DM
3013 * @return array array of stdClasses
3014 */
3015 protected static function get_modules_info($disablecache=false) {
3016 global $DB;
b9934a17 3017
ad3ed98b 3018 $cache = cache::make('core', 'plugininfo_mod');
b8a6f26e
DM
3019
3020 $modulesinfo = $cache->get('modulesinfo');
3021
3022 if ($modulesinfo === false or $disablecache) {
f433088d 3023 try {
b8a6f26e 3024 $modulesinfo = $DB->get_records('modules', null, 'name', 'name,id,version,visible');
f433088d
PS
3025 } catch (dml_exception $e) {
3026 // before install
b8a6f26e 3027 $modulesinfo = array();
f433088d 3028 }
b8a6f26e 3029 $cache->set('modulesinfo', $modulesinfo);
b9934a17
DM
3030 }
3031
b8a6f26e 3032 return $modulesinfo;
b9934a17
DM
3033 }
3034}
3035
0242bdc7
TH
3036
3037/**
3038 * Class for question behaviours.
3039 */
b6ad8594
DM
3040class plugininfo_qbehaviour extends plugininfo_base {
3041
828788f0
TH
3042 public function get_uninstall_url() {
3043 return new moodle_url('/admin/qbehaviours.php',
3044 array('delete' => $this->name, 'sesskey' => sesskey()));
3045 }
0242bdc7
TH
3046}
3047
3048
b9934a17
DM
3049/**
3050 * Class for question types
3051 */
b6ad8594
DM
3052class plugininfo_qtype extends plugininfo_base {
3053
828788f0
TH
3054 public function get_uninstall_url() {
3055 return new moodle_url('/admin/qtypes.php',
3056 array('delete' => $this->name, 'sesskey' => sesskey()));
3057 }
66f3684a
MG
3058
3059 public function get_settings_section_name() {
3060 return 'qtypesetting' . $this->name;
3061 }
3062
3063 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3064 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3065 $ADMIN = $adminroot; // may be used in settings.php
3066 $qtype = $this; // also can be used inside settings.php
3067 $section = $this->get_settings_section_name();
3068
3069 $settings = null;
837e1812
TH
3070 $systemcontext = context_system::instance();
3071 if (($hassiteconfig || has_capability('moodle/question:config', $systemcontext)) &&
3072 file_exists($this->full_path('settings.php'))) {
66f3684a 3073 $settings = new admin_settingpage($section, $this->displayname,
837e1812 3074 'moodle/question:config', $this->is_enabled() === false);
66f3684a
MG
3075 include($this->full_path('settings.php')); // this may also set $settings to null
3076 }
3077 if ($settings) {
3078 $ADMIN->add($parentnodename, $settings);
3079 }
3080 }
b9934a17
DM
3081}
3082
b9934a17
DM
3083
3084/**
3085 * Class for authentication plugins
3086 */
b6ad8594 3087class plugininfo_auth extends plugininfo_base {
b9934a17 3088
b9934a17
DM
3089 public function is_enabled() {
3090 global $CFG;
b9934a17
DM
3091
3092 if (in_array($this->name, array('nologin', 'manual'))) {
3093 // these two are always enabled and can't be disabled
3094 return null;
3095 }
3096
b8a6f26e 3097 $enabled = array_flip(explode(',', $CFG->auth));
b9934a17
DM
3098
3099 return isset($enabled[$this->name]);
3100 }
3101
cbe9f609
MG
3102 public function get_settings_section_name() {
3103 return 'authsetting' . $this->name;
3104 }
3105
3106 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3107 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3108 $ADMIN = $adminroot; // may be used in settings.php
3109 $auth = $this; // also to be used inside settings.php
3110 $section = $this->get_settings_section_name();
3111
3112 $settings = null;
3113 if ($hassiteconfig) {
3114 if (file_exists($this->full_path('settings.php'))) {
3115 // TODO: finish implementation of common settings - locking, etc.
3116 $settings = new admin_settingpage($section, $this->displayname,
3117 'moodle/site:config', $this->is_enabled() === false);
3118 include($this->full_path('settings.php')); // this may also set $settings to null
3119 } else {
3120 $settingsurl = new moodle_url('/admin/auth_config.php', array('auth' => $this->name));
3121 $settings = new admin_externalpage($section, $this->displayname,
3122 $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
3123 }
3124 }
3125 if ($settings) {
3126 $ADMIN->add($parentnodename, $settings);
b9934a17
DM
3127 }
3128 }
3129}
3130
b6ad8594 3131
b9934a17
DM
3132/**
3133 * Class for enrolment plugins
3134 */
b6ad8594 3135class plugininfo_enrol extends plugininfo_base {
b9934a17 3136
b9934a17
DM
3137 public function is_enabled() {
3138 global $CFG;
b9934a17 3139
b6ad8594
DM
3140 // We do not actually need whole enrolment classes here so we do not call
3141 // {@link enrol_get_plugins()}. Note that this may produce slightly different
3142 // results, for example if the enrolment plugin does not contain lib.php
3143 // but it is listed in $CFG->enrol_plugins_enabled
3144
b8a6f26e 3145 $enabled = array_flip(explode(',', $CFG->enrol_plugins_enabled));
b9934a17
DM
3146
3147 return isset($enabled[$this->name]);
3148 }
3149
79c5c3fa 3150 public function get_settings_section_name() {
c7a33990
PS
3151 if (file_exists($this->full_path('settings.php'))) {
3152 return 'enrolsettings' . $this->name;
3153 } else {
3154 return null;
3155 }
79c5c3fa 3156 }
b9934a17 3157
79c5c3fa
MG
3158 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3159 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
c7a33990
PS
3160
3161 if (!$hassiteconfig or !file_exists($this->full_path('settings.php'))) {
3162 return;
3163 }
3164 $section = $this->get_settings_section_name();
3165
79c5c3fa
MG
3166 $ADMIN = $adminroot; // may be used in settings.php
3167 $enrol = $this; // also can be used inside settings.php
c7a33990
PS
3168 $settings = new admin_settingpage($section, $this->displayname,
3169 'moodle/site:config', $this->is_enabled() === false);
3170
3171 include($this->full_path('settings.php')); // This may also set $settings to null!
79c5c3fa 3172
79c5c3fa
MG
3173 if ($settings) {
3174 $ADMIN->add($parentnodename, $settings);
b9934a17
DM
3175 }
3176 }
3177
b9934a17
DM
3178 public function get_uninstall_url() {
3179 return new moodle_url('/admin/enrol.php', array('action' => 'uninstall', 'enrol' => $this->name, 'sesskey' => sesskey()));
3180 }
3181}
3182
b6ad8594 3183
b9934a17
DM
3184/**
3185 * Class for messaging processors
3186 */
b6ad8594 3187class plugininfo_message extends plugininfo_base {
b9934a17 3188
e8d16932
MG
3189 public function get_settings_section_name() {
3190 return 'messagesetting' . $this->name;
3191 }
3192
3193 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3194 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3195 $ADMIN = $adminroot; // may be used in settings.php
3196 if (!$hassiteconfig) {
3197 return;
3198 }
3199 $section = $this->get_settings_section_name();
3200
3201 $settings = null;
bc795b98
RK
3202 $processors = get_message_processors();
3203 if (isset($processors[$this->name])) {
3204 $processor = $processors[$this->name];
3205 if ($processor->available && $processor->hassettings) {
e8d16932
MG
3206 $settings = new admin_settingpage($section, $this->displayname,
3207 'moodle/site:config', $this->is_enabled() === false);
3208 include($this->full_path('settings.php')); // this may also set $settings to null
bc795b98 3209 }
0210ce10 3210 }
e8d16932
MG
3211 if ($settings) {
3212 $ADMIN->add($parentnodename, $settings);
3213 }
b9934a17 3214 }
b9934a17 3215
bede23f7
RK
3216 /**
3217 * @see plugintype_interface::is_enabled()
3218 */
3219 public function is_enabled() {
3220 $processors = get_message_processors();
3221 if (isset($processors[$this->name])) {
3222 return $processors[$this->name]->configured && $processors[$this->name]->enabled;
0210ce10 3223 } else {
bede23f7
RK
3224 return parent::is_enabled();
3225 }
3226 }
3f9d9e28
RK
3227
3228 /**
3229 * @see plugintype_interface::get_uninstall_url()
3230 */
3231 public function get_uninstall_url() {
3232 $processors = get_message_processors();
3233 if (isset($processors[$this->name])) {
e8d16932 3234 return new moodle_url('/admin/message.php', array('uninstall' => $processors[$this->name]->id, 'sesskey' => sesskey()));
3f9d9e28
RK
3235 } else {
3236 return parent::get_uninstall_url();
0210ce10 3237 }
b9934a17
DM
3238 }
3239}
3240
b6ad8594 3241
b9934a17
DM
3242/**
3243 * Class for repositories
3244 */
b6ad8594 3245class plugininfo_repository extends plugininfo_base {
b9934a17 3246
b9934a17
DM
3247 public function is_enabled() {
3248
3249 $enabled = self::get_enabled_repositories();
3250
3251 return isset($enabled[$this->name]);
3252 }
3253
c517dd68
MG
3254 public function get_settings_section_name() {
3255 return 'repositorysettings'.$this->name;
3256 }
b9934a17 3257
c517dd68
MG
3258 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3259 if ($hassiteconfig && $this->is_enabled()) {
3260 // completely no access to repository setting when it is not enabled
3261 $sectionname = $this->get_settings_section_name();
3262 $settingsurl = new moodle_url('/admin/repository.php',
3263 array('sesskey' => sesskey(), 'action' => 'edit', 'repos' => $this->name));
3264 $settings = new admin_externalpage($sectionname, $this->displayname,
3265 $settingsurl, 'moodle/site:config', false);
3266 $adminroot->add($parentnodename, $settings);
b9934a17
DM
3267 }
3268 }
3269
3270 /**
3271 * Provides access to the records in {repository} table
3272 *
b8a6f26e 3273 * @param bool $disablecache do not attempt to obtain data from the cache
b9934a17
DM
3274 * @return array array of stdClasses
3275 */
3276 protected static function get_enabled_repositories($disablecache=false) {
3277 global $DB;
b9934a17 3278
ad3ed98b 3279 $cache = cache::make('core', 'plugininfo_repository');
b8a6f26e
DM
3280
3281 $enabled = $cache->get('enabled');
3282
3283 if ($enabled === false or $disablecache) {
3284 $enabled = $DB->get_records('repository', null, 'type', 'type,visible,sortorder');
3285 $cache->set('enabled', $enabled);
b9934a17
DM
3286 }
3287
b8a6f26e 3288 return $enabled;
b9934a17
DM
3289 }
3290}
3291
b6ad8594 3292
b9934a17
DM
3293/**
3294 * Class for portfolios
3295 */
b6ad8594 3296class plugininfo_portfolio extends plugininfo_base {
b9934a17 3297
b9934a17
DM
3298 public function is_enabled() {
3299
3300 $enabled = self::get_enabled_portfolios();
3301
3302 return isset($enabled[$this->name]);
3303 }
3304
3305 /**
b8a6f26e 3306 * Returns list of enabled portfolio plugins
b9934a17 3307 *
b8a6f26e
DM
3308 * Portfolio plugin is enabled if there is at least one record in the {portfolio_instance}
3309 * table for it.
3310 *
3311 * @param bool $disablecache do not attempt to obtain data from the cache
3312 * @return array array of stdClasses with properties plugin and visible indexed by plugin
b9934a17
DM
3313 */
3314 protected static function get_enabled_portfolios($disablecache=false) {
3315 global $DB;
b9934a17 3316
ad3ed98b 3317 $cache = cache::make('core', 'plugininfo_portfolio');
b8a6f26e
DM
3318
3319 $enabled = $cache->get('enabled');
3320
3321 if ($enabled === false or $disablecache) {
3322 $enabled = array();
3323 $instances = $DB->get_recordset('portfolio_instance', null, '', 'plugin,visible');
b9934a17 3324 foreach ($instances as $instance) {
b8a6f26e 3325 if (isset($enabled[$instance->plugin])) {
b9934a17 3326 if ($instance->visible) {
b8a6f26e 3327 $enabled[$instance->plugin]->visible = $instance->visible;
b9934a17
DM
3328 }
3329 } else {
b8a6f26e 3330 $enabled[$instance->plugin] = $instance;
b9934a17
DM
3331 }
3332 }
b8a6f26e
DM
3333 $instances->close();
3334 $cache->set('enabled', $enabled);
b9934a17
DM
3335 }
3336
b8a6f26e 3337 return $enabled;
b9934a17
DM
3338 }
3339}
3340
b6ad8594 3341
b9934a17
DM
3342/**
3343 * Class for themes
3344 */
b6ad8594 3345class plugininfo_theme extends plugininfo_base {
b9934a17 3346
b9934a17
DM
3347 public function is_enabled() {
3348 global $CFG;
3349
3350 if ((!empty($CFG->theme) and $CFG->theme === $this->name) or
3351 (!empty($CFG->themelegacy) and $CFG->themelegacy === $this->name)) {
3352 return true;
3353 } else {
3354 return parent::is_enabled();
3355 }
3356 }
3357}
3358
b6ad8594 3359
b9934a17
DM
3360/**
3361 * Class representing an MNet service
3362 */
b6ad8594 3363class plugininfo_mnetservice extends plugininfo_base {
b9934a17 3364
b9934a17
DM
3365 public function is_enabled() {
3366 global $CFG;
3367
3368 if (empty($CFG->mnet_dispatcher_mode) || $CFG->mnet_dispatcher_mode !== 'strict') {
3369 return false;
3370 } else {
3371 return parent::is_enabled();
3372 }
3373 }
3374}
3cdfaeef 3375
b6ad8594 3376
3cdfaeef
PS
3377/**
3378 * Class for admin tool plugins
3379 */
b6ad8594 3380class plugininfo_tool extends plugininfo_base {
3cdfaeef
PS
3381
3382 public function get_uninstall_url() {
3383 return new moodle_url('/admin/tools.php', array('delete' => $this->name, 'sesskey' => sesskey()));
3384 }
3385}
4f6bba20 3386
b6ad8594 3387
4f6bba20
PS
3388/**
3389 * Class for admin tool plugins
3390 */
b6ad8594 3391class plugininfo_report extends plugininfo_base {
4f6bba20
PS
3392
3393 public function get_uninstall_url() {
3394 return new moodle_url('/admin/reports.php', array('delete' => $this->name, 'sesskey' => sesskey()));
3395 }
3396}
888ce02a
RK
3397
3398
3399/**
3400 * Class for local plugins
3401 */
3402class plugininfo_local extends plugininfo_base {
3403
3404 public function get_uninstall_url() {
3405 return new moodle_url('/admin/localplugins.php', array('delete' => $this->name, 'sesskey' => sesskey()));
3406 }
888ce02a 3407}
888ce02a 3408
087001ee
MG
3409/**
3410 * Class for HTML editors
3411 */
3412class plugininfo_editor extends plugininfo_base {
3413
3414 public function get_settings_section_name() {
3415 return 'editorsettings' . $this->name;
3416 }
3417
3418 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3419 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3420 $ADMIN = $adminroot; // may be used in settings.php
3421 $editor = $this; // also can be used inside settings.php
3422 $section = $this->get_settings_section_name();
3423
3424 $settings = null;
3425 if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3426 $settings = new admin_settingpage($section, $this->displayname,
3427 'moodle/site:config', $this->is_enabled() === false);
3428 include($this->full_path('settings.php')); // this may also set $settings to null
3429 }
3430 if ($settings) {
3431 $ADMIN->add($parentnodename, $settings);
3432 }
3433 }
3434
3435 /**
3436 * Returns the information about plugin availability
3437 *
3438 * True means that the plugin is enabled. False means that the plugin is
3439 * disabled. Null means that the information is not available, or the
3440 * plugin does not support configurable availability or the availability
3441 * can not be changed.
3442 *
3443 * @return null|bool
3444 */
3445 public function is_enabled() {
3446 global $CFG;
3447 if (empty($CFG->texteditors)) {
3448 $CFG->texteditors = 'tinymce,textarea';
3449 }
3450 if (in_array($this->name, explode(',', $CFG->texteditors))) {
3451 return true;
3452 }
3453 return false;
3454 }
3455}
d98305bd
MG
3456
3457/**
3458 * Class for plagiarism plugins
3459 */
3460class plugininfo_plagiarism extends plugininfo_base {
3461
3462 public function get_settings_section_name() {
3463 return 'plagiarism'. $this->name;
3464 }
3465
3466 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3467 // plagiarism plugin just redirect to settings.php in the plugins directory
3468 if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3469 $section = $this->get_settings_section_name();
3470 $settingsurl = new moodle_url($this->get_dir().'/settings.php');
3471 $settings = new admin_externalpage($section, $this->displayname,
3472 $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
3473 $adminroot->add($parentnodename, $settings);
3474 }
3475 }
3476}
2567584d
MG
3477
3478/**
3479 * Class for webservice protocols
3480 */
3481class plugininfo_webservice extends plugininfo_base {
3482
3483 public function get_settings_section_name() {
3484 return 'webservicesetting' . $this->name;
3485 }
3486
3487 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3488 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3489 $ADMIN = $adminroot; // may be used in settings.php
3490 $webservice = $this; // also can be used inside settings.php
3491 $section = $this->get_settings_section_name();
3492
3493 $settings = null;
3494 if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3495 $settings = new admin_settingpage($section, $this->displayname,
3496 'moodle/site:config', $this->is_enabled() === false);
3497 include($this->full_path('settings.php')); // this may also set $settings to null
3498 }
3499 if ($settings) {
3500 $ADMIN->add($parentnodename, $settings);
888ce02a
RK
3501 }
3502 }
2567584d
MG
3503
3504 public function is_enabled() {
3505 global $CFG;
3506 if (empty($CFG->enablewebservices)) {
3507 return false;
3508 }
3509 $active_webservices = empty($CFG->webserviceprotocols) ? array() : explode(',', $CFG->webserviceprotocols);
3510 if (in_array($this->name, $active_webservices)) {
3511 return true;
3512 }
3513 return false;
3514 }
3515
3516 public function get_uninstall_url() {
3517 return new moodle_url('/admin/webservice/protocols.php',
3518 array('sesskey' => sesskey(), 'action' => 'uninstall', 'webservice' => $this->name));
3519 }
888ce02a 3520}
3776335c
MG
3521
3522/**
3523 * Class for course formats
3524 */
3525class plugininfo_format extends plugininfo_base {
3526
3527 /**
3528 * Gathers and returns the information about all plugins of the given type
3529 *
3530 * @param string $type the name of the plugintype, eg. mod, auth or workshopform
3531 * @param string $typerootdir full path to the location of the plugin dir
3532 * @param string $typeclass the name of the actually called class
3533 * @return array of plugintype classes, indexed by the plugin name
3534 */
3535 public static function get_plugins($type, $typerootdir, $typeclass) {
3536 global $CFG;
3537 $formats = parent::get_plugins($type, $typerootdir, $typeclass);
3538 require_once($CFG->dirroot.'/course/lib.php');
3539 $order = get_sorted_course_formats();
3540 $sortedformats = array();
3541 foreach ($order as $formatname) {
3542 $sortedformats[$formatname] = $formats[$formatname];
3543 }
3544 return $sortedformats;
3545 }
3546
3547 public function get_settings_section_name() {
3548 return 'formatsetting' . $this->name;
3549 }
3550
3551 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3552 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3553 $ADMIN = $adminroot; // also may be used in settings.php
3554 $section = $this->get_settings_section_name();
3555
3556 $settings = null;
3557 if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3558 $settings = new admin_settingpage($section, $this->displayname,
3559 'moodle/site:config', $this->is_enabled() === false);
3560 include($this->full_path('settings.php')); // this may also set $settings to null
3561 }
3562 if ($settings) {
3563 $ADMIN->add($parentnodename, $settings);
3564 }