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