MDL-36903 Verify the SSL certificate of available updates provider
[moodle.git] / lib / pluginlib.php
blob6feda47da97c1a3e1abf07105bbefd0af73fb09d
1 <?php
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.
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/>.
18 /**
19 * Defines classes used for plugins management
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.
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
31 defined('MOODLE_INTERNAL') || die();
33 /**
34 * Singleton class providing general plugins management functionality
36 class plugin_manager {
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';
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';
51 /** the standard plugin is about to be deleted */
52 const PLUGIN_STATUS_DELETE = 'delete';
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';
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;
65 /**
66 * Direct initiation not allowed, use the factory method {@link self::instance()}
68 protected function __construct() {
71 /**
72 * Sorry, this is singleton
74 protected function __clone() {
77 /**
78 * Factory method for this class
80 * @return plugin_manager the singleton instance
82 public static function instance() {
83 if (is_null(self::$singletoninstance)) {
84 self::$singletoninstance = new self();
86 return self::$singletoninstance;
89 /**
90 * Reset any caches
91 * @param bool $phpunitreset
93 public static function reset_caches($phpunitreset = false) {
94 if ($phpunitreset) {
95 self::$singletoninstance = null;
99 /**
100 * Returns a tree of known plugins and information about them
102 * @param bool $disablecache force reload, cache can be used otherwise
103 * @return array 2D array. The first keys are plugin type names (e.g. qtype);
104 * the second keys are the plugin local name (e.g. multichoice); and
105 * the values are the corresponding objects extending {@link plugininfo_base}
107 public function get_plugins($disablecache=false) {
108 global $CFG;
110 if ($disablecache or is_null($this->pluginsinfo)) {
111 // Hack: include mod and editor subplugin management classes first,
112 // the adminlib.php is supposed to contain extra admin settings too.
113 require_once($CFG->libdir.'/adminlib.php');
114 foreach(array('mod', 'editor') as $type) {
115 foreach (get_plugin_list($type) as $dir) {
116 if (file_exists("$dir/adminlib.php")) {
117 include_once("$dir/adminlib.php");
121 $this->pluginsinfo = array();
122 $plugintypes = get_plugin_types();
123 $plugintypes = $this->reorder_plugin_types($plugintypes);
124 foreach ($plugintypes as $plugintype => $plugintyperootdir) {
125 if (in_array($plugintype, array('base', 'general'))) {
126 throw new coding_exception('Illegal usage of reserved word for plugin type');
128 if (class_exists('plugininfo_' . $plugintype)) {
129 $plugintypeclass = 'plugininfo_' . $plugintype;
130 } else {
131 $plugintypeclass = 'plugininfo_general';
133 if (!in_array('plugininfo_base', class_parents($plugintypeclass))) {
134 throw new coding_exception('Class ' . $plugintypeclass . ' must extend plugininfo_base');
136 $plugins = call_user_func(array($plugintypeclass, 'get_plugins'), $plugintype, $plugintyperootdir, $plugintypeclass);
137 $this->pluginsinfo[$plugintype] = $plugins;
140 if (empty($CFG->disableupdatenotifications) and !during_initial_install()) {
141 // append the information about available updates provided by {@link available_update_checker()}
142 $provider = available_update_checker::instance();
143 foreach ($this->pluginsinfo as $plugintype => $plugins) {
144 foreach ($plugins as $plugininfoholder) {
145 $plugininfoholder->check_available_updates($provider);
151 return $this->pluginsinfo;
155 * Returns list of plugins that define their subplugins and the information
156 * about them from the db/subplugins.php file.
158 * At the moment, only activity modules and editors can define subplugins.
160 * @param bool $disablecache force reload, cache can be used otherwise
161 * @return array with keys like 'mod_quiz', and values the data from the
162 * corresponding db/subplugins.php file.
164 public function get_subplugins($disablecache=false) {
166 if ($disablecache or is_null($this->subpluginsinfo)) {
167 $this->subpluginsinfo = array();
168 foreach (array('mod', 'editor') as $type) {
169 $owners = get_plugin_list($type);
170 foreach ($owners as $component => $ownerdir) {
171 $componentsubplugins = array();
172 if (file_exists($ownerdir . '/db/subplugins.php')) {
173 $subplugins = array();
174 include($ownerdir . '/db/subplugins.php');
175 foreach ($subplugins as $subplugintype => $subplugintyperootdir) {
176 $subplugin = new stdClass();
177 $subplugin->type = $subplugintype;
178 $subplugin->typerootdir = $subplugintyperootdir;
179 $componentsubplugins[$subplugintype] = $subplugin;
181 $this->subpluginsinfo[$type . '_' . $component] = $componentsubplugins;
187 return $this->subpluginsinfo;
191 * Returns the name of the plugin that defines the given subplugin type
193 * If the given subplugin type is not actually a subplugin, returns false.
195 * @param string $subplugintype the name of subplugin type, eg. workshopform or quiz
196 * @return false|string the name of the parent plugin, eg. mod_workshop
198 public function get_parent_of_subplugin($subplugintype) {
200 $parent = false;
201 foreach ($this->get_subplugins() as $pluginname => $subplugintypes) {
202 if (isset($subplugintypes[$subplugintype])) {
203 $parent = $pluginname;
204 break;
208 return $parent;
212 * Returns a localized name of a given plugin
214 * @param string $plugin name of the plugin, eg mod_workshop or auth_ldap
215 * @return string
217 public function plugin_name($plugin) {
218 list($type, $name) = normalize_component($plugin);
219 return $this->pluginsinfo[$type][$name]->displayname;
223 * Returns a localized name of a plugin type in plural form
225 * Most plugin types define their names in core_plugin lang file. In case of subplugins,
226 * we try to ask the parent plugin for the name. In the worst case, we will return
227 * the value of the passed $type parameter.
229 * @param string $type the type of the plugin, e.g. mod or workshopform
230 * @return string
232 public function plugintype_name_plural($type) {
234 if (get_string_manager()->string_exists('type_' . $type . '_plural', 'core_plugin')) {
235 // for most plugin types, their names are defined in core_plugin lang file
236 return get_string('type_' . $type . '_plural', 'core_plugin');
238 } else if ($parent = $this->get_parent_of_subplugin($type)) {
239 // if this is a subplugin, try to ask the parent plugin for the name
240 if (get_string_manager()->string_exists('subplugintype_' . $type . '_plural', $parent)) {
241 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type . '_plural', $parent);
242 } else {
243 return $this->plugin_name($parent) . ' / ' . $type;
246 } else {
247 return $type;
252 * @param string $component frankenstyle component name.
253 * @return plugininfo_base|null the corresponding plugin information.
255 public function get_plugin_info($component) {
256 list($type, $name) = normalize_component($component);
257 $plugins = $this->get_plugins();
258 if (isset($plugins[$type][$name])) {
259 return $plugins[$type][$name];
260 } else {
261 return null;
266 * Get a list of any other plugins that require this one.
267 * @param string $component frankenstyle component name.
268 * @return array of frankensyle component names that require this one.
270 public function other_plugins_that_require($component) {
271 $others = array();
272 foreach ($this->get_plugins() as $type => $plugins) {
273 foreach ($plugins as $plugin) {
274 $required = $plugin->get_other_required_plugins();
275 if (isset($required[$component])) {
276 $others[] = $plugin->component;
280 return $others;
284 * Check a dependencies list against the list of installed plugins.
285 * @param array $dependencies compenent name to required version or ANY_VERSION.
286 * @return bool true if all the dependencies are satisfied.
288 public function are_dependencies_satisfied($dependencies) {
289 foreach ($dependencies as $component => $requiredversion) {
290 $otherplugin = $this->get_plugin_info($component);
291 if (is_null($otherplugin)) {
292 return false;
295 if ($requiredversion != ANY_VERSION and $otherplugin->versiondisk < $requiredversion) {
296 return false;
300 return true;
304 * Checks all dependencies for all installed plugins
306 * This is used by install and upgrade. The array passed by reference as the second
307 * argument is populated with the list of plugins that have failed dependencies (note that
308 * a single plugin can appear multiple times in the $failedplugins).
310 * @param int $moodleversion the version from version.php.
311 * @param array $failedplugins to return the list of plugins with non-satisfied dependencies
312 * @return bool true if all the dependencies are satisfied for all plugins.
314 public function all_plugins_ok($moodleversion, &$failedplugins = array()) {
316 $return = true;
317 foreach ($this->get_plugins() as $type => $plugins) {
318 foreach ($plugins as $plugin) {
320 if (!$plugin->is_core_dependency_satisfied($moodleversion)) {
321 $return = false;
322 $failedplugins[] = $plugin->component;
325 if (!$this->are_dependencies_satisfied($plugin->get_other_required_plugins())) {
326 $return = false;
327 $failedplugins[] = $plugin->component;
332 return $return;
336 * Checks if there are some plugins with a known available update
338 * @return bool true if there is at least one available update
340 public function some_plugins_updatable() {
341 foreach ($this->get_plugins() as $type => $plugins) {
342 foreach ($plugins as $plugin) {
343 if ($plugin->available_updates()) {
344 return true;
349 return false;
353 * Defines a list of all plugins that were originally shipped in the standard Moodle distribution,
354 * but are not anymore and are deleted during upgrades.
356 * The main purpose of this list is to hide missing plugins during upgrade.
358 * @param string $type plugin type
359 * @param string $name plugin name
360 * @return bool
362 public static function is_deleted_standard_plugin($type, $name) {
363 static $plugins = array(
364 // do not add 1.9-2.2 plugin removals here
367 if (!isset($plugins[$type])) {
368 return false;
370 return in_array($name, $plugins[$type]);
374 * Defines a white list of all plugins shipped in the standard Moodle distribution
376 * @param string $type
377 * @return false|array array of standard plugins or false if the type is unknown
379 public static function standard_plugins_list($type) {
380 static $standard_plugins = array(
382 'assignment' => array(
383 'offline', 'online', 'upload', 'uploadsingle'
386 'assignsubmission' => array(
387 'comments', 'file', 'onlinetext'
390 'assignfeedback' => array(
391 'comments', 'file', 'offline'
394 'auth' => array(
395 'cas', 'db', 'email', 'fc', 'imap', 'ldap', 'manual', 'mnet',
396 'nntp', 'nologin', 'none', 'pam', 'pop3', 'radius',
397 'shibboleth', 'webservice'
400 'block' => array(
401 'activity_modules', 'admin_bookmarks', 'blog_menu',
402 'blog_recent', 'blog_tags', 'calendar_month',
403 'calendar_upcoming', 'comments', 'community',
404 'completionstatus', 'course_list', 'course_overview',
405 'course_summary', 'feedback', 'glossary_random', 'html',
406 'login', 'mentees', 'messages', 'mnet_hosts', 'myprofile',
407 'navigation', 'news_items', 'online_users', 'participants',
408 'private_files', 'quiz_results', 'recent_activity',
409 'rss_client', 'search_forums', 'section_links',
410 'selfcompletion', 'settings', 'site_main_menu',
411 'social_activities', 'tag_flickr', 'tag_youtube', 'tags'
414 'booktool' => array(
415 'exportimscp', 'importhtml', 'print'
418 'cachelock' => array(
419 'file'
422 'cachestore' => array(
423 'file', 'memcache', 'memcached', 'mongodb', 'session', 'static'
426 'coursereport' => array(
427 //deprecated!
430 'datafield' => array(
431 'checkbox', 'date', 'file', 'latlong', 'menu', 'multimenu',
432 'number', 'picture', 'radiobutton', 'text', 'textarea', 'url'
435 'datapreset' => array(
436 'imagegallery'
439 'editor' => array(
440 'textarea', 'tinymce'
443 'enrol' => array(
444 'authorize', 'category', 'cohort', 'database', 'flatfile',
445 'guest', 'imsenterprise', 'ldap', 'manual', 'meta', 'mnet',
446 'paypal', 'self'
449 'filter' => array(
450 'activitynames', 'algebra', 'censor', 'emailprotect',
451 'emoticon', 'mediaplugin', 'multilang', 'tex', 'tidy',
452 'urltolink', 'data', 'glossary'
455 'format' => array(
456 'scorm', 'social', 'topics', 'weeks'
459 'gradeexport' => array(
460 'ods', 'txt', 'xls', 'xml'
463 'gradeimport' => array(
464 'csv', 'xml'
467 'gradereport' => array(
468 'grader', 'outcomes', 'overview', 'user'
471 'gradingform' => array(
472 'rubric', 'guide'
475 'local' => array(
478 'message' => array(
479 'email', 'jabber', 'popup'
482 'mnetservice' => array(
483 'enrol'
486 'mod' => array(
487 'assign', 'assignment', 'book', 'chat', 'choice', 'data', 'feedback', 'folder',
488 'forum', 'glossary', 'imscp', 'label', 'lesson', 'lti', 'page',
489 'quiz', 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop'
492 'plagiarism' => array(
495 'portfolio' => array(
496 'boxnet', 'download', 'flickr', 'googledocs', 'mahara', 'picasa'
499 'profilefield' => array(
500 'checkbox', 'datetime', 'menu', 'text', 'textarea'
503 'qbehaviour' => array(
504 'adaptive', 'adaptivenopenalty', 'deferredcbm',
505 'deferredfeedback', 'immediatecbm', 'immediatefeedback',
506 'informationitem', 'interactive', 'interactivecountback',
507 'manualgraded', 'missing'
510 'qformat' => array(
511 'aiken', 'blackboard', 'blackboard_six', 'examview', 'gift',
512 'learnwise', 'missingword', 'multianswer', 'webct',
513 'xhtml', 'xml'
516 'qtype' => array(
517 'calculated', 'calculatedmulti', 'calculatedsimple',
518 'description', 'essay', 'match', 'missingtype', 'multianswer',
519 'multichoice', 'numerical', 'random', 'randomsamatch',
520 'shortanswer', 'truefalse'
523 'quiz' => array(
524 'grading', 'overview', 'responses', 'statistics'
527 'quizaccess' => array(
528 'delaybetweenattempts', 'ipaddress', 'numattempts', 'openclosedate',
529 'password', 'safebrowser', 'securewindow', 'timelimit'
532 'report' => array(
533 'backups', 'completion', 'configlog', 'courseoverview',
534 'log', 'loglive', 'outline', 'participation', 'progress', 'questioninstances', 'security', 'stats'
537 'repository' => array(
538 'alfresco', 'boxnet', 'coursefiles', 'dropbox', 'equella', 'filesystem',
539 'flickr', 'flickr_public', 'googledocs', 'local', 'merlot',
540 'picasa', 'recent', 's3', 'upload', 'url', 'user', 'webdav',
541 'wikimedia', 'youtube'
544 'scormreport' => array(
545 'basic',
546 'interactions',
547 'graphs'
550 'tinymce' => array(
551 'dragmath', 'moodleemoticon', 'moodleimage', 'moodlemedia', 'moodlenolink', 'spellchecker',
554 'theme' => array(
555 'afterburner', 'anomaly', 'arialist', 'base', 'binarius',
556 'boxxie', 'brick', 'canvas', 'formal_white', 'formfactor',
557 'fusion', 'leatherbound', 'magazine', 'mymobile', 'nimble',
558 'nonzero', 'overlay', 'serenity', 'sky_high', 'splash',
559 'standard', 'standardold'
562 'tool' => array(
563 'assignmentupgrade', 'capability', 'customlang', 'dbtransfer', 'generator',
564 'health', 'innodb', 'langimport', 'multilangupgrade', 'phpunit', 'profiling',
565 'qeupgradehelper', 'replace', 'spamcleaner', 'timezoneimport', 'unittest',
566 'uploaduser', 'unsuproles', 'xmldb'
569 'webservice' => array(
570 'amf', 'rest', 'soap', 'xmlrpc'
573 'workshopallocation' => array(
574 'manual', 'random', 'scheduled'
577 'workshopeval' => array(
578 'best'
581 'workshopform' => array(
582 'accumulative', 'comments', 'numerrors', 'rubric'
586 if (isset($standard_plugins[$type])) {
587 return $standard_plugins[$type];
588 } else {
589 return false;
594 * Reorders plugin types into a sequence to be displayed
596 * For technical reasons, plugin types returned by {@link get_plugin_types()} are
597 * in a certain order that does not need to fit the expected order for the display.
598 * Particularly, activity modules should be displayed first as they represent the
599 * real heart of Moodle. They should be followed by other plugin types that are
600 * used to build the courses (as that is what one expects from LMS). After that,
601 * other supportive plugin types follow.
603 * @param array $types associative array
604 * @return array same array with altered order of items
606 protected function reorder_plugin_types(array $types) {
607 $fix = array(
608 'mod' => $types['mod'],
609 'block' => $types['block'],
610 'qtype' => $types['qtype'],
611 'qbehaviour' => $types['qbehaviour'],
612 'qformat' => $types['qformat'],
613 'filter' => $types['filter'],
614 'enrol' => $types['enrol'],
616 foreach ($types as $type => $path) {
617 if (!isset($fix[$type])) {
618 $fix[$type] = $path;
621 return $fix;
627 * General exception thrown by the {@link available_update_checker} class
629 class available_update_checker_exception extends moodle_exception {
632 * @param string $errorcode exception description identifier
633 * @param mixed $debuginfo debugging data to display
635 public function __construct($errorcode, $debuginfo=null) {
636 parent::__construct($errorcode, 'core_plugin', '', null, print_r($debuginfo, true));
642 * Singleton class that handles checking for available updates
644 class available_update_checker {
646 /** @var available_update_checker holds the singleton instance */
647 protected static $singletoninstance;
648 /** @var null|int the timestamp of when the most recent response was fetched */
649 protected $recentfetch = null;
650 /** @var null|array the recent response from the update notification provider */
651 protected $recentresponse = null;
652 /** @var null|string the numerical version of the local Moodle code */
653 protected $currentversion = null;
654 /** @var null|string the release info of the local Moodle code */
655 protected $currentrelease = null;
656 /** @var null|string branch of the local Moodle code */
657 protected $currentbranch = null;
658 /** @var array of (string)frankestyle => (string)version list of additional plugins deployed at this site */
659 protected $currentplugins = array();
662 * Direct initiation not allowed, use the factory method {@link self::instance()}
664 protected function __construct() {
668 * Sorry, this is singleton
670 protected function __clone() {
674 * Factory method for this class
676 * @return available_update_checker the singleton instance
678 public static function instance() {
679 if (is_null(self::$singletoninstance)) {
680 self::$singletoninstance = new self();
682 return self::$singletoninstance;
686 * Reset any caches
687 * @param bool $phpunitreset
689 public static function reset_caches($phpunitreset = false) {
690 if ($phpunitreset) {
691 self::$singletoninstance = null;
696 * Returns the timestamp of the last execution of {@link fetch()}
698 * @return int|null null if it has never been executed or we don't known
700 public function get_last_timefetched() {
702 $this->restore_response();
704 if (!empty($this->recentfetch)) {
705 return $this->recentfetch;
707 } else {
708 return null;
713 * Fetches the available update status from the remote site
715 * @throws available_update_checker_exception
717 public function fetch() {
718 $response = $this->get_response();
719 $this->validate_response($response);
720 $this->store_response($response);
724 * Returns the available update information for the given component
726 * This method returns null if the most recent response does not contain any information
727 * about it. The returned structure is an array of available updates for the given
728 * component. Each update info is an object with at least one property called
729 * 'version'. Other possible properties are 'release', 'maturity', 'url' and 'downloadurl'.
731 * For the 'core' component, the method returns real updates only (those with higher version).
732 * For all other components, the list of all known remote updates is returned and the caller
733 * (usually the {@link plugin_manager}) is supposed to make the actual comparison of versions.
735 * @param string $component frankenstyle
736 * @param array $options with supported keys 'minmaturity' and/or 'notifybuilds'
737 * @return null|array null or array of available_update_info objects
739 public function get_update_info($component, array $options = array()) {
741 if (!isset($options['minmaturity'])) {
742 $options['minmaturity'] = 0;
745 if (!isset($options['notifybuilds'])) {
746 $options['notifybuilds'] = false;
749 if ($component == 'core') {
750 $this->load_current_environment();
753 $this->restore_response();
755 if (empty($this->recentresponse['updates'][$component])) {
756 return null;
759 $updates = array();
760 foreach ($this->recentresponse['updates'][$component] as $info) {
761 $update = new available_update_info($component, $info);
762 if (isset($update->maturity) and ($update->maturity < $options['minmaturity'])) {
763 continue;
765 if ($component == 'core') {
766 if ($update->version <= $this->currentversion) {
767 continue;
769 if (empty($options['notifybuilds']) and $this->is_same_release($update->release)) {
770 continue;
773 $updates[] = $update;
776 if (empty($updates)) {
777 return null;
780 return $updates;
784 * The method being run via cron.php
786 public function cron() {
787 global $CFG;
789 if (!$this->cron_autocheck_enabled()) {
790 $this->cron_mtrace('Automatic check for available updates not enabled, skipping.');
791 return;
794 $now = $this->cron_current_timestamp();
796 if ($this->cron_has_fresh_fetch($now)) {
797 $this->cron_mtrace('Recently fetched info about available updates is still fresh enough, skipping.');
798 return;
801 if ($this->cron_has_outdated_fetch($now)) {
802 $this->cron_mtrace('Outdated or missing info about available updates, forced fetching ... ', '');
803 $this->cron_execute();
804 return;
807 $offset = $this->cron_execution_offset();
808 $start = mktime(1, 0, 0, date('n', $now), date('j', $now), date('Y', $now)); // 01:00 AM today local time
809 if ($now > $start + $offset) {
810 $this->cron_mtrace('Regular daily check for available updates ... ', '');
811 $this->cron_execute();
812 return;
816 /// end of public API //////////////////////////////////////////////////////
819 * Makes cURL request to get data from the remote site
821 * @return string raw request result
822 * @throws available_update_checker_exception
824 protected function get_response() {
825 global $CFG;
826 require_once($CFG->libdir.'/filelib.php');
828 $curl = new curl(array('proxy' => true));
829 $response = $curl->post($this->prepare_request_url(), $this->prepare_request_params(), $this->prepare_request_options());
830 $curlerrno = $curl->get_errno();
831 if (!empty($curlerrno)) {
832 throw new available_update_checker_exception('err_response_curl', 'cURL error '.$curlerrno.': '.$curl->error);
834 $curlinfo = $curl->get_info();
835 if ($curlinfo['http_code'] != 200) {
836 throw new available_update_checker_exception('err_response_http_code', $curlinfo['http_code']);
838 return $response;
842 * Makes sure the response is valid, has correct API format etc.
844 * @param string $response raw response as returned by the {@link self::get_response()}
845 * @throws available_update_checker_exception
847 protected function validate_response($response) {
849 $response = $this->decode_response($response);
851 if (empty($response)) {
852 throw new available_update_checker_exception('err_response_empty');
855 if (empty($response['status']) or $response['status'] !== 'OK') {
856 throw new available_update_checker_exception('err_response_status', $response['status']);
859 if (empty($response['apiver']) or $response['apiver'] !== '1.1') {
860 throw new available_update_checker_exception('err_response_format_version', $response['apiver']);
863 if (empty($response['forbranch']) or $response['forbranch'] !== moodle_major_version(true)) {
864 throw new available_update_checker_exception('err_response_target_version', $response['forbranch']);
869 * Decodes the raw string response from the update notifications provider
871 * @param string $response as returned by {@link self::get_response()}
872 * @return array decoded response structure
874 protected function decode_response($response) {
875 return json_decode($response, true);
879 * Stores the valid fetched response for later usage
881 * This implementation uses the config_plugins table as the permanent storage.
883 * @param string $response raw valid data returned by {@link self::get_response()}
885 protected function store_response($response) {
887 set_config('recentfetch', time(), 'core_plugin');
888 set_config('recentresponse', $response, 'core_plugin');
890 $this->restore_response(true);
894 * Loads the most recent raw response record we have fetched
896 * After this method is called, $this->recentresponse is set to an array. If the
897 * array is empty, then either no data have been fetched yet or the fetched data
898 * do not have expected format (and thence they are ignored and a debugging
899 * message is displayed).
901 * This implementation uses the config_plugins table as the permanent storage.
903 * @param bool $forcereload reload even if it was already loaded
905 protected function restore_response($forcereload = false) {
907 if (!$forcereload and !is_null($this->recentresponse)) {
908 // we already have it, nothing to do
909 return;
912 $config = get_config('core_plugin');
914 if (!empty($config->recentresponse) and !empty($config->recentfetch)) {
915 try {
916 $this->validate_response($config->recentresponse);
917 $this->recentfetch = $config->recentfetch;
918 $this->recentresponse = $this->decode_response($config->recentresponse);
919 } catch (available_update_checker_exception $e) {
920 // The server response is not valid. Behave as if no data were fetched yet.
921 // This may happen when the most recent update info (cached locally) has been
922 // fetched with the previous branch of Moodle (like during an upgrade from 2.x
923 // to 2.y) or when the API of the response has changed.
924 $this->recentresponse = array();
927 } else {
928 $this->recentresponse = array();
933 * Compares two raw {@link $recentresponse} records and returns the list of changed updates
935 * This method is used to populate potential update info to be sent to site admins.
937 * @param array $old
938 * @param array $new
939 * @throws available_update_checker_exception
940 * @return array parts of $new['updates'] that have changed
942 protected function compare_responses(array $old, array $new) {
944 if (empty($new)) {
945 return array();
948 if (!array_key_exists('updates', $new)) {
949 throw new available_update_checker_exception('err_response_format');
952 if (empty($old)) {
953 return $new['updates'];
956 if (!array_key_exists('updates', $old)) {
957 throw new available_update_checker_exception('err_response_format');
960 $changes = array();
962 foreach ($new['updates'] as $newcomponent => $newcomponentupdates) {
963 if (empty($old['updates'][$newcomponent])) {
964 $changes[$newcomponent] = $newcomponentupdates;
965 continue;
967 foreach ($newcomponentupdates as $newcomponentupdate) {
968 $inold = false;
969 foreach ($old['updates'][$newcomponent] as $oldcomponentupdate) {
970 if ($newcomponentupdate['version'] == $oldcomponentupdate['version']) {
971 $inold = true;
974 if (!$inold) {
975 if (!isset($changes[$newcomponent])) {
976 $changes[$newcomponent] = array();
978 $changes[$newcomponent][] = $newcomponentupdate;
983 return $changes;
987 * Returns the URL to send update requests to
989 * During the development or testing, you can set $CFG->alternativeupdateproviderurl
990 * to a custom URL that will be used. Otherwise the standard URL will be returned.
992 * @return string URL
994 protected function prepare_request_url() {
995 global $CFG;
997 if (!empty($CFG->config_php_settings['alternativeupdateproviderurl'])) {
998 return $CFG->config_php_settings['alternativeupdateproviderurl'];
999 } else {
1000 return 'https://download.moodle.org/api/1.1/updates.php';
1005 * Sets the properties currentversion, currentrelease, currentbranch and currentplugins
1007 * @param bool $forcereload
1009 protected function load_current_environment($forcereload=false) {
1010 global $CFG;
1012 if (!is_null($this->currentversion) and !$forcereload) {
1013 // nothing to do
1014 return;
1017 $version = null;
1018 $release = null;
1020 require($CFG->dirroot.'/version.php');
1021 $this->currentversion = $version;
1022 $this->currentrelease = $release;
1023 $this->currentbranch = moodle_major_version(true);
1025 $pluginman = plugin_manager::instance();
1026 foreach ($pluginman->get_plugins() as $type => $plugins) {
1027 foreach ($plugins as $plugin) {
1028 if (!$plugin->is_standard()) {
1029 $this->currentplugins[$plugin->component] = $plugin->versiondisk;
1036 * Returns the list of HTTP params to be sent to the updates provider URL
1038 * @return array of (string)param => (string)value
1040 protected function prepare_request_params() {
1041 global $CFG;
1043 $this->load_current_environment();
1044 $this->restore_response();
1046 $params = array();
1047 $params['format'] = 'json';
1049 if (isset($this->recentresponse['ticket'])) {
1050 $params['ticket'] = $this->recentresponse['ticket'];
1053 if (isset($this->currentversion)) {
1054 $params['version'] = $this->currentversion;
1055 } else {
1056 throw new coding_exception('Main Moodle version must be already known here');
1059 if (isset($this->currentbranch)) {
1060 $params['branch'] = $this->currentbranch;
1061 } else {
1062 throw new coding_exception('Moodle release must be already known here');
1065 $plugins = array();
1066 foreach ($this->currentplugins as $plugin => $version) {
1067 $plugins[] = $plugin.'@'.$version;
1069 if (!empty($plugins)) {
1070 $params['plugins'] = implode(',', $plugins);
1073 return $params;
1077 * Returns the list of cURL options to use when fetching available updates data
1079 * @return array of (string)param => (string)value
1081 protected function prepare_request_options() {
1082 global $CFG;
1084 $options = array(
1085 'CURLOPT_SSL_VERIFYHOST' => 2, // this is the default in {@link curl} class but just in case
1086 'CURLOPT_SSL_VERIFYPEER' => true,
1089 $cacertfile = $CFG->dataroot.'/moodleorgca.crt';
1090 if (is_readable($cacertfile)) {
1091 // Do not use CA certs provided by the operating system. Instead,
1092 // use this CA cert to verify the updates provider.
1093 $options['CURLOPT_CAINFO'] = $cacertfile;
1096 return $options;
1100 * Returns the current timestamp
1102 * @return int the timestamp
1104 protected function cron_current_timestamp() {
1105 return time();
1109 * Output cron debugging info
1111 * @see mtrace()
1112 * @param string $msg output message
1113 * @param string $eol end of line
1115 protected function cron_mtrace($msg, $eol = PHP_EOL) {
1116 mtrace($msg, $eol);
1120 * Decide if the autocheck feature is disabled in the server setting
1122 * @return bool true if autocheck enabled, false if disabled
1124 protected function cron_autocheck_enabled() {
1125 global $CFG;
1127 if (empty($CFG->updateautocheck)) {
1128 return false;
1129 } else {
1130 return true;
1135 * Decide if the recently fetched data are still fresh enough
1137 * @param int $now current timestamp
1138 * @return bool true if no need to re-fetch, false otherwise
1140 protected function cron_has_fresh_fetch($now) {
1141 $recent = $this->get_last_timefetched();
1143 if (empty($recent)) {
1144 return false;
1147 if ($now < $recent) {
1148 $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1149 return true;
1152 if ($now - $recent > 24 * HOURSECS) {
1153 return false;
1156 return true;
1160 * Decide if the fetch is outadated or even missing
1162 * @param int $now current timestamp
1163 * @return bool false if no need to re-fetch, true otherwise
1165 protected function cron_has_outdated_fetch($now) {
1166 $recent = $this->get_last_timefetched();
1168 if (empty($recent)) {
1169 return true;
1172 if ($now < $recent) {
1173 $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1174 return false;
1177 if ($now - $recent > 48 * HOURSECS) {
1178 return true;
1181 return false;
1185 * Returns the cron execution offset for this site
1187 * The main {@link self::cron()} is supposed to run every night in some random time
1188 * between 01:00 and 06:00 AM (local time). The exact moment is defined by so called
1189 * execution offset, that is the amount of time after 01:00 AM. The offset value is
1190 * initially generated randomly and then used consistently at the site. This way, the
1191 * regular checks against the download.moodle.org server are spread in time.
1193 * @return int the offset number of seconds from range 1 sec to 5 hours
1195 protected function cron_execution_offset() {
1196 global $CFG;
1198 if (empty($CFG->updatecronoffset)) {
1199 set_config('updatecronoffset', rand(1, 5 * HOURSECS));
1202 return $CFG->updatecronoffset;
1206 * Fetch available updates info and eventually send notification to site admins
1208 protected function cron_execute() {
1210 try {
1211 $this->restore_response();
1212 $previous = $this->recentresponse;
1213 $this->fetch();
1214 $this->restore_response(true);
1215 $current = $this->recentresponse;
1216 $changes = $this->compare_responses($previous, $current);
1217 $notifications = $this->cron_notifications($changes);
1218 $this->cron_notify($notifications);
1219 $this->cron_mtrace('done');
1220 } catch (available_update_checker_exception $e) {
1221 $this->cron_mtrace('FAILED!');
1226 * Given the list of changes in available updates, pick those to send to site admins
1228 * @param array $changes as returned by {@link self::compare_responses()}
1229 * @return array of available_update_info objects to send to site admins
1231 protected function cron_notifications(array $changes) {
1232 global $CFG;
1234 $notifications = array();
1235 $pluginman = plugin_manager::instance();
1236 $plugins = $pluginman->get_plugins(true);
1238 foreach ($changes as $component => $componentchanges) {
1239 if (empty($componentchanges)) {
1240 continue;
1242 $componentupdates = $this->get_update_info($component,
1243 array('minmaturity' => $CFG->updateminmaturity, 'notifybuilds' => $CFG->updatenotifybuilds));
1244 if (empty($componentupdates)) {
1245 continue;
1247 // notify only about those $componentchanges that are present in $componentupdates
1248 // to respect the preferences
1249 foreach ($componentchanges as $componentchange) {
1250 foreach ($componentupdates as $componentupdate) {
1251 if ($componentupdate->version == $componentchange['version']) {
1252 if ($component == 'core') {
1253 // in case of 'core' this is enough, we already know that the
1254 // $componentupdate is a real update with higher version
1255 $notifications[] = $componentupdate;
1256 } else {
1257 // use the plugin_manager to check if the reported $componentchange
1258 // is a real update with higher version. such a real update must be
1259 // present in the 'availableupdates' property of one of the component's
1260 // available_update_info object
1261 list($plugintype, $pluginname) = normalize_component($component);
1262 if (!empty($plugins[$plugintype][$pluginname]->availableupdates)) {
1263 foreach ($plugins[$plugintype][$pluginname]->availableupdates as $availableupdate) {
1264 if ($availableupdate->version == $componentchange['version']) {
1265 $notifications[] = $componentupdate;
1275 return $notifications;
1279 * Sends the given notifications to site admins via messaging API
1281 * @param array $notifications array of available_update_info objects to send
1283 protected function cron_notify(array $notifications) {
1284 global $CFG;
1286 if (empty($notifications)) {
1287 return;
1290 $admins = get_admins();
1292 if (empty($admins)) {
1293 return;
1296 $this->cron_mtrace('sending notifications ... ', '');
1298 $text = get_string('updatenotifications', 'core_admin') . PHP_EOL;
1299 $html = html_writer::tag('h1', get_string('updatenotifications', 'core_admin')) . PHP_EOL;
1301 $coreupdates = array();
1302 $pluginupdates = array();
1304 foreach ($notifications as $notification) {
1305 if ($notification->component == 'core') {
1306 $coreupdates[] = $notification;
1307 } else {
1308 $pluginupdates[] = $notification;
1312 if (!empty($coreupdates)) {
1313 $text .= PHP_EOL . get_string('updateavailable', 'core_admin') . PHP_EOL;
1314 $html .= html_writer::tag('h2', get_string('updateavailable', 'core_admin')) . PHP_EOL;
1315 $html .= html_writer::start_tag('ul') . PHP_EOL;
1316 foreach ($coreupdates as $coreupdate) {
1317 $html .= html_writer::start_tag('li');
1318 if (isset($coreupdate->release)) {
1319 $text .= get_string('updateavailable_release', 'core_admin', $coreupdate->release);
1320 $html .= html_writer::tag('strong', get_string('updateavailable_release', 'core_admin', $coreupdate->release));
1322 if (isset($coreupdate->version)) {
1323 $text .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
1324 $html .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
1326 if (isset($coreupdate->maturity)) {
1327 $text .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
1328 $html .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
1330 $text .= PHP_EOL;
1331 $html .= html_writer::end_tag('li') . PHP_EOL;
1333 $text .= PHP_EOL;
1334 $html .= html_writer::end_tag('ul') . PHP_EOL;
1336 $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/index.php');
1337 $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
1338 $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/index.php', $CFG->wwwroot.'/'.$CFG->admin.'/index.php'));
1339 $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
1342 if (!empty($pluginupdates)) {
1343 $text .= PHP_EOL . get_string('updateavailableforplugin', 'core_admin') . PHP_EOL;
1344 $html .= html_writer::tag('h2', get_string('updateavailableforplugin', 'core_admin')) . PHP_EOL;
1346 $html .= html_writer::start_tag('ul') . PHP_EOL;
1347 foreach ($pluginupdates as $pluginupdate) {
1348 $html .= html_writer::start_tag('li');
1349 $text .= get_string('pluginname', $pluginupdate->component);
1350 $html .= html_writer::tag('strong', get_string('pluginname', $pluginupdate->component));
1352 $text .= ' ('.$pluginupdate->component.')';
1353 $html .= ' ('.$pluginupdate->component.')';
1355 $text .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
1356 $html .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
1358 $text .= PHP_EOL;
1359 $html .= html_writer::end_tag('li') . PHP_EOL;
1361 $text .= PHP_EOL;
1362 $html .= html_writer::end_tag('ul') . PHP_EOL;
1364 $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php');
1365 $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
1366 $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/plugins.php', $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php'));
1367 $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
1370 $a = array('siteurl' => $CFG->wwwroot);
1371 $text .= get_string('updatenotificationfooter', 'core_admin', $a) . PHP_EOL;
1372 $a = array('siteurl' => html_writer::link($CFG->wwwroot, $CFG->wwwroot));
1373 $html .= html_writer::tag('footer', html_writer::tag('p', get_string('updatenotificationfooter', 'core_admin', $a),
1374 array('style' => 'font-size:smaller; color:#333;')));
1376 foreach ($admins as $admin) {
1377 $message = new stdClass();
1378 $message->component = 'moodle';
1379 $message->name = 'availableupdate';
1380 $message->userfrom = get_admin();
1381 $message->userto = $admin;
1382 $message->subject = get_string('updatenotificationsubject', 'core_admin', array('siteurl' => $CFG->wwwroot));
1383 $message->fullmessage = $text;
1384 $message->fullmessageformat = FORMAT_PLAIN;
1385 $message->fullmessagehtml = $html;
1386 $message->smallmessage = get_string('updatenotifications', 'core_admin');
1387 $message->notification = 1;
1388 message_send($message);
1393 * Compare two release labels and decide if they are the same
1395 * @param string $remote release info of the available update
1396 * @param null|string $local release info of the local code, defaults to $release defined in version.php
1397 * @return boolean true if the releases declare the same minor+major version
1399 protected function is_same_release($remote, $local=null) {
1401 if (is_null($local)) {
1402 $this->load_current_environment();
1403 $local = $this->currentrelease;
1406 $pattern = '/^([0-9\.\+]+)([^(]*)/';
1408 preg_match($pattern, $remote, $remotematches);
1409 preg_match($pattern, $local, $localmatches);
1411 $remotematches[1] = str_replace('+', '', $remotematches[1]);
1412 $localmatches[1] = str_replace('+', '', $localmatches[1]);
1414 if ($remotematches[1] === $localmatches[1] and rtrim($remotematches[2]) === rtrim($localmatches[2])) {
1415 return true;
1416 } else {
1417 return false;
1424 * Defines the structure of objects returned by {@link available_update_checker::get_update_info()}
1426 class available_update_info {
1428 /** @var string frankenstyle component name */
1429 public $component;
1430 /** @var int the available version of the component */
1431 public $version;
1432 /** @var string|null optional release name */
1433 public $release = null;
1434 /** @var int|null optional maturity info, eg {@link MATURITY_STABLE} */
1435 public $maturity = null;
1436 /** @var string|null optional URL of a page with more info about the update */
1437 public $url = null;
1438 /** @var string|null optional URL of a ZIP package that can be downloaded and installed */
1439 public $download = null;
1440 /** @var string|null of self::download is set, then this must be the MD5 hash of the ZIP */
1441 public $downloadmd5 = null;
1444 * Creates new instance of the class
1446 * The $info array must provide at least the 'version' value and optionally all other
1447 * values to populate the object's properties.
1449 * @param string $name the frankenstyle component name
1450 * @param array $info associative array with other properties
1452 public function __construct($name, array $info) {
1453 $this->component = $name;
1454 foreach ($info as $k => $v) {
1455 if (property_exists('available_update_info', $k) and $k != 'component') {
1456 $this->$k = $v;
1464 * Implements a communication bridge to the mdeploy.php utility
1466 class available_update_deployer {
1468 const HTTP_PARAM_PREFIX = 'updteautodpldata_'; // Hey, even Google has not heard of such a prefix! So it MUST be safe :-p
1469 const HTTP_PARAM_CHECKER = 'datapackagesize'; // Name of the parameter that holds the number of items in the received data items
1471 /** @var available_update_deployer holds the singleton instance */
1472 protected static $singletoninstance;
1473 /** @var moodle_url URL of a page that includes the deployer UI */
1474 protected $callerurl;
1475 /** @var moodle_url URL to return after the deployment */
1476 protected $returnurl;
1479 * Direct instantiation not allowed, use the factory method {@link self::instance()}
1481 protected function __construct() {
1485 * Sorry, this is singleton
1487 protected function __clone() {
1491 * Factory method for this class
1493 * @return available_update_deployer the singleton instance
1495 public static function instance() {
1496 if (is_null(self::$singletoninstance)) {
1497 self::$singletoninstance = new self();
1499 return self::$singletoninstance;
1503 * Reset caches used by this script
1505 * @param bool $phpunitreset is this called as a part of PHPUnit reset?
1507 public static function reset_caches($phpunitreset = false) {
1508 if ($phpunitreset) {
1509 self::$singletoninstance = null;
1514 * Is automatic deployment enabled?
1516 * @return bool
1518 public function enabled() {
1519 global $CFG;
1521 if (!empty($CFG->disableupdateautodeploy)) {
1522 // The feature is prohibited via config.php
1523 return false;
1526 return get_config('updateautodeploy');
1530 * Sets some base properties of the class to make it usable.
1532 * @param moodle_url $callerurl the base URL of a script that will handle the class'es form data
1533 * @param moodle_url $returnurl the final URL to return to when the deployment is finished
1535 public function initialize(moodle_url $callerurl, moodle_url $returnurl) {
1537 if (!$this->enabled()) {
1538 throw new coding_exception('Unable to initialize the deployer, the feature is not enabled.');
1541 $this->callerurl = $callerurl;
1542 $this->returnurl = $returnurl;
1546 * Has the deployer been initialized?
1548 * Initialized deployer means that the following properties were set:
1549 * callerurl, returnurl
1551 * @return bool
1553 public function initialized() {
1555 if (!$this->enabled()) {
1556 return false;
1559 if (empty($this->callerurl)) {
1560 return false;
1563 if (empty($this->returnurl)) {
1564 return false;
1567 return true;
1571 * Returns a list of reasons why the deployment can not happen
1573 * If the returned array is empty, the deployment seems to be possible. The returned
1574 * structure is an associative array with keys representing individual impediments.
1575 * Possible keys are: missingdownloadurl, missingdownloadmd5, notwritable.
1577 * @param available_update_info $info
1578 * @return array
1580 public function deployment_impediments(available_update_info $info) {
1582 $impediments = array();
1584 if (empty($info->download)) {
1585 $impediments['missingdownloadurl'] = true;
1588 if (empty($info->downloadmd5)) {
1589 $impediments['missingdownloadmd5'] = true;
1592 if (!$this->component_writable($info->component)) {
1593 $impediments['notwritable'] = true;
1596 return $impediments;
1600 * Check to see if the current version of the plugin seems to be a checkout of an external repository.
1602 * @param available_update_info $info
1603 * @return false|string
1605 public function plugin_external_source(available_update_info $info) {
1607 $paths = get_plugin_types(true);
1608 list($plugintype, $pluginname) = normalize_component($info->component);
1609 $pluginroot = $paths[$plugintype].'/'.$pluginname;
1611 if (is_dir($pluginroot.'/.git')) {
1612 return 'git';
1615 if (is_dir($pluginroot.'/CVS')) {
1616 return 'cvs';
1619 if (is_dir($pluginroot.'/.svn')) {
1620 return 'svn';
1623 return false;
1627 * Prepares a renderable widget to confirm installation of an available update.
1629 * @param available_update_info $info component version to deploy
1630 * @return renderable
1632 public function make_confirm_widget(available_update_info $info) {
1634 if (!$this->initialized()) {
1635 throw new coding_exception('Illegal method call - deployer not initialized.');
1638 $params = $this->data_to_params(array(
1639 'updateinfo' => (array)$info, // see http://www.php.net/manual/en/language.types.array.php#language.types.array.casting
1642 $widget = new single_button(
1643 new moodle_url($this->callerurl, $params),
1644 get_string('updateavailableinstall', 'core_admin'),
1645 'post'
1648 return $widget;
1652 * Prepares a renderable widget to execute installation of an available update.
1654 * @param available_update_info $info component version to deploy
1655 * @return renderable
1657 public function make_execution_widget(available_update_info $info) {
1658 global $CFG;
1660 if (!$this->initialized()) {
1661 throw new coding_exception('Illegal method call - deployer not initialized.');
1664 $pluginrootpaths = get_plugin_types(true);
1666 list($plugintype, $pluginname) = normalize_component($info->component);
1668 if (empty($pluginrootpaths[$plugintype])) {
1669 throw new coding_exception('Unknown plugin type root location', $plugintype);
1672 list($passfile, $password) = $this->prepare_authorization();
1674 $upgradeurl = new moodle_url('/admin');
1676 $params = array(
1677 'upgrade' => true,
1678 'type' => $plugintype,
1679 'name' => $pluginname,
1680 'typeroot' => $pluginrootpaths[$plugintype],
1681 'package' => $info->download,
1682 'md5' => $info->downloadmd5,
1683 'dataroot' => $CFG->dataroot,
1684 'dirroot' => $CFG->dirroot,
1685 'passfile' => $passfile,
1686 'password' => $password,
1687 'returnurl' => $upgradeurl->out(true),
1690 $widget = new single_button(
1691 new moodle_url('/mdeploy.php', $params),
1692 get_string('updateavailableinstall', 'core_admin'),
1693 'post'
1696 return $widget;
1700 * Returns array of data objects passed to this tool.
1702 * @return array
1704 public function submitted_data() {
1706 $data = $this->params_to_data($_POST);
1708 if (empty($data) or empty($data[self::HTTP_PARAM_CHECKER])) {
1709 return false;
1712 if (!empty($data['updateinfo']) and is_object($data['updateinfo'])) {
1713 $updateinfo = $data['updateinfo'];
1714 if (!empty($updateinfo->component) and !empty($updateinfo->version)) {
1715 $data['updateinfo'] = new available_update_info($updateinfo->component, (array)$updateinfo);
1719 if (!empty($data['callerurl'])) {
1720 $data['callerurl'] = new moodle_url($data['callerurl']);
1723 if (!empty($data['returnurl'])) {
1724 $data['returnurl'] = new moodle_url($data['returnurl']);
1727 return $data;
1731 * Handles magic getters and setters for protected properties.
1733 * @param string $name method name, e.g. set_returnurl()
1734 * @param array $arguments arguments to be passed to the array
1736 public function __call($name, array $arguments = array()) {
1738 if (substr($name, 0, 4) === 'set_') {
1739 $property = substr($name, 4);
1740 if (empty($property)) {
1741 throw new coding_exception('Invalid property name (empty)');
1743 if (empty($arguments)) {
1744 $arguments = array(true); // Default value for flag-like properties.
1746 // Make sure it is a protected property.
1747 $isprotected = false;
1748 $reflection = new ReflectionObject($this);
1749 foreach ($reflection->getProperties(ReflectionProperty::IS_PROTECTED) as $reflectionproperty) {
1750 if ($reflectionproperty->getName() === $property) {
1751 $isprotected = true;
1752 break;
1755 if (!$isprotected) {
1756 throw new coding_exception('Unable to set property - it does not exist or it is not protected');
1758 $value = reset($arguments);
1759 $this->$property = $value;
1760 return;
1763 if (substr($name, 0, 4) === 'get_') {
1764 $property = substr($name, 4);
1765 if (empty($property)) {
1766 throw new coding_exception('Invalid property name (empty)');
1768 if (!empty($arguments)) {
1769 throw new coding_exception('No parameter expected');
1771 // Make sure it is a protected property.
1772 $isprotected = false;
1773 $reflection = new ReflectionObject($this);
1774 foreach ($reflection->getProperties(ReflectionProperty::IS_PROTECTED) as $reflectionproperty) {
1775 if ($reflectionproperty->getName() === $property) {
1776 $isprotected = true;
1777 break;
1780 if (!$isprotected) {
1781 throw new coding_exception('Unable to get property - it does not exist or it is not protected');
1783 return $this->$property;
1788 * Generates a random token and stores it in a file in moodledata directory.
1790 * @return array of the (string)filename and (string)password in this order
1792 public function prepare_authorization() {
1793 global $CFG;
1795 make_upload_directory('mdeploy/auth/');
1797 $attempts = 0;
1798 $success = false;
1800 while (!$success and $attempts < 5) {
1801 $attempts++;
1803 $passfile = $this->generate_passfile();
1804 $password = $this->generate_password();
1805 $now = time();
1807 $filepath = $CFG->dataroot.'/mdeploy/auth/'.$passfile;
1809 if (!file_exists($filepath)) {
1810 $success = file_put_contents($filepath, $password . PHP_EOL . $now . PHP_EOL, LOCK_EX);
1814 if ($success) {
1815 return array($passfile, $password);
1817 } else {
1818 throw new moodle_exception('unable_prepare_authorization', 'core_plugin');
1822 // End of external API
1825 * Prepares an array of HTTP parameters that can be passed to another page.
1827 * @param array|object $data associative array or an object holding the data, data JSON-able
1828 * @return array suitable as a param for moodle_url
1830 protected function data_to_params($data) {
1832 // Append some our own data
1833 if (!empty($this->callerurl)) {
1834 $data['callerurl'] = $this->callerurl->out(false);
1836 if (!empty($this->callerurl)) {
1837 $data['returnurl'] = $this->returnurl->out(false);
1840 // Finally append the count of items in the package.
1841 $data[self::HTTP_PARAM_CHECKER] = count($data);
1843 // Generate params
1844 $params = array();
1845 foreach ($data as $name => $value) {
1846 $transname = self::HTTP_PARAM_PREFIX.$name;
1847 $transvalue = json_encode($value);
1848 $params[$transname] = $transvalue;
1851 return $params;
1855 * Converts HTTP parameters passed to the script into native PHP data
1857 * @param array $params such as $_REQUEST or $_POST
1858 * @return array data passed for this class
1860 protected function params_to_data(array $params) {
1862 if (empty($params)) {
1863 return array();
1866 $data = array();
1867 foreach ($params as $name => $value) {
1868 if (strpos($name, self::HTTP_PARAM_PREFIX) === 0) {
1869 $realname = substr($name, strlen(self::HTTP_PARAM_PREFIX));
1870 $realvalue = json_decode($value);
1871 $data[$realname] = $realvalue;
1875 return $data;
1879 * Returns a random string to be used as a filename of the password storage.
1881 * @return string
1883 protected function generate_passfile() {
1884 return clean_param(uniqid('mdeploy_', true), PARAM_FILE);
1888 * Returns a random string to be used as the authorization token
1890 * @return string
1892 protected function generate_password() {
1893 return complex_random_string();
1897 * Checks if the given component's directory is writable
1899 * For the purpose of the deployment, the web server process has to have
1900 * write access to all files in the component's directory (recursively) and for the
1901 * directory itself.
1903 * @see worker::move_directory_source_precheck()
1904 * @param string $component normalized component name
1905 * @return boolean
1907 protected function component_writable($component) {
1909 list($plugintype, $pluginname) = normalize_component($component);
1911 $directory = get_plugin_directory($plugintype, $pluginname);
1913 if (is_null($directory)) {
1914 throw new coding_exception('Unknown component location', $component);
1917 return $this->directory_writable($directory);
1921 * Checks if the directory and all its contents (recursively) is writable
1923 * @param string $path full path to a directory
1924 * @return boolean
1926 private function directory_writable($path) {
1928 if (!is_writable($path)) {
1929 return false;
1932 if (is_dir($path)) {
1933 $handle = opendir($path);
1934 } else {
1935 return false;
1938 $result = true;
1940 while ($filename = readdir($handle)) {
1941 $filepath = $path.'/'.$filename;
1943 if ($filename === '.' or $filename === '..') {
1944 continue;
1947 if (is_dir($filepath)) {
1948 $result = $result && $this->directory_writable($filepath);
1950 } else {
1951 $result = $result && is_writable($filepath);
1955 closedir($handle);
1957 return $result;
1963 * Factory class producing required subclasses of {@link plugininfo_base}
1965 class plugininfo_default_factory {
1968 * Makes a new instance of the plugininfo class
1970 * @param string $type the plugin type, eg. 'mod'
1971 * @param string $typerootdir full path to the location of all the plugins of this type
1972 * @param string $name the plugin name, eg. 'workshop'
1973 * @param string $namerootdir full path to the location of the plugin
1974 * @param string $typeclass the name of class that holds the info about the plugin
1975 * @return plugininfo_base the instance of $typeclass
1977 public static function make($type, $typerootdir, $name, $namerootdir, $typeclass) {
1978 $plugin = new $typeclass();
1979 $plugin->type = $type;
1980 $plugin->typerootdir = $typerootdir;
1981 $plugin->name = $name;
1982 $plugin->rootdir = $namerootdir;
1984 $plugin->init_display_name();
1985 $plugin->load_disk_version();
1986 $plugin->load_db_version();
1987 $plugin->load_required_main_version();
1988 $plugin->init_is_standard();
1990 return $plugin;
1996 * Base class providing access to the information about a plugin
1998 * @property-read string component the component name, type_name
2000 abstract class plugininfo_base {
2002 /** @var string the plugintype name, eg. mod, auth or workshopform */
2003 public $type;
2004 /** @var string full path to the location of all the plugins of this type */
2005 public $typerootdir;
2006 /** @var string the plugin name, eg. assignment, ldap */
2007 public $name;
2008 /** @var string the localized plugin name */
2009 public $displayname;
2010 /** @var string the plugin source, one of plugin_manager::PLUGIN_SOURCE_xxx constants */
2011 public $source;
2012 /** @var fullpath to the location of this plugin */
2013 public $rootdir;
2014 /** @var int|string the version of the plugin's source code */
2015 public $versiondisk;
2016 /** @var int|string the version of the installed plugin */
2017 public $versiondb;
2018 /** @var int|float|string required version of Moodle core */
2019 public $versionrequires;
2020 /** @var array other plugins that this one depends on, lazy-loaded by {@link get_other_required_plugins()} */
2021 public $dependencies;
2022 /** @var int number of instances of the plugin - not supported yet */
2023 public $instances;
2024 /** @var int order of the plugin among other plugins of the same type - not supported yet */
2025 public $sortorder;
2026 /** @var array|null array of {@link available_update_info} for this plugin */
2027 public $availableupdates;
2030 * Gathers and returns the information about all plugins of the given type
2032 * @param string $type the name of the plugintype, eg. mod, auth or workshopform
2033 * @param string $typerootdir full path to the location of the plugin dir
2034 * @param string $typeclass the name of the actually called class
2035 * @return array of plugintype classes, indexed by the plugin name
2037 public static function get_plugins($type, $typerootdir, $typeclass) {
2039 // get the information about plugins at the disk
2040 $plugins = get_plugin_list($type);
2041 $ondisk = array();
2042 foreach ($plugins as $pluginname => $pluginrootdir) {
2043 $ondisk[$pluginname] = plugininfo_default_factory::make($type, $typerootdir,
2044 $pluginname, $pluginrootdir, $typeclass);
2046 return $ondisk;
2050 * Sets {@link $displayname} property to a localized name of the plugin
2052 public function init_display_name() {
2053 if (!get_string_manager()->string_exists('pluginname', $this->component)) {
2054 $this->displayname = '[pluginname,' . $this->component . ']';
2055 } else {
2056 $this->displayname = get_string('pluginname', $this->component);
2061 * Magic method getter, redirects to read only values.
2063 * @param string $name
2064 * @return mixed
2066 public function __get($name) {
2067 switch ($name) {
2068 case 'component': return $this->type . '_' . $this->name;
2070 default:
2071 debugging('Invalid plugin property accessed! '.$name);
2072 return null;
2077 * Return the full path name of a file within the plugin.
2079 * No check is made to see if the file exists.
2081 * @param string $relativepath e.g. 'version.php'.
2082 * @return string e.g. $CFG->dirroot . '/mod/quiz/version.php'.
2084 public function full_path($relativepath) {
2085 if (empty($this->rootdir)) {
2086 return '';
2088 return $this->rootdir . '/' . $relativepath;
2092 * Load the data from version.php.
2094 * @return stdClass the object called $plugin defined in version.php
2096 protected function load_version_php() {
2097 $versionfile = $this->full_path('version.php');
2099 $plugin = new stdClass();
2100 if (is_readable($versionfile)) {
2101 include($versionfile);
2103 return $plugin;
2107 * Sets {@link $versiondisk} property to a numerical value representing the
2108 * version of the plugin's source code.
2110 * If the value is null after calling this method, either the plugin
2111 * does not use versioning (typically does not have any database
2112 * data) or is missing from disk.
2114 public function load_disk_version() {
2115 $plugin = $this->load_version_php();
2116 if (isset($plugin->version)) {
2117 $this->versiondisk = $plugin->version;
2122 * Sets {@link $versionrequires} property to a numerical value representing
2123 * the version of Moodle core that this plugin requires.
2125 public function load_required_main_version() {
2126 $plugin = $this->load_version_php();
2127 if (isset($plugin->requires)) {
2128 $this->versionrequires = $plugin->requires;
2133 * Initialise {@link $dependencies} to the list of other plugins (in any)
2134 * that this one requires to be installed.
2136 protected function load_other_required_plugins() {
2137 $plugin = $this->load_version_php();
2138 if (!empty($plugin->dependencies)) {
2139 $this->dependencies = $plugin->dependencies;
2140 } else {
2141 $this->dependencies = array(); // By default, no dependencies.
2146 * Get the list of other plugins that this plugin requires to be installed.
2148 * @return array with keys the frankenstyle plugin name, and values either
2149 * a version string (like '2011101700') or the constant ANY_VERSION.
2151 public function get_other_required_plugins() {
2152 if (is_null($this->dependencies)) {
2153 $this->load_other_required_plugins();
2155 return $this->dependencies;
2159 * Sets {@link $versiondb} property to a numerical value representing the
2160 * currently installed version of the plugin.
2162 * If the value is null after calling this method, either the plugin
2163 * does not use versioning (typically does not have any database
2164 * data) or has not been installed yet.
2166 public function load_db_version() {
2167 if ($ver = self::get_version_from_config_plugins($this->component)) {
2168 $this->versiondb = $ver;
2173 * Sets {@link $source} property to one of plugin_manager::PLUGIN_SOURCE_xxx
2174 * constants.
2176 * If the property's value is null after calling this method, then
2177 * the type of the plugin has not been recognized and you should throw
2178 * an exception.
2180 public function init_is_standard() {
2182 $standard = plugin_manager::standard_plugins_list($this->type);
2184 if ($standard !== false) {
2185 $standard = array_flip($standard);
2186 if (isset($standard[$this->name])) {
2187 $this->source = plugin_manager::PLUGIN_SOURCE_STANDARD;
2188 } else if (!is_null($this->versiondb) and is_null($this->versiondisk)
2189 and plugin_manager::is_deleted_standard_plugin($this->type, $this->name)) {
2190 $this->source = plugin_manager::PLUGIN_SOURCE_STANDARD; // to be deleted
2191 } else {
2192 $this->source = plugin_manager::PLUGIN_SOURCE_EXTENSION;
2198 * Returns true if the plugin is shipped with the official distribution
2199 * of the current Moodle version, false otherwise.
2201 * @return bool
2203 public function is_standard() {
2204 return $this->source === plugin_manager::PLUGIN_SOURCE_STANDARD;
2208 * Returns true if the the given Moodle version is enough to run this plugin
2210 * @param string|int|double $moodleversion
2211 * @return bool
2213 public function is_core_dependency_satisfied($moodleversion) {
2215 if (empty($this->versionrequires)) {
2216 return true;
2218 } else {
2219 return (double)$this->versionrequires <= (double)$moodleversion;
2224 * Returns the status of the plugin
2226 * @return string one of plugin_manager::PLUGIN_STATUS_xxx constants
2228 public function get_status() {
2230 if (is_null($this->versiondb) and is_null($this->versiondisk)) {
2231 return plugin_manager::PLUGIN_STATUS_NODB;
2233 } else if (is_null($this->versiondb) and !is_null($this->versiondisk)) {
2234 return plugin_manager::PLUGIN_STATUS_NEW;
2236 } else if (!is_null($this->versiondb) and is_null($this->versiondisk)) {
2237 if (plugin_manager::is_deleted_standard_plugin($this->type, $this->name)) {
2238 return plugin_manager::PLUGIN_STATUS_DELETE;
2239 } else {
2240 return plugin_manager::PLUGIN_STATUS_MISSING;
2243 } else if ((string)$this->versiondb === (string)$this->versiondisk) {
2244 return plugin_manager::PLUGIN_STATUS_UPTODATE;
2246 } else if ($this->versiondb < $this->versiondisk) {
2247 return plugin_manager::PLUGIN_STATUS_UPGRADE;
2249 } else if ($this->versiondb > $this->versiondisk) {
2250 return plugin_manager::PLUGIN_STATUS_DOWNGRADE;
2252 } else {
2253 // $version = pi(); and similar funny jokes - hopefully Donald E. Knuth will never contribute to Moodle ;-)
2254 throw new coding_exception('Unable to determine plugin state, check the plugin versions');
2259 * Returns the information about plugin availability
2261 * True means that the plugin is enabled. False means that the plugin is
2262 * disabled. Null means that the information is not available, or the
2263 * plugin does not support configurable availability or the availability
2264 * can not be changed.
2266 * @return null|bool
2268 public function is_enabled() {
2269 return null;
2273 * Populates the property {@link $availableupdates} with the information provided by
2274 * available update checker
2276 * @param available_update_checker $provider the class providing the available update info
2278 public function check_available_updates(available_update_checker $provider) {
2279 global $CFG;
2281 if (isset($CFG->updateminmaturity)) {
2282 $minmaturity = $CFG->updateminmaturity;
2283 } else {
2284 // this can happen during the very first upgrade to 2.3
2285 $minmaturity = MATURITY_STABLE;
2288 $this->availableupdates = $provider->get_update_info($this->component,
2289 array('minmaturity' => $minmaturity));
2293 * If there are updates for this plugin available, returns them.
2295 * Returns array of {@link available_update_info} objects, if some update
2296 * is available. Returns null if there is no update available or if the update
2297 * availability is unknown.
2299 * @return array|null
2301 public function available_updates() {
2303 if (empty($this->availableupdates) or !is_array($this->availableupdates)) {
2304 return null;
2307 $updates = array();
2309 foreach ($this->availableupdates as $availableupdate) {
2310 if ($availableupdate->version > $this->versiondisk) {
2311 $updates[] = $availableupdate;
2315 if (empty($updates)) {
2316 return null;
2319 return $updates;
2323 * Returns the node name used in admin settings menu for this plugin settings (if applicable)
2325 * @return null|string node name or null if plugin does not create settings node (default)
2327 public function get_settings_section_name() {
2328 return null;
2332 * Returns the URL of the plugin settings screen
2334 * Null value means that the plugin either does not have the settings screen
2335 * or its location is not available via this library.
2337 * @return null|moodle_url
2339 public function get_settings_url() {
2340 $section = $this->get_settings_section_name();
2341 if ($section === null) {
2342 return null;
2344 $settings = admin_get_root()->locate($section);
2345 if ($settings && $settings instanceof admin_settingpage) {
2346 return new moodle_url('/admin/settings.php', array('section' => $section));
2347 } else if ($settings && $settings instanceof admin_externalpage) {
2348 return new moodle_url($settings->url);
2349 } else {
2350 return null;
2355 * Loads plugin settings to the settings tree
2357 * This function usually includes settings.php file in plugins folder.
2358 * Alternatively it can create a link to some settings page (instance of admin_externalpage)
2360 * @param part_of_admin_tree $adminroot
2361 * @param string $parentnodename
2362 * @param bool $hassiteconfig whether the current user has moodle/site:config capability
2364 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
2368 * Returns the URL of the screen where this plugin can be uninstalled
2370 * Visiting that URL must be safe, that is a manual confirmation is needed
2371 * for actual uninstallation of the plugin. Null value means that the
2372 * plugin either does not support uninstallation, or does not require any
2373 * database cleanup or the location of the screen is not available via this
2374 * library.
2376 * @return null|moodle_url
2378 public function get_uninstall_url() {
2379 return null;
2383 * Returns relative directory of the plugin with heading '/'
2385 * @return string
2387 public function get_dir() {
2388 global $CFG;
2390 return substr($this->rootdir, strlen($CFG->dirroot));
2394 * Provides access to plugin versions from {config_plugins}
2396 * @param string $plugin plugin name
2397 * @param double $disablecache optional, defaults to false
2398 * @return int|false the stored value or false if not found
2400 protected function get_version_from_config_plugins($plugin, $disablecache=false) {
2401 global $DB;
2402 static $pluginversions = null;
2404 if (is_null($pluginversions) or $disablecache) {
2405 try {
2406 $pluginversions = $DB->get_records_menu('config_plugins', array('name' => 'version'), 'plugin', 'plugin,value');
2407 } catch (dml_exception $e) {
2408 // before install
2409 $pluginversions = array();
2413 if (!array_key_exists($plugin, $pluginversions)) {
2414 return false;
2417 return $pluginversions[$plugin];
2423 * General class for all plugin types that do not have their own class
2425 class plugininfo_general extends plugininfo_base {
2430 * Class for page side blocks
2432 class plugininfo_block extends plugininfo_base {
2434 public static function get_plugins($type, $typerootdir, $typeclass) {
2436 // get the information about blocks at the disk
2437 $blocks = parent::get_plugins($type, $typerootdir, $typeclass);
2439 // add blocks missing from disk
2440 $blocksinfo = self::get_blocks_info();
2441 foreach ($blocksinfo as $blockname => $blockinfo) {
2442 if (isset($blocks[$blockname])) {
2443 continue;
2445 $plugin = new $typeclass();
2446 $plugin->type = $type;
2447 $plugin->typerootdir = $typerootdir;
2448 $plugin->name = $blockname;
2449 $plugin->rootdir = null;
2450 $plugin->displayname = $blockname;
2451 $plugin->versiondb = $blockinfo->version;
2452 $plugin->init_is_standard();
2454 $blocks[$blockname] = $plugin;
2457 return $blocks;
2461 * Magic method getter, redirects to read only values.
2463 * For block plugins pretends the object has 'visible' property for compatibility
2464 * with plugins developed for Moodle version below 2.4
2466 * @param string $name
2467 * @return mixed
2469 public function __get($name) {
2470 if ($name === 'visible') {
2471 debugging('This is now an instance of plugininfo_block, please use $block->is_enabled() instead of $block->visible', DEBUG_DEVELOPER);
2472 return ($this->is_enabled() !== false);
2474 return parent::__get($name);
2477 public function init_display_name() {
2479 if (get_string_manager()->string_exists('pluginname', 'block_' . $this->name)) {
2480 $this->displayname = get_string('pluginname', 'block_' . $this->name);
2482 } else if (($block = block_instance($this->name)) !== false) {
2483 $this->displayname = $block->get_title();
2485 } else {
2486 parent::init_display_name();
2490 public function load_db_version() {
2491 global $DB;
2493 $blocksinfo = self::get_blocks_info();
2494 if (isset($blocksinfo[$this->name]->version)) {
2495 $this->versiondb = $blocksinfo[$this->name]->version;
2499 public function is_enabled() {
2501 $blocksinfo = self::get_blocks_info();
2502 if (isset($blocksinfo[$this->name]->visible)) {
2503 if ($blocksinfo[$this->name]->visible) {
2504 return true;
2505 } else {
2506 return false;
2508 } else {
2509 return parent::is_enabled();
2513 public function get_settings_section_name() {
2514 return 'blocksetting' . $this->name;
2517 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
2518 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
2519 $ADMIN = $adminroot; // may be used in settings.php
2520 $block = $this; // also can be used inside settings.php
2521 $section = $this->get_settings_section_name();
2523 if (!$hassiteconfig || (($blockinstance = block_instance($this->name)) === false)) {
2524 return;
2527 $settings = null;
2528 if ($blockinstance->has_config()) {
2529 if (file_exists($this->full_path('settings.php'))) {
2530 $settings = new admin_settingpage($section, $this->displayname,
2531 'moodle/site:config', $this->is_enabled() === false);
2532 include($this->full_path('settings.php')); // this may also set $settings to null
2533 } else {
2534 $blocksinfo = self::get_blocks_info();
2535 $settingsurl = new moodle_url('/admin/block.php', array('block' => $blocksinfo[$this->name]->id));
2536 $settings = new admin_externalpage($section, $this->displayname,
2537 $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
2540 if ($settings) {
2541 $ADMIN->add($parentnodename, $settings);
2545 public function get_uninstall_url() {
2547 $blocksinfo = self::get_blocks_info();
2548 return new moodle_url('/admin/blocks.php', array('delete' => $blocksinfo[$this->name]->id, 'sesskey' => sesskey()));
2552 * Provides access to the records in {block} table
2554 * @param bool $disablecache do not use internal static cache
2555 * @return array array of stdClasses
2557 protected static function get_blocks_info($disablecache=false) {
2558 global $DB;
2559 static $blocksinfocache = null;
2561 if (is_null($blocksinfocache) or $disablecache) {
2562 try {
2563 $blocksinfocache = $DB->get_records('block', null, 'name', 'name,id,version,visible');
2564 } catch (dml_exception $e) {
2565 // before install
2566 $blocksinfocache = array();
2570 return $blocksinfocache;
2576 * Class for text filters
2578 class plugininfo_filter extends plugininfo_base {
2580 public static function get_plugins($type, $typerootdir, $typeclass) {
2581 global $CFG, $DB;
2583 $filters = array();
2585 // get the list of filters from both /filter and /mod location
2586 $installed = filter_get_all_installed();
2588 foreach ($installed as $filterlegacyname => $displayname) {
2589 $plugin = new $typeclass();
2590 $plugin->type = $type;
2591 $plugin->typerootdir = $typerootdir;
2592 $plugin->name = self::normalize_legacy_name($filterlegacyname);
2593 $plugin->rootdir = $CFG->dirroot . '/' . $filterlegacyname;
2594 $plugin->displayname = $displayname;
2596 $plugin->load_disk_version();
2597 $plugin->load_db_version();
2598 $plugin->load_required_main_version();
2599 $plugin->init_is_standard();
2601 $filters[$plugin->name] = $plugin;
2604 $globalstates = self::get_global_states();
2606 if ($DB->get_manager()->table_exists('filter_active')) {
2607 // if we're upgrading from 1.9, the table does not exist yet
2608 // if it does, make sure that all installed filters are registered
2609 $needsreload = false;
2610 foreach (array_keys($installed) as $filterlegacyname) {
2611 if (!isset($globalstates[self::normalize_legacy_name($filterlegacyname)])) {
2612 filter_set_global_state($filterlegacyname, TEXTFILTER_DISABLED);
2613 $needsreload = true;
2616 if ($needsreload) {
2617 $globalstates = self::get_global_states(true);
2621 // make sure that all registered filters are installed, just in case
2622 foreach ($globalstates as $name => $info) {
2623 if (!isset($filters[$name])) {
2624 // oops, there is a record in filter_active but the filter is not installed
2625 $plugin = new $typeclass();
2626 $plugin->type = $type;
2627 $plugin->typerootdir = $typerootdir;
2628 $plugin->name = $name;
2629 $plugin->rootdir = $CFG->dirroot . '/' . $info->legacyname;
2630 $plugin->displayname = $info->legacyname;
2632 $plugin->load_db_version();
2634 if (is_null($plugin->versiondb)) {
2635 // this is a hack to stimulate 'Missing from disk' error
2636 // because $plugin->versiondisk will be null !== false
2637 $plugin->versiondb = false;
2640 $filters[$plugin->name] = $plugin;
2644 return $filters;
2647 public function init_display_name() {
2648 // do nothing, the name is set in self::get_plugins()
2652 * @see load_version_php()
2654 protected function load_version_php() {
2655 if (strpos($this->name, 'mod_') === 0) {
2656 // filters bundled with modules do not have a version.php and so
2657 // do not provide their own versioning information.
2658 return new stdClass();
2660 return parent::load_version_php();
2663 public function is_enabled() {
2665 $globalstates = self::get_global_states();
2667 foreach ($globalstates as $filterlegacyname => $info) {
2668 $name = self::normalize_legacy_name($filterlegacyname);
2669 if ($name === $this->name) {
2670 if ($info->active == TEXTFILTER_DISABLED) {
2671 return false;
2672 } else {
2673 // it may be 'On' or 'Off, but available'
2674 return null;
2679 return null;
2682 public function get_settings_section_name() {
2683 $globalstates = self::get_global_states();
2684 if (!isset($globalstates[$this->name])) {
2685 return parent::get_settings_section_name();
2687 $legacyname = $globalstates[$this->name]->legacyname;
2688 return 'filtersetting' . str_replace('/', '', $legacyname);
2691 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
2692 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
2693 $ADMIN = $adminroot; // may be used in settings.php
2694 $filter = $this; // also can be used inside settings.php
2696 $globalstates = self::get_global_states();
2697 $settings = null;
2698 if ($hassiteconfig && isset($globalstates[$this->name]) && file_exists($this->full_path('filtersettings.php'))) {
2699 $section = $this->get_settings_section_name();
2700 $settings = new admin_settingpage($section, $this->displayname,
2701 'moodle/site:config', $this->is_enabled() === false);
2702 include($this->full_path('filtersettings.php')); // this may also set $settings to null
2704 if ($settings) {
2705 $ADMIN->add($parentnodename, $settings);
2709 public function get_uninstall_url() {
2711 if (strpos($this->name, 'mod_') === 0) {
2712 return null;
2713 } else {
2714 $globalstates = self::get_global_states();
2715 $legacyname = $globalstates[$this->name]->legacyname;
2716 return new moodle_url('/admin/filters.php', array('sesskey' => sesskey(), 'filterpath' => $legacyname, 'action' => 'delete'));
2721 * Convert legacy filter names like 'filter/foo' or 'mod/bar' into frankenstyle
2723 * @param string $legacyfiltername legacy filter name
2724 * @return string frankenstyle-like name
2726 protected static function normalize_legacy_name($legacyfiltername) {
2728 $name = str_replace('/', '_', $legacyfiltername);
2729 if (strpos($name, 'filter_') === 0) {
2730 $name = substr($name, 7);
2731 if (empty($name)) {
2732 throw new coding_exception('Unable to determine filter name: ' . $legacyfiltername);
2736 return $name;
2740 * Provides access to the results of {@link filter_get_global_states()}
2741 * but indexed by the normalized filter name
2743 * The legacy filter name is available as ->legacyname property.
2745 * @param bool $disablecache
2746 * @return array
2748 protected static function get_global_states($disablecache=false) {
2749 global $DB;
2750 static $globalstatescache = null;
2752 if ($disablecache or is_null($globalstatescache)) {
2754 if (!$DB->get_manager()->table_exists('filter_active')) {
2755 // we're upgrading from 1.9 and the table used by {@link filter_get_global_states()}
2756 // does not exist yet
2757 $globalstatescache = array();
2759 } else {
2760 foreach (filter_get_global_states() as $legacyname => $info) {
2761 $name = self::normalize_legacy_name($legacyname);
2762 $filterinfo = new stdClass();
2763 $filterinfo->legacyname = $legacyname;
2764 $filterinfo->active = $info->active;
2765 $filterinfo->sortorder = $info->sortorder;
2766 $globalstatescache[$name] = $filterinfo;
2771 return $globalstatescache;
2777 * Class for activity modules
2779 class plugininfo_mod extends plugininfo_base {
2781 public static function get_plugins($type, $typerootdir, $typeclass) {
2783 // get the information about plugins at the disk
2784 $modules = parent::get_plugins($type, $typerootdir, $typeclass);
2786 // add modules missing from disk
2787 $modulesinfo = self::get_modules_info();
2788 foreach ($modulesinfo as $modulename => $moduleinfo) {
2789 if (isset($modules[$modulename])) {
2790 continue;
2792 $plugin = new $typeclass();
2793 $plugin->type = $type;
2794 $plugin->typerootdir = $typerootdir;
2795 $plugin->name = $modulename;
2796 $plugin->rootdir = null;
2797 $plugin->displayname = $modulename;
2798 $plugin->versiondb = $moduleinfo->version;
2799 $plugin->init_is_standard();
2801 $modules[$modulename] = $plugin;
2804 return $modules;
2808 * Magic method getter, redirects to read only values.
2810 * For module plugins we pretend the object has 'visible' property for compatibility
2811 * with plugins developed for Moodle version below 2.4
2813 * @param string $name
2814 * @return mixed
2816 public function __get($name) {
2817 if ($name === 'visible') {
2818 debugging('This is now an instance of plugininfo_mod, please use $module->is_enabled() instead of $module->visible', DEBUG_DEVELOPER);
2819 return ($this->is_enabled() !== false);
2821 return parent::__get($name);
2824 public function init_display_name() {
2825 if (get_string_manager()->string_exists('pluginname', $this->component)) {
2826 $this->displayname = get_string('pluginname', $this->component);
2827 } else {
2828 $this->displayname = get_string('modulename', $this->component);
2833 * Load the data from version.php.
2834 * @return object the data object defined in version.php.
2836 protected function load_version_php() {
2837 $versionfile = $this->full_path('version.php');
2839 $module = new stdClass();
2840 if (is_readable($versionfile)) {
2841 include($versionfile);
2843 return $module;
2846 public function load_db_version() {
2847 global $DB;
2849 $modulesinfo = self::get_modules_info();
2850 if (isset($modulesinfo[$this->name]->version)) {
2851 $this->versiondb = $modulesinfo[$this->name]->version;
2855 public function is_enabled() {
2857 $modulesinfo = self::get_modules_info();
2858 if (isset($modulesinfo[$this->name]->visible)) {
2859 if ($modulesinfo[$this->name]->visible) {
2860 return true;
2861 } else {
2862 return false;
2864 } else {
2865 return parent::is_enabled();
2869 public function get_settings_section_name() {
2870 return 'modsetting' . $this->name;
2873 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
2874 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
2875 $ADMIN = $adminroot; // may be used in settings.php
2876 $module = $this; // also can be used inside settings.php
2877 $section = $this->get_settings_section_name();
2879 $modulesinfo = self::get_modules_info();
2880 $settings = null;
2881 if ($hassiteconfig && isset($modulesinfo[$this->name]) && file_exists($this->full_path('settings.php'))) {
2882 $settings = new admin_settingpage($section, $this->displayname,
2883 'moodle/site:config', $this->is_enabled() === false);
2884 include($this->full_path('settings.php')); // this may also set $settings to null
2886 if ($settings) {
2887 $ADMIN->add($parentnodename, $settings);
2891 public function get_uninstall_url() {
2893 if ($this->name !== 'forum') {
2894 return new moodle_url('/admin/modules.php', array('delete' => $this->name, 'sesskey' => sesskey()));
2895 } else {
2896 return null;
2901 * Provides access to the records in {modules} table
2903 * @param bool $disablecache do not use internal static cache
2904 * @return array array of stdClasses
2906 protected static function get_modules_info($disablecache=false) {
2907 global $DB;
2908 static $modulesinfocache = null;
2910 if (is_null($modulesinfocache) or $disablecache) {
2911 try {
2912 $modulesinfocache = $DB->get_records('modules', null, 'name', 'name,id,version,visible');
2913 } catch (dml_exception $e) {
2914 // before install
2915 $modulesinfocache = array();
2919 return $modulesinfocache;
2925 * Class for question behaviours.
2927 class plugininfo_qbehaviour extends plugininfo_base {
2929 public function get_uninstall_url() {
2930 return new moodle_url('/admin/qbehaviours.php',
2931 array('delete' => $this->name, 'sesskey' => sesskey()));
2937 * Class for question types
2939 class plugininfo_qtype extends plugininfo_base {
2941 public function get_uninstall_url() {
2942 return new moodle_url('/admin/qtypes.php',
2943 array('delete' => $this->name, 'sesskey' => sesskey()));
2946 public function get_settings_section_name() {
2947 return 'qtypesetting' . $this->name;
2950 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
2951 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
2952 $ADMIN = $adminroot; // may be used in settings.php
2953 $qtype = $this; // also can be used inside settings.php
2954 $section = $this->get_settings_section_name();
2956 $settings = null;
2957 $systemcontext = context_system::instance();
2958 if (($hassiteconfig || has_capability('moodle/question:config', $systemcontext)) &&
2959 file_exists($this->full_path('settings.php'))) {
2960 $settings = new admin_settingpage($section, $this->displayname,
2961 'moodle/question:config', $this->is_enabled() === false);
2962 include($this->full_path('settings.php')); // this may also set $settings to null
2964 if ($settings) {
2965 $ADMIN->add($parentnodename, $settings);
2972 * Class for authentication plugins
2974 class plugininfo_auth extends plugininfo_base {
2976 public function is_enabled() {
2977 global $CFG;
2978 /** @var null|array list of enabled authentication plugins */
2979 static $enabled = null;
2981 if (in_array($this->name, array('nologin', 'manual'))) {
2982 // these two are always enabled and can't be disabled
2983 return null;
2986 if (is_null($enabled)) {
2987 $enabled = array_flip(explode(',', $CFG->auth));
2990 return isset($enabled[$this->name]);
2993 public function get_settings_section_name() {
2994 return 'authsetting' . $this->name;
2997 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
2998 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
2999 $ADMIN = $adminroot; // may be used in settings.php
3000 $auth = $this; // also to be used inside settings.php
3001 $section = $this->get_settings_section_name();
3003 $settings = null;
3004 if ($hassiteconfig) {
3005 if (file_exists($this->full_path('settings.php'))) {
3006 // TODO: finish implementation of common settings - locking, etc.
3007 $settings = new admin_settingpage($section, $this->displayname,
3008 'moodle/site:config', $this->is_enabled() === false);
3009 include($this->full_path('settings.php')); // this may also set $settings to null
3010 } else {
3011 $settingsurl = new moodle_url('/admin/auth_config.php', array('auth' => $this->name));
3012 $settings = new admin_externalpage($section, $this->displayname,
3013 $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
3016 if ($settings) {
3017 $ADMIN->add($parentnodename, $settings);
3024 * Class for enrolment plugins
3026 class plugininfo_enrol extends plugininfo_base {
3028 public function is_enabled() {
3029 global $CFG;
3030 /** @var null|array list of enabled enrolment plugins */
3031 static $enabled = null;
3033 // We do not actually need whole enrolment classes here so we do not call
3034 // {@link enrol_get_plugins()}. Note that this may produce slightly different
3035 // results, for example if the enrolment plugin does not contain lib.php
3036 // but it is listed in $CFG->enrol_plugins_enabled
3038 if (is_null($enabled)) {
3039 $enabled = array_flip(explode(',', $CFG->enrol_plugins_enabled));
3042 return isset($enabled[$this->name]);
3045 public function get_settings_section_name() {
3046 return 'enrolsettings' . $this->name;
3049 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3050 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3051 $ADMIN = $adminroot; // may be used in settings.php
3052 $enrol = $this; // also can be used inside settings.php
3053 $section = $this->get_settings_section_name();
3055 $settings = null;
3056 if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3057 $settings = new admin_settingpage($section, $this->displayname,
3058 'moodle/site:config', $this->is_enabled() === false);
3059 include($this->full_path('settings.php')); // this may also set $settings to null
3061 if ($settings) {
3062 $ADMIN->add($parentnodename, $settings);
3066 public function get_uninstall_url() {
3067 return new moodle_url('/admin/enrol.php', array('action' => 'uninstall', 'enrol' => $this->name, 'sesskey' => sesskey()));
3073 * Class for messaging processors
3075 class plugininfo_message extends plugininfo_base {
3077 public function get_settings_section_name() {
3078 return 'messagesetting' . $this->name;
3081 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3082 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3083 $ADMIN = $adminroot; // may be used in settings.php
3084 if (!$hassiteconfig) {
3085 return;
3087 $section = $this->get_settings_section_name();
3089 $settings = null;
3090 $processors = get_message_processors();
3091 if (isset($processors[$this->name])) {
3092 $processor = $processors[$this->name];
3093 if ($processor->available && $processor->hassettings) {
3094 $settings = new admin_settingpage($section, $this->displayname,
3095 'moodle/site:config', $this->is_enabled() === false);
3096 include($this->full_path('settings.php')); // this may also set $settings to null
3099 if ($settings) {
3100 $ADMIN->add($parentnodename, $settings);
3105 * @see plugintype_interface::is_enabled()
3107 public function is_enabled() {
3108 $processors = get_message_processors();
3109 if (isset($processors[$this->name])) {
3110 return $processors[$this->name]->configured && $processors[$this->name]->enabled;
3111 } else {
3112 return parent::is_enabled();
3117 * @see plugintype_interface::get_uninstall_url()
3119 public function get_uninstall_url() {
3120 $processors = get_message_processors();
3121 if (isset($processors[$this->name])) {
3122 return new moodle_url('/admin/message.php', array('uninstall' => $processors[$this->name]->id, 'sesskey' => sesskey()));
3123 } else {
3124 return parent::get_uninstall_url();
3131 * Class for repositories
3133 class plugininfo_repository extends plugininfo_base {
3135 public function is_enabled() {
3137 $enabled = self::get_enabled_repositories();
3139 return isset($enabled[$this->name]);
3142 public function get_settings_section_name() {
3143 return 'repositorysettings'.$this->name;
3146 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3147 if ($hassiteconfig && $this->is_enabled()) {
3148 // completely no access to repository setting when it is not enabled
3149 $sectionname = $this->get_settings_section_name();
3150 $settingsurl = new moodle_url('/admin/repository.php',
3151 array('sesskey' => sesskey(), 'action' => 'edit', 'repos' => $this->name));
3152 $settings = new admin_externalpage($sectionname, $this->displayname,
3153 $settingsurl, 'moodle/site:config', false);
3154 $adminroot->add($parentnodename, $settings);
3159 * Provides access to the records in {repository} table
3161 * @param bool $disablecache do not use internal static cache
3162 * @return array array of stdClasses
3164 protected static function get_enabled_repositories($disablecache=false) {
3165 global $DB;
3166 static $repositories = null;
3168 if (is_null($repositories) or $disablecache) {
3169 $repositories = $DB->get_records('repository', null, 'type', 'type,visible,sortorder');
3172 return $repositories;
3178 * Class for portfolios
3180 class plugininfo_portfolio extends plugininfo_base {
3182 public function is_enabled() {
3184 $enabled = self::get_enabled_portfolios();
3186 return isset($enabled[$this->name]);
3190 * Provides access to the records in {portfolio_instance} table
3192 * @param bool $disablecache do not use internal static cache
3193 * @return array array of stdClasses
3195 protected static function get_enabled_portfolios($disablecache=false) {
3196 global $DB;
3197 static $portfolios = null;
3199 if (is_null($portfolios) or $disablecache) {
3200 $portfolios = array();
3201 $instances = $DB->get_recordset('portfolio_instance', null, 'plugin');
3202 foreach ($instances as $instance) {
3203 if (isset($portfolios[$instance->plugin])) {
3204 if ($instance->visible) {
3205 $portfolios[$instance->plugin]->visible = $instance->visible;
3207 } else {
3208 $portfolios[$instance->plugin] = $instance;
3213 return $portfolios;
3219 * Class for themes
3221 class plugininfo_theme extends plugininfo_base {
3223 public function is_enabled() {
3224 global $CFG;
3226 if ((!empty($CFG->theme) and $CFG->theme === $this->name) or
3227 (!empty($CFG->themelegacy) and $CFG->themelegacy === $this->name)) {
3228 return true;
3229 } else {
3230 return parent::is_enabled();
3237 * Class representing an MNet service
3239 class plugininfo_mnetservice extends plugininfo_base {
3241 public function is_enabled() {
3242 global $CFG;
3244 if (empty($CFG->mnet_dispatcher_mode) || $CFG->mnet_dispatcher_mode !== 'strict') {
3245 return false;
3246 } else {
3247 return parent::is_enabled();
3254 * Class for admin tool plugins
3256 class plugininfo_tool extends plugininfo_base {
3258 public function get_uninstall_url() {
3259 return new moodle_url('/admin/tools.php', array('delete' => $this->name, 'sesskey' => sesskey()));
3265 * Class for admin tool plugins
3267 class plugininfo_report extends plugininfo_base {
3269 public function get_uninstall_url() {
3270 return new moodle_url('/admin/reports.php', array('delete' => $this->name, 'sesskey' => sesskey()));
3276 * Class for local plugins
3278 class plugininfo_local extends plugininfo_base {
3280 public function get_uninstall_url() {
3281 return new moodle_url('/admin/localplugins.php', array('delete' => $this->name, 'sesskey' => sesskey()));
3286 * Class for HTML editors
3288 class plugininfo_editor extends plugininfo_base {
3290 public function get_settings_section_name() {
3291 return 'editorsettings' . $this->name;
3294 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3295 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3296 $ADMIN = $adminroot; // may be used in settings.php
3297 $editor = $this; // also can be used inside settings.php
3298 $section = $this->get_settings_section_name();
3300 $settings = null;
3301 if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3302 $settings = new admin_settingpage($section, $this->displayname,
3303 'moodle/site:config', $this->is_enabled() === false);
3304 include($this->full_path('settings.php')); // this may also set $settings to null
3306 if ($settings) {
3307 $ADMIN->add($parentnodename, $settings);
3312 * Returns the information about plugin availability
3314 * True means that the plugin is enabled. False means that the plugin is
3315 * disabled. Null means that the information is not available, or the
3316 * plugin does not support configurable availability or the availability
3317 * can not be changed.
3319 * @return null|bool
3321 public function is_enabled() {
3322 global $CFG;
3323 if (empty($CFG->texteditors)) {
3324 $CFG->texteditors = 'tinymce,textarea';
3326 if (in_array($this->name, explode(',', $CFG->texteditors))) {
3327 return true;
3329 return false;
3334 * Class for plagiarism plugins
3336 class plugininfo_plagiarism extends plugininfo_base {
3338 public function get_settings_section_name() {
3339 return 'plagiarism'. $this->name;
3342 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3343 // plagiarism plugin just redirect to settings.php in the plugins directory
3344 if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3345 $section = $this->get_settings_section_name();
3346 $settingsurl = new moodle_url($this->get_dir().'/settings.php');
3347 $settings = new admin_externalpage($section, $this->displayname,
3348 $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
3349 $adminroot->add($parentnodename, $settings);
3355 * Class for webservice protocols
3357 class plugininfo_webservice extends plugininfo_base {
3359 public function get_settings_section_name() {
3360 return 'webservicesetting' . $this->name;
3363 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3364 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3365 $ADMIN = $adminroot; // may be used in settings.php
3366 $webservice = $this; // also can be used inside settings.php
3367 $section = $this->get_settings_section_name();
3369 $settings = null;
3370 if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3371 $settings = new admin_settingpage($section, $this->displayname,
3372 'moodle/site:config', $this->is_enabled() === false);
3373 include($this->full_path('settings.php')); // this may also set $settings to null
3375 if ($settings) {
3376 $ADMIN->add($parentnodename, $settings);
3380 public function is_enabled() {
3381 global $CFG;
3382 if (empty($CFG->enablewebservices)) {
3383 return false;
3385 $active_webservices = empty($CFG->webserviceprotocols) ? array() : explode(',', $CFG->webserviceprotocols);
3386 if (in_array($this->name, $active_webservices)) {
3387 return true;
3389 return false;
3392 public function get_uninstall_url() {
3393 return new moodle_url('/admin/webservice/protocols.php',
3394 array('sesskey' => sesskey(), 'action' => 'uninstall', 'webservice' => $this->name));
3399 * Class for course formats
3401 class plugininfo_format extends plugininfo_base {
3404 * Gathers and returns the information about all plugins of the given type
3406 * @param string $type the name of the plugintype, eg. mod, auth or workshopform
3407 * @param string $typerootdir full path to the location of the plugin dir
3408 * @param string $typeclass the name of the actually called class
3409 * @return array of plugintype classes, indexed by the plugin name
3411 public static function get_plugins($type, $typerootdir, $typeclass) {
3412 global $CFG;
3413 $formats = parent::get_plugins($type, $typerootdir, $typeclass);
3414 require_once($CFG->dirroot.'/course/lib.php');
3415 $order = get_sorted_course_formats();
3416 $sortedformats = array();
3417 foreach ($order as $formatname) {
3418 $sortedformats[$formatname] = $formats[$formatname];
3420 return $sortedformats;
3423 public function get_settings_section_name() {
3424 return 'formatsetting' . $this->name;
3427 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3428 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3429 $ADMIN = $adminroot; // also may be used in settings.php
3430 $section = $this->get_settings_section_name();
3432 $settings = null;
3433 if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3434 $settings = new admin_settingpage($section, $this->displayname,
3435 'moodle/site:config', $this->is_enabled() === false);
3436 include($this->full_path('settings.php')); // this may also set $settings to null
3438 if ($settings) {
3439 $ADMIN->add($parentnodename, $settings);
3443 public function is_enabled() {
3444 return !get_config($this->component, 'disabled');
3447 public function get_uninstall_url() {
3448 if ($this->name !== get_config('moodlecourse', 'format') && $this->name !== 'site') {
3449 return new moodle_url('/admin/courseformats.php',
3450 array('sesskey' => sesskey(), 'action' => 'uninstall', 'format' => $this->name));
3452 return parent::get_uninstall_url();