MDL-36720 Fetch available updates info via HTTPS
[moodle.git] / lib / pluginlib.php
blob7383d7b8898b828cb739614bf3d8c45de0300c33
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());
830 $curlinfo = $curl->get_info();
831 if ($curlinfo['http_code'] != 200) {
832 throw new available_update_checker_exception('err_response_http_code', $curlinfo['http_code']);
834 return $response;
838 * Makes sure the response is valid, has correct API format etc.
840 * @param string $response raw response as returned by the {@link self::get_response()}
841 * @throws available_update_checker_exception
843 protected function validate_response($response) {
845 $response = $this->decode_response($response);
847 if (empty($response)) {
848 throw new available_update_checker_exception('err_response_empty');
851 if (empty($response['status']) or $response['status'] !== 'OK') {
852 throw new available_update_checker_exception('err_response_status', $response['status']);
855 if (empty($response['apiver']) or $response['apiver'] !== '1.1') {
856 throw new available_update_checker_exception('err_response_format_version', $response['apiver']);
859 if (empty($response['forbranch']) or $response['forbranch'] !== moodle_major_version(true)) {
860 throw new available_update_checker_exception('err_response_target_version', $response['forbranch']);
865 * Decodes the raw string response from the update notifications provider
867 * @param string $response as returned by {@link self::get_response()}
868 * @return array decoded response structure
870 protected function decode_response($response) {
871 return json_decode($response, true);
875 * Stores the valid fetched response for later usage
877 * This implementation uses the config_plugins table as the permanent storage.
879 * @param string $response raw valid data returned by {@link self::get_response()}
881 protected function store_response($response) {
883 set_config('recentfetch', time(), 'core_plugin');
884 set_config('recentresponse', $response, 'core_plugin');
886 $this->restore_response(true);
890 * Loads the most recent raw response record we have fetched
892 * After this method is called, $this->recentresponse is set to an array. If the
893 * array is empty, then either no data have been fetched yet or the fetched data
894 * do not have expected format (and thence they are ignored and a debugging
895 * message is displayed).
897 * This implementation uses the config_plugins table as the permanent storage.
899 * @param bool $forcereload reload even if it was already loaded
901 protected function restore_response($forcereload = false) {
903 if (!$forcereload and !is_null($this->recentresponse)) {
904 // we already have it, nothing to do
905 return;
908 $config = get_config('core_plugin');
910 if (!empty($config->recentresponse) and !empty($config->recentfetch)) {
911 try {
912 $this->validate_response($config->recentresponse);
913 $this->recentfetch = $config->recentfetch;
914 $this->recentresponse = $this->decode_response($config->recentresponse);
915 } catch (available_update_checker_exception $e) {
916 // The server response is not valid. Behave as if no data were fetched yet.
917 // This may happen when the most recent update info (cached locally) has been
918 // fetched with the previous branch of Moodle (like during an upgrade from 2.x
919 // to 2.y) or when the API of the response has changed.
920 $this->recentresponse = array();
923 } else {
924 $this->recentresponse = array();
929 * Compares two raw {@link $recentresponse} records and returns the list of changed updates
931 * This method is used to populate potential update info to be sent to site admins.
933 * @param array $old
934 * @param array $new
935 * @throws available_update_checker_exception
936 * @return array parts of $new['updates'] that have changed
938 protected function compare_responses(array $old, array $new) {
940 if (empty($new)) {
941 return array();
944 if (!array_key_exists('updates', $new)) {
945 throw new available_update_checker_exception('err_response_format');
948 if (empty($old)) {
949 return $new['updates'];
952 if (!array_key_exists('updates', $old)) {
953 throw new available_update_checker_exception('err_response_format');
956 $changes = array();
958 foreach ($new['updates'] as $newcomponent => $newcomponentupdates) {
959 if (empty($old['updates'][$newcomponent])) {
960 $changes[$newcomponent] = $newcomponentupdates;
961 continue;
963 foreach ($newcomponentupdates as $newcomponentupdate) {
964 $inold = false;
965 foreach ($old['updates'][$newcomponent] as $oldcomponentupdate) {
966 if ($newcomponentupdate['version'] == $oldcomponentupdate['version']) {
967 $inold = true;
970 if (!$inold) {
971 if (!isset($changes[$newcomponent])) {
972 $changes[$newcomponent] = array();
974 $changes[$newcomponent][] = $newcomponentupdate;
979 return $changes;
983 * Returns the URL to send update requests to
985 * During the development or testing, you can set $CFG->alternativeupdateproviderurl
986 * to a custom URL that will be used. Otherwise the standard URL will be returned.
988 * @return string URL
990 protected function prepare_request_url() {
991 global $CFG;
993 if (!empty($CFG->config_php_settings['alternativeupdateproviderurl'])) {
994 return $CFG->config_php_settings['alternativeupdateproviderurl'];
995 } else {
996 return 'https://download.moodle.org/api/1.1/updates.php';
1001 * Sets the properties currentversion, currentrelease, currentbranch and currentplugins
1003 * @param bool $forcereload
1005 protected function load_current_environment($forcereload=false) {
1006 global $CFG;
1008 if (!is_null($this->currentversion) and !$forcereload) {
1009 // nothing to do
1010 return;
1013 $version = null;
1014 $release = null;
1016 require($CFG->dirroot.'/version.php');
1017 $this->currentversion = $version;
1018 $this->currentrelease = $release;
1019 $this->currentbranch = moodle_major_version(true);
1021 $pluginman = plugin_manager::instance();
1022 foreach ($pluginman->get_plugins() as $type => $plugins) {
1023 foreach ($plugins as $plugin) {
1024 if (!$plugin->is_standard()) {
1025 $this->currentplugins[$plugin->component] = $plugin->versiondisk;
1032 * Returns the list of HTTP params to be sent to the updates provider URL
1034 * @return array of (string)param => (string)value
1036 protected function prepare_request_params() {
1037 global $CFG;
1039 $this->load_current_environment();
1040 $this->restore_response();
1042 $params = array();
1043 $params['format'] = 'json';
1045 if (isset($this->recentresponse['ticket'])) {
1046 $params['ticket'] = $this->recentresponse['ticket'];
1049 if (isset($this->currentversion)) {
1050 $params['version'] = $this->currentversion;
1051 } else {
1052 throw new coding_exception('Main Moodle version must be already known here');
1055 if (isset($this->currentbranch)) {
1056 $params['branch'] = $this->currentbranch;
1057 } else {
1058 throw new coding_exception('Moodle release must be already known here');
1061 $plugins = array();
1062 foreach ($this->currentplugins as $plugin => $version) {
1063 $plugins[] = $plugin.'@'.$version;
1065 if (!empty($plugins)) {
1066 $params['plugins'] = implode(',', $plugins);
1069 return $params;
1073 * Returns the current timestamp
1075 * @return int the timestamp
1077 protected function cron_current_timestamp() {
1078 return time();
1082 * Output cron debugging info
1084 * @see mtrace()
1085 * @param string $msg output message
1086 * @param string $eol end of line
1088 protected function cron_mtrace($msg, $eol = PHP_EOL) {
1089 mtrace($msg, $eol);
1093 * Decide if the autocheck feature is disabled in the server setting
1095 * @return bool true if autocheck enabled, false if disabled
1097 protected function cron_autocheck_enabled() {
1098 global $CFG;
1100 if (empty($CFG->updateautocheck)) {
1101 return false;
1102 } else {
1103 return true;
1108 * Decide if the recently fetched data are still fresh enough
1110 * @param int $now current timestamp
1111 * @return bool true if no need to re-fetch, false otherwise
1113 protected function cron_has_fresh_fetch($now) {
1114 $recent = $this->get_last_timefetched();
1116 if (empty($recent)) {
1117 return false;
1120 if ($now < $recent) {
1121 $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1122 return true;
1125 if ($now - $recent > 24 * HOURSECS) {
1126 return false;
1129 return true;
1133 * Decide if the fetch is outadated or even missing
1135 * @param int $now current timestamp
1136 * @return bool false if no need to re-fetch, true otherwise
1138 protected function cron_has_outdated_fetch($now) {
1139 $recent = $this->get_last_timefetched();
1141 if (empty($recent)) {
1142 return true;
1145 if ($now < $recent) {
1146 $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1147 return false;
1150 if ($now - $recent > 48 * HOURSECS) {
1151 return true;
1154 return false;
1158 * Returns the cron execution offset for this site
1160 * The main {@link self::cron()} is supposed to run every night in some random time
1161 * between 01:00 and 06:00 AM (local time). The exact moment is defined by so called
1162 * execution offset, that is the amount of time after 01:00 AM. The offset value is
1163 * initially generated randomly and then used consistently at the site. This way, the
1164 * regular checks against the download.moodle.org server are spread in time.
1166 * @return int the offset number of seconds from range 1 sec to 5 hours
1168 protected function cron_execution_offset() {
1169 global $CFG;
1171 if (empty($CFG->updatecronoffset)) {
1172 set_config('updatecronoffset', rand(1, 5 * HOURSECS));
1175 return $CFG->updatecronoffset;
1179 * Fetch available updates info and eventually send notification to site admins
1181 protected function cron_execute() {
1183 try {
1184 $this->restore_response();
1185 $previous = $this->recentresponse;
1186 $this->fetch();
1187 $this->restore_response(true);
1188 $current = $this->recentresponse;
1189 $changes = $this->compare_responses($previous, $current);
1190 $notifications = $this->cron_notifications($changes);
1191 $this->cron_notify($notifications);
1192 $this->cron_mtrace('done');
1193 } catch (available_update_checker_exception $e) {
1194 $this->cron_mtrace('FAILED!');
1199 * Given the list of changes in available updates, pick those to send to site admins
1201 * @param array $changes as returned by {@link self::compare_responses()}
1202 * @return array of available_update_info objects to send to site admins
1204 protected function cron_notifications(array $changes) {
1205 global $CFG;
1207 $notifications = array();
1208 $pluginman = plugin_manager::instance();
1209 $plugins = $pluginman->get_plugins(true);
1211 foreach ($changes as $component => $componentchanges) {
1212 if (empty($componentchanges)) {
1213 continue;
1215 $componentupdates = $this->get_update_info($component,
1216 array('minmaturity' => $CFG->updateminmaturity, 'notifybuilds' => $CFG->updatenotifybuilds));
1217 if (empty($componentupdates)) {
1218 continue;
1220 // notify only about those $componentchanges that are present in $componentupdates
1221 // to respect the preferences
1222 foreach ($componentchanges as $componentchange) {
1223 foreach ($componentupdates as $componentupdate) {
1224 if ($componentupdate->version == $componentchange['version']) {
1225 if ($component == 'core') {
1226 // in case of 'core' this is enough, we already know that the
1227 // $componentupdate is a real update with higher version
1228 $notifications[] = $componentupdate;
1229 } else {
1230 // use the plugin_manager to check if the reported $componentchange
1231 // is a real update with higher version. such a real update must be
1232 // present in the 'availableupdates' property of one of the component's
1233 // available_update_info object
1234 list($plugintype, $pluginname) = normalize_component($component);
1235 if (!empty($plugins[$plugintype][$pluginname]->availableupdates)) {
1236 foreach ($plugins[$plugintype][$pluginname]->availableupdates as $availableupdate) {
1237 if ($availableupdate->version == $componentchange['version']) {
1238 $notifications[] = $componentupdate;
1248 return $notifications;
1252 * Sends the given notifications to site admins via messaging API
1254 * @param array $notifications array of available_update_info objects to send
1256 protected function cron_notify(array $notifications) {
1257 global $CFG;
1259 if (empty($notifications)) {
1260 return;
1263 $admins = get_admins();
1265 if (empty($admins)) {
1266 return;
1269 $this->cron_mtrace('sending notifications ... ', '');
1271 $text = get_string('updatenotifications', 'core_admin') . PHP_EOL;
1272 $html = html_writer::tag('h1', get_string('updatenotifications', 'core_admin')) . PHP_EOL;
1274 $coreupdates = array();
1275 $pluginupdates = array();
1277 foreach ($notifications as $notification) {
1278 if ($notification->component == 'core') {
1279 $coreupdates[] = $notification;
1280 } else {
1281 $pluginupdates[] = $notification;
1285 if (!empty($coreupdates)) {
1286 $text .= PHP_EOL . get_string('updateavailable', 'core_admin') . PHP_EOL;
1287 $html .= html_writer::tag('h2', get_string('updateavailable', 'core_admin')) . PHP_EOL;
1288 $html .= html_writer::start_tag('ul') . PHP_EOL;
1289 foreach ($coreupdates as $coreupdate) {
1290 $html .= html_writer::start_tag('li');
1291 if (isset($coreupdate->release)) {
1292 $text .= get_string('updateavailable_release', 'core_admin', $coreupdate->release);
1293 $html .= html_writer::tag('strong', get_string('updateavailable_release', 'core_admin', $coreupdate->release));
1295 if (isset($coreupdate->version)) {
1296 $text .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
1297 $html .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
1299 if (isset($coreupdate->maturity)) {
1300 $text .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
1301 $html .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
1303 $text .= PHP_EOL;
1304 $html .= html_writer::end_tag('li') . PHP_EOL;
1306 $text .= PHP_EOL;
1307 $html .= html_writer::end_tag('ul') . PHP_EOL;
1309 $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/index.php');
1310 $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
1311 $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/index.php', $CFG->wwwroot.'/'.$CFG->admin.'/index.php'));
1312 $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
1315 if (!empty($pluginupdates)) {
1316 $text .= PHP_EOL . get_string('updateavailableforplugin', 'core_admin') . PHP_EOL;
1317 $html .= html_writer::tag('h2', get_string('updateavailableforplugin', 'core_admin')) . PHP_EOL;
1319 $html .= html_writer::start_tag('ul') . PHP_EOL;
1320 foreach ($pluginupdates as $pluginupdate) {
1321 $html .= html_writer::start_tag('li');
1322 $text .= get_string('pluginname', $pluginupdate->component);
1323 $html .= html_writer::tag('strong', get_string('pluginname', $pluginupdate->component));
1325 $text .= ' ('.$pluginupdate->component.')';
1326 $html .= ' ('.$pluginupdate->component.')';
1328 $text .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
1329 $html .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
1331 $text .= PHP_EOL;
1332 $html .= html_writer::end_tag('li') . PHP_EOL;
1334 $text .= PHP_EOL;
1335 $html .= html_writer::end_tag('ul') . PHP_EOL;
1337 $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php');
1338 $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
1339 $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/plugins.php', $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php'));
1340 $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
1343 $a = array('siteurl' => $CFG->wwwroot);
1344 $text .= get_string('updatenotificationfooter', 'core_admin', $a) . PHP_EOL;
1345 $a = array('siteurl' => html_writer::link($CFG->wwwroot, $CFG->wwwroot));
1346 $html .= html_writer::tag('footer', html_writer::tag('p', get_string('updatenotificationfooter', 'core_admin', $a),
1347 array('style' => 'font-size:smaller; color:#333;')));
1349 foreach ($admins as $admin) {
1350 $message = new stdClass();
1351 $message->component = 'moodle';
1352 $message->name = 'availableupdate';
1353 $message->userfrom = get_admin();
1354 $message->userto = $admin;
1355 $message->subject = get_string('updatenotificationsubject', 'core_admin', array('siteurl' => $CFG->wwwroot));
1356 $message->fullmessage = $text;
1357 $message->fullmessageformat = FORMAT_PLAIN;
1358 $message->fullmessagehtml = $html;
1359 $message->smallmessage = get_string('updatenotifications', 'core_admin');
1360 $message->notification = 1;
1361 message_send($message);
1366 * Compare two release labels and decide if they are the same
1368 * @param string $remote release info of the available update
1369 * @param null|string $local release info of the local code, defaults to $release defined in version.php
1370 * @return boolean true if the releases declare the same minor+major version
1372 protected function is_same_release($remote, $local=null) {
1374 if (is_null($local)) {
1375 $this->load_current_environment();
1376 $local = $this->currentrelease;
1379 $pattern = '/^([0-9\.\+]+)([^(]*)/';
1381 preg_match($pattern, $remote, $remotematches);
1382 preg_match($pattern, $local, $localmatches);
1384 $remotematches[1] = str_replace('+', '', $remotematches[1]);
1385 $localmatches[1] = str_replace('+', '', $localmatches[1]);
1387 if ($remotematches[1] === $localmatches[1] and rtrim($remotematches[2]) === rtrim($localmatches[2])) {
1388 return true;
1389 } else {
1390 return false;
1397 * Defines the structure of objects returned by {@link available_update_checker::get_update_info()}
1399 class available_update_info {
1401 /** @var string frankenstyle component name */
1402 public $component;
1403 /** @var int the available version of the component */
1404 public $version;
1405 /** @var string|null optional release name */
1406 public $release = null;
1407 /** @var int|null optional maturity info, eg {@link MATURITY_STABLE} */
1408 public $maturity = null;
1409 /** @var string|null optional URL of a page with more info about the update */
1410 public $url = null;
1411 /** @var string|null optional URL of a ZIP package that can be downloaded and installed */
1412 public $download = null;
1413 /** @var string|null of self::download is set, then this must be the MD5 hash of the ZIP */
1414 public $downloadmd5 = null;
1417 * Creates new instance of the class
1419 * The $info array must provide at least the 'version' value and optionally all other
1420 * values to populate the object's properties.
1422 * @param string $name the frankenstyle component name
1423 * @param array $info associative array with other properties
1425 public function __construct($name, array $info) {
1426 $this->component = $name;
1427 foreach ($info as $k => $v) {
1428 if (property_exists('available_update_info', $k) and $k != 'component') {
1429 $this->$k = $v;
1437 * Implements a communication bridge to the mdeploy.php utility
1439 class available_update_deployer {
1441 const HTTP_PARAM_PREFIX = 'updteautodpldata_'; // Hey, even Google has not heard of such a prefix! So it MUST be safe :-p
1442 const HTTP_PARAM_CHECKER = 'datapackagesize'; // Name of the parameter that holds the number of items in the received data items
1444 /** @var available_update_deployer holds the singleton instance */
1445 protected static $singletoninstance;
1446 /** @var moodle_url URL of a page that includes the deployer UI */
1447 protected $callerurl;
1448 /** @var moodle_url URL to return after the deployment */
1449 protected $returnurl;
1452 * Direct instantiation not allowed, use the factory method {@link self::instance()}
1454 protected function __construct() {
1458 * Sorry, this is singleton
1460 protected function __clone() {
1464 * Factory method for this class
1466 * @return available_update_deployer the singleton instance
1468 public static function instance() {
1469 if (is_null(self::$singletoninstance)) {
1470 self::$singletoninstance = new self();
1472 return self::$singletoninstance;
1476 * Reset caches used by this script
1478 * @param bool $phpunitreset is this called as a part of PHPUnit reset?
1480 public static function reset_caches($phpunitreset = false) {
1481 if ($phpunitreset) {
1482 self::$singletoninstance = null;
1487 * Is automatic deployment enabled?
1489 * @return bool
1491 public function enabled() {
1492 global $CFG;
1494 if (!empty($CFG->disableupdateautodeploy)) {
1495 // The feature is prohibited via config.php
1496 return false;
1499 return get_config('updateautodeploy');
1503 * Sets some base properties of the class to make it usable.
1505 * @param moodle_url $callerurl the base URL of a script that will handle the class'es form data
1506 * @param moodle_url $returnurl the final URL to return to when the deployment is finished
1508 public function initialize(moodle_url $callerurl, moodle_url $returnurl) {
1510 if (!$this->enabled()) {
1511 throw new coding_exception('Unable to initialize the deployer, the feature is not enabled.');
1514 $this->callerurl = $callerurl;
1515 $this->returnurl = $returnurl;
1519 * Has the deployer been initialized?
1521 * Initialized deployer means that the following properties were set:
1522 * callerurl, returnurl
1524 * @return bool
1526 public function initialized() {
1528 if (!$this->enabled()) {
1529 return false;
1532 if (empty($this->callerurl)) {
1533 return false;
1536 if (empty($this->returnurl)) {
1537 return false;
1540 return true;
1544 * Returns a list of reasons why the deployment can not happen
1546 * If the returned array is empty, the deployment seems to be possible. The returned
1547 * structure is an associative array with keys representing individual impediments.
1548 * Possible keys are: missingdownloadurl, missingdownloadmd5, notwritable.
1550 * @param available_update_info $info
1551 * @return array
1553 public function deployment_impediments(available_update_info $info) {
1555 $impediments = array();
1557 if (empty($info->download)) {
1558 $impediments['missingdownloadurl'] = true;
1561 if (empty($info->downloadmd5)) {
1562 $impediments['missingdownloadmd5'] = true;
1565 if (!$this->component_writable($info->component)) {
1566 $impediments['notwritable'] = true;
1569 return $impediments;
1573 * Check to see if the current version of the plugin seems to be a checkout of an external repository.
1575 * @param available_update_info $info
1576 * @return false|string
1578 public function plugin_external_source(available_update_info $info) {
1580 $paths = get_plugin_types(true);
1581 list($plugintype, $pluginname) = normalize_component($info->component);
1582 $pluginroot = $paths[$plugintype].'/'.$pluginname;
1584 if (is_dir($pluginroot.'/.git')) {
1585 return 'git';
1588 if (is_dir($pluginroot.'/CVS')) {
1589 return 'cvs';
1592 if (is_dir($pluginroot.'/.svn')) {
1593 return 'svn';
1596 return false;
1600 * Prepares a renderable widget to confirm installation of an available update.
1602 * @param available_update_info $info component version to deploy
1603 * @return renderable
1605 public function make_confirm_widget(available_update_info $info) {
1607 if (!$this->initialized()) {
1608 throw new coding_exception('Illegal method call - deployer not initialized.');
1611 $params = $this->data_to_params(array(
1612 'updateinfo' => (array)$info, // see http://www.php.net/manual/en/language.types.array.php#language.types.array.casting
1615 $widget = new single_button(
1616 new moodle_url($this->callerurl, $params),
1617 get_string('updateavailableinstall', 'core_admin'),
1618 'post'
1621 return $widget;
1625 * Prepares a renderable widget to execute installation of an available update.
1627 * @param available_update_info $info component version to deploy
1628 * @return renderable
1630 public function make_execution_widget(available_update_info $info) {
1631 global $CFG;
1633 if (!$this->initialized()) {
1634 throw new coding_exception('Illegal method call - deployer not initialized.');
1637 $pluginrootpaths = get_plugin_types(true);
1639 list($plugintype, $pluginname) = normalize_component($info->component);
1641 if (empty($pluginrootpaths[$plugintype])) {
1642 throw new coding_exception('Unknown plugin type root location', $plugintype);
1645 list($passfile, $password) = $this->prepare_authorization();
1647 $upgradeurl = new moodle_url('/admin');
1649 $params = array(
1650 'upgrade' => true,
1651 'type' => $plugintype,
1652 'name' => $pluginname,
1653 'typeroot' => $pluginrootpaths[$plugintype],
1654 'package' => $info->download,
1655 'md5' => $info->downloadmd5,
1656 'dataroot' => $CFG->dataroot,
1657 'dirroot' => $CFG->dirroot,
1658 'passfile' => $passfile,
1659 'password' => $password,
1660 'returnurl' => $upgradeurl->out(true),
1663 $widget = new single_button(
1664 new moodle_url('/mdeploy.php', $params),
1665 get_string('updateavailableinstall', 'core_admin'),
1666 'post'
1669 return $widget;
1673 * Returns array of data objects passed to this tool.
1675 * @return array
1677 public function submitted_data() {
1679 $data = $this->params_to_data($_POST);
1681 if (empty($data) or empty($data[self::HTTP_PARAM_CHECKER])) {
1682 return false;
1685 if (!empty($data['updateinfo']) and is_object($data['updateinfo'])) {
1686 $updateinfo = $data['updateinfo'];
1687 if (!empty($updateinfo->component) and !empty($updateinfo->version)) {
1688 $data['updateinfo'] = new available_update_info($updateinfo->component, (array)$updateinfo);
1692 if (!empty($data['callerurl'])) {
1693 $data['callerurl'] = new moodle_url($data['callerurl']);
1696 if (!empty($data['returnurl'])) {
1697 $data['returnurl'] = new moodle_url($data['returnurl']);
1700 return $data;
1704 * Handles magic getters and setters for protected properties.
1706 * @param string $name method name, e.g. set_returnurl()
1707 * @param array $arguments arguments to be passed to the array
1709 public function __call($name, array $arguments = array()) {
1711 if (substr($name, 0, 4) === 'set_') {
1712 $property = substr($name, 4);
1713 if (empty($property)) {
1714 throw new coding_exception('Invalid property name (empty)');
1716 if (empty($arguments)) {
1717 $arguments = array(true); // Default value for flag-like properties.
1719 // Make sure it is a protected property.
1720 $isprotected = false;
1721 $reflection = new ReflectionObject($this);
1722 foreach ($reflection->getProperties(ReflectionProperty::IS_PROTECTED) as $reflectionproperty) {
1723 if ($reflectionproperty->getName() === $property) {
1724 $isprotected = true;
1725 break;
1728 if (!$isprotected) {
1729 throw new coding_exception('Unable to set property - it does not exist or it is not protected');
1731 $value = reset($arguments);
1732 $this->$property = $value;
1733 return;
1736 if (substr($name, 0, 4) === 'get_') {
1737 $property = substr($name, 4);
1738 if (empty($property)) {
1739 throw new coding_exception('Invalid property name (empty)');
1741 if (!empty($arguments)) {
1742 throw new coding_exception('No parameter expected');
1744 // Make sure it is a protected property.
1745 $isprotected = false;
1746 $reflection = new ReflectionObject($this);
1747 foreach ($reflection->getProperties(ReflectionProperty::IS_PROTECTED) as $reflectionproperty) {
1748 if ($reflectionproperty->getName() === $property) {
1749 $isprotected = true;
1750 break;
1753 if (!$isprotected) {
1754 throw new coding_exception('Unable to get property - it does not exist or it is not protected');
1756 return $this->$property;
1761 * Generates a random token and stores it in a file in moodledata directory.
1763 * @return array of the (string)filename and (string)password in this order
1765 public function prepare_authorization() {
1766 global $CFG;
1768 make_upload_directory('mdeploy/auth/');
1770 $attempts = 0;
1771 $success = false;
1773 while (!$success and $attempts < 5) {
1774 $attempts++;
1776 $passfile = $this->generate_passfile();
1777 $password = $this->generate_password();
1778 $now = time();
1780 $filepath = $CFG->dataroot.'/mdeploy/auth/'.$passfile;
1782 if (!file_exists($filepath)) {
1783 $success = file_put_contents($filepath, $password . PHP_EOL . $now . PHP_EOL, LOCK_EX);
1787 if ($success) {
1788 return array($passfile, $password);
1790 } else {
1791 throw new moodle_exception('unable_prepare_authorization', 'core_plugin');
1795 // End of external API
1798 * Prepares an array of HTTP parameters that can be passed to another page.
1800 * @param array|object $data associative array or an object holding the data, data JSON-able
1801 * @return array suitable as a param for moodle_url
1803 protected function data_to_params($data) {
1805 // Append some our own data
1806 if (!empty($this->callerurl)) {
1807 $data['callerurl'] = $this->callerurl->out(false);
1809 if (!empty($this->callerurl)) {
1810 $data['returnurl'] = $this->returnurl->out(false);
1813 // Finally append the count of items in the package.
1814 $data[self::HTTP_PARAM_CHECKER] = count($data);
1816 // Generate params
1817 $params = array();
1818 foreach ($data as $name => $value) {
1819 $transname = self::HTTP_PARAM_PREFIX.$name;
1820 $transvalue = json_encode($value);
1821 $params[$transname] = $transvalue;
1824 return $params;
1828 * Converts HTTP parameters passed to the script into native PHP data
1830 * @param array $params such as $_REQUEST or $_POST
1831 * @return array data passed for this class
1833 protected function params_to_data(array $params) {
1835 if (empty($params)) {
1836 return array();
1839 $data = array();
1840 foreach ($params as $name => $value) {
1841 if (strpos($name, self::HTTP_PARAM_PREFIX) === 0) {
1842 $realname = substr($name, strlen(self::HTTP_PARAM_PREFIX));
1843 $realvalue = json_decode($value);
1844 $data[$realname] = $realvalue;
1848 return $data;
1852 * Returns a random string to be used as a filename of the password storage.
1854 * @return string
1856 protected function generate_passfile() {
1857 return clean_param(uniqid('mdeploy_', true), PARAM_FILE);
1861 * Returns a random string to be used as the authorization token
1863 * @return string
1865 protected function generate_password() {
1866 return complex_random_string();
1870 * Checks if the given component's directory is writable
1872 * For the purpose of the deployment, the web server process has to have
1873 * write access to all files in the component's directory (recursively) and for the
1874 * directory itself.
1876 * @see worker::move_directory_source_precheck()
1877 * @param string $component normalized component name
1878 * @return boolean
1880 protected function component_writable($component) {
1882 list($plugintype, $pluginname) = normalize_component($component);
1884 $directory = get_plugin_directory($plugintype, $pluginname);
1886 if (is_null($directory)) {
1887 throw new coding_exception('Unknown component location', $component);
1890 return $this->directory_writable($directory);
1894 * Checks if the directory and all its contents (recursively) is writable
1896 * @param string $path full path to a directory
1897 * @return boolean
1899 private function directory_writable($path) {
1901 if (!is_writable($path)) {
1902 return false;
1905 if (is_dir($path)) {
1906 $handle = opendir($path);
1907 } else {
1908 return false;
1911 $result = true;
1913 while ($filename = readdir($handle)) {
1914 $filepath = $path.'/'.$filename;
1916 if ($filename === '.' or $filename === '..') {
1917 continue;
1920 if (is_dir($filepath)) {
1921 $result = $result && $this->directory_writable($filepath);
1923 } else {
1924 $result = $result && is_writable($filepath);
1928 closedir($handle);
1930 return $result;
1936 * Factory class producing required subclasses of {@link plugininfo_base}
1938 class plugininfo_default_factory {
1941 * Makes a new instance of the plugininfo class
1943 * @param string $type the plugin type, eg. 'mod'
1944 * @param string $typerootdir full path to the location of all the plugins of this type
1945 * @param string $name the plugin name, eg. 'workshop'
1946 * @param string $namerootdir full path to the location of the plugin
1947 * @param string $typeclass the name of class that holds the info about the plugin
1948 * @return plugininfo_base the instance of $typeclass
1950 public static function make($type, $typerootdir, $name, $namerootdir, $typeclass) {
1951 $plugin = new $typeclass();
1952 $plugin->type = $type;
1953 $plugin->typerootdir = $typerootdir;
1954 $plugin->name = $name;
1955 $plugin->rootdir = $namerootdir;
1957 $plugin->init_display_name();
1958 $plugin->load_disk_version();
1959 $plugin->load_db_version();
1960 $plugin->load_required_main_version();
1961 $plugin->init_is_standard();
1963 return $plugin;
1969 * Base class providing access to the information about a plugin
1971 * @property-read string component the component name, type_name
1973 abstract class plugininfo_base {
1975 /** @var string the plugintype name, eg. mod, auth or workshopform */
1976 public $type;
1977 /** @var string full path to the location of all the plugins of this type */
1978 public $typerootdir;
1979 /** @var string the plugin name, eg. assignment, ldap */
1980 public $name;
1981 /** @var string the localized plugin name */
1982 public $displayname;
1983 /** @var string the plugin source, one of plugin_manager::PLUGIN_SOURCE_xxx constants */
1984 public $source;
1985 /** @var fullpath to the location of this plugin */
1986 public $rootdir;
1987 /** @var int|string the version of the plugin's source code */
1988 public $versiondisk;
1989 /** @var int|string the version of the installed plugin */
1990 public $versiondb;
1991 /** @var int|float|string required version of Moodle core */
1992 public $versionrequires;
1993 /** @var array other plugins that this one depends on, lazy-loaded by {@link get_other_required_plugins()} */
1994 public $dependencies;
1995 /** @var int number of instances of the plugin - not supported yet */
1996 public $instances;
1997 /** @var int order of the plugin among other plugins of the same type - not supported yet */
1998 public $sortorder;
1999 /** @var array|null array of {@link available_update_info} for this plugin */
2000 public $availableupdates;
2003 * Gathers and returns the information about all plugins of the given type
2005 * @param string $type the name of the plugintype, eg. mod, auth or workshopform
2006 * @param string $typerootdir full path to the location of the plugin dir
2007 * @param string $typeclass the name of the actually called class
2008 * @return array of plugintype classes, indexed by the plugin name
2010 public static function get_plugins($type, $typerootdir, $typeclass) {
2012 // get the information about plugins at the disk
2013 $plugins = get_plugin_list($type);
2014 $ondisk = array();
2015 foreach ($plugins as $pluginname => $pluginrootdir) {
2016 $ondisk[$pluginname] = plugininfo_default_factory::make($type, $typerootdir,
2017 $pluginname, $pluginrootdir, $typeclass);
2019 return $ondisk;
2023 * Sets {@link $displayname} property to a localized name of the plugin
2025 public function init_display_name() {
2026 if (!get_string_manager()->string_exists('pluginname', $this->component)) {
2027 $this->displayname = '[pluginname,' . $this->component . ']';
2028 } else {
2029 $this->displayname = get_string('pluginname', $this->component);
2034 * Magic method getter, redirects to read only values.
2036 * @param string $name
2037 * @return mixed
2039 public function __get($name) {
2040 switch ($name) {
2041 case 'component': return $this->type . '_' . $this->name;
2043 default:
2044 debugging('Invalid plugin property accessed! '.$name);
2045 return null;
2050 * Return the full path name of a file within the plugin.
2052 * No check is made to see if the file exists.
2054 * @param string $relativepath e.g. 'version.php'.
2055 * @return string e.g. $CFG->dirroot . '/mod/quiz/version.php'.
2057 public function full_path($relativepath) {
2058 if (empty($this->rootdir)) {
2059 return '';
2061 return $this->rootdir . '/' . $relativepath;
2065 * Load the data from version.php.
2067 * @return stdClass the object called $plugin defined in version.php
2069 protected function load_version_php() {
2070 $versionfile = $this->full_path('version.php');
2072 $plugin = new stdClass();
2073 if (is_readable($versionfile)) {
2074 include($versionfile);
2076 return $plugin;
2080 * Sets {@link $versiondisk} property to a numerical value representing the
2081 * version of the plugin's source code.
2083 * If the value is null after calling this method, either the plugin
2084 * does not use versioning (typically does not have any database
2085 * data) or is missing from disk.
2087 public function load_disk_version() {
2088 $plugin = $this->load_version_php();
2089 if (isset($plugin->version)) {
2090 $this->versiondisk = $plugin->version;
2095 * Sets {@link $versionrequires} property to a numerical value representing
2096 * the version of Moodle core that this plugin requires.
2098 public function load_required_main_version() {
2099 $plugin = $this->load_version_php();
2100 if (isset($plugin->requires)) {
2101 $this->versionrequires = $plugin->requires;
2106 * Initialise {@link $dependencies} to the list of other plugins (in any)
2107 * that this one requires to be installed.
2109 protected function load_other_required_plugins() {
2110 $plugin = $this->load_version_php();
2111 if (!empty($plugin->dependencies)) {
2112 $this->dependencies = $plugin->dependencies;
2113 } else {
2114 $this->dependencies = array(); // By default, no dependencies.
2119 * Get the list of other plugins that this plugin requires to be installed.
2121 * @return array with keys the frankenstyle plugin name, and values either
2122 * a version string (like '2011101700') or the constant ANY_VERSION.
2124 public function get_other_required_plugins() {
2125 if (is_null($this->dependencies)) {
2126 $this->load_other_required_plugins();
2128 return $this->dependencies;
2132 * Sets {@link $versiondb} property to a numerical value representing the
2133 * currently installed version of the plugin.
2135 * If the value is null after calling this method, either the plugin
2136 * does not use versioning (typically does not have any database
2137 * data) or has not been installed yet.
2139 public function load_db_version() {
2140 if ($ver = self::get_version_from_config_plugins($this->component)) {
2141 $this->versiondb = $ver;
2146 * Sets {@link $source} property to one of plugin_manager::PLUGIN_SOURCE_xxx
2147 * constants.
2149 * If the property's value is null after calling this method, then
2150 * the type of the plugin has not been recognized and you should throw
2151 * an exception.
2153 public function init_is_standard() {
2155 $standard = plugin_manager::standard_plugins_list($this->type);
2157 if ($standard !== false) {
2158 $standard = array_flip($standard);
2159 if (isset($standard[$this->name])) {
2160 $this->source = plugin_manager::PLUGIN_SOURCE_STANDARD;
2161 } else if (!is_null($this->versiondb) and is_null($this->versiondisk)
2162 and plugin_manager::is_deleted_standard_plugin($this->type, $this->name)) {
2163 $this->source = plugin_manager::PLUGIN_SOURCE_STANDARD; // to be deleted
2164 } else {
2165 $this->source = plugin_manager::PLUGIN_SOURCE_EXTENSION;
2171 * Returns true if the plugin is shipped with the official distribution
2172 * of the current Moodle version, false otherwise.
2174 * @return bool
2176 public function is_standard() {
2177 return $this->source === plugin_manager::PLUGIN_SOURCE_STANDARD;
2181 * Returns true if the the given Moodle version is enough to run this plugin
2183 * @param string|int|double $moodleversion
2184 * @return bool
2186 public function is_core_dependency_satisfied($moodleversion) {
2188 if (empty($this->versionrequires)) {
2189 return true;
2191 } else {
2192 return (double)$this->versionrequires <= (double)$moodleversion;
2197 * Returns the status of the plugin
2199 * @return string one of plugin_manager::PLUGIN_STATUS_xxx constants
2201 public function get_status() {
2203 if (is_null($this->versiondb) and is_null($this->versiondisk)) {
2204 return plugin_manager::PLUGIN_STATUS_NODB;
2206 } else if (is_null($this->versiondb) and !is_null($this->versiondisk)) {
2207 return plugin_manager::PLUGIN_STATUS_NEW;
2209 } else if (!is_null($this->versiondb) and is_null($this->versiondisk)) {
2210 if (plugin_manager::is_deleted_standard_plugin($this->type, $this->name)) {
2211 return plugin_manager::PLUGIN_STATUS_DELETE;
2212 } else {
2213 return plugin_manager::PLUGIN_STATUS_MISSING;
2216 } else if ((string)$this->versiondb === (string)$this->versiondisk) {
2217 return plugin_manager::PLUGIN_STATUS_UPTODATE;
2219 } else if ($this->versiondb < $this->versiondisk) {
2220 return plugin_manager::PLUGIN_STATUS_UPGRADE;
2222 } else if ($this->versiondb > $this->versiondisk) {
2223 return plugin_manager::PLUGIN_STATUS_DOWNGRADE;
2225 } else {
2226 // $version = pi(); and similar funny jokes - hopefully Donald E. Knuth will never contribute to Moodle ;-)
2227 throw new coding_exception('Unable to determine plugin state, check the plugin versions');
2232 * Returns the information about plugin availability
2234 * True means that the plugin is enabled. False means that the plugin is
2235 * disabled. Null means that the information is not available, or the
2236 * plugin does not support configurable availability or the availability
2237 * can not be changed.
2239 * @return null|bool
2241 public function is_enabled() {
2242 return null;
2246 * Populates the property {@link $availableupdates} with the information provided by
2247 * available update checker
2249 * @param available_update_checker $provider the class providing the available update info
2251 public function check_available_updates(available_update_checker $provider) {
2252 global $CFG;
2254 if (isset($CFG->updateminmaturity)) {
2255 $minmaturity = $CFG->updateminmaturity;
2256 } else {
2257 // this can happen during the very first upgrade to 2.3
2258 $minmaturity = MATURITY_STABLE;
2261 $this->availableupdates = $provider->get_update_info($this->component,
2262 array('minmaturity' => $minmaturity));
2266 * If there are updates for this plugin available, returns them.
2268 * Returns array of {@link available_update_info} objects, if some update
2269 * is available. Returns null if there is no update available or if the update
2270 * availability is unknown.
2272 * @return array|null
2274 public function available_updates() {
2276 if (empty($this->availableupdates) or !is_array($this->availableupdates)) {
2277 return null;
2280 $updates = array();
2282 foreach ($this->availableupdates as $availableupdate) {
2283 if ($availableupdate->version > $this->versiondisk) {
2284 $updates[] = $availableupdate;
2288 if (empty($updates)) {
2289 return null;
2292 return $updates;
2296 * Returns the node name used in admin settings menu for this plugin settings (if applicable)
2298 * @return null|string node name or null if plugin does not create settings node (default)
2300 public function get_settings_section_name() {
2301 return null;
2305 * Returns the URL of the plugin settings screen
2307 * Null value means that the plugin either does not have the settings screen
2308 * or its location is not available via this library.
2310 * @return null|moodle_url
2312 public function get_settings_url() {
2313 $section = $this->get_settings_section_name();
2314 if ($section === null) {
2315 return null;
2317 $settings = admin_get_root()->locate($section);
2318 if ($settings && $settings instanceof admin_settingpage) {
2319 return new moodle_url('/admin/settings.php', array('section' => $section));
2320 } else if ($settings && $settings instanceof admin_externalpage) {
2321 return new moodle_url($settings->url);
2322 } else {
2323 return null;
2328 * Loads plugin settings to the settings tree
2330 * This function usually includes settings.php file in plugins folder.
2331 * Alternatively it can create a link to some settings page (instance of admin_externalpage)
2333 * @param part_of_admin_tree $adminroot
2334 * @param string $parentnodename
2335 * @param bool $hassiteconfig whether the current user has moodle/site:config capability
2337 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
2341 * Returns the URL of the screen where this plugin can be uninstalled
2343 * Visiting that URL must be safe, that is a manual confirmation is needed
2344 * for actual uninstallation of the plugin. Null value means that the
2345 * plugin either does not support uninstallation, or does not require any
2346 * database cleanup or the location of the screen is not available via this
2347 * library.
2349 * @return null|moodle_url
2351 public function get_uninstall_url() {
2352 return null;
2356 * Returns relative directory of the plugin with heading '/'
2358 * @return string
2360 public function get_dir() {
2361 global $CFG;
2363 return substr($this->rootdir, strlen($CFG->dirroot));
2367 * Provides access to plugin versions from {config_plugins}
2369 * @param string $plugin plugin name
2370 * @param double $disablecache optional, defaults to false
2371 * @return int|false the stored value or false if not found
2373 protected function get_version_from_config_plugins($plugin, $disablecache=false) {
2374 global $DB;
2375 static $pluginversions = null;
2377 if (is_null($pluginversions) or $disablecache) {
2378 try {
2379 $pluginversions = $DB->get_records_menu('config_plugins', array('name' => 'version'), 'plugin', 'plugin,value');
2380 } catch (dml_exception $e) {
2381 // before install
2382 $pluginversions = array();
2386 if (!array_key_exists($plugin, $pluginversions)) {
2387 return false;
2390 return $pluginversions[$plugin];
2396 * General class for all plugin types that do not have their own class
2398 class plugininfo_general extends plugininfo_base {
2403 * Class for page side blocks
2405 class plugininfo_block extends plugininfo_base {
2407 public static function get_plugins($type, $typerootdir, $typeclass) {
2409 // get the information about blocks at the disk
2410 $blocks = parent::get_plugins($type, $typerootdir, $typeclass);
2412 // add blocks missing from disk
2413 $blocksinfo = self::get_blocks_info();
2414 foreach ($blocksinfo as $blockname => $blockinfo) {
2415 if (isset($blocks[$blockname])) {
2416 continue;
2418 $plugin = new $typeclass();
2419 $plugin->type = $type;
2420 $plugin->typerootdir = $typerootdir;
2421 $plugin->name = $blockname;
2422 $plugin->rootdir = null;
2423 $plugin->displayname = $blockname;
2424 $plugin->versiondb = $blockinfo->version;
2425 $plugin->init_is_standard();
2427 $blocks[$blockname] = $plugin;
2430 return $blocks;
2434 * Magic method getter, redirects to read only values.
2436 * For block plugins pretends the object has 'visible' property for compatibility
2437 * with plugins developed for Moodle version below 2.4
2439 * @param string $name
2440 * @return mixed
2442 public function __get($name) {
2443 if ($name === 'visible') {
2444 debugging('This is now an instance of plugininfo_block, please use $block->is_enabled() instead of $block->visible', DEBUG_DEVELOPER);
2445 return ($this->is_enabled() !== false);
2447 return parent::__get($name);
2450 public function init_display_name() {
2452 if (get_string_manager()->string_exists('pluginname', 'block_' . $this->name)) {
2453 $this->displayname = get_string('pluginname', 'block_' . $this->name);
2455 } else if (($block = block_instance($this->name)) !== false) {
2456 $this->displayname = $block->get_title();
2458 } else {
2459 parent::init_display_name();
2463 public function load_db_version() {
2464 global $DB;
2466 $blocksinfo = self::get_blocks_info();
2467 if (isset($blocksinfo[$this->name]->version)) {
2468 $this->versiondb = $blocksinfo[$this->name]->version;
2472 public function is_enabled() {
2474 $blocksinfo = self::get_blocks_info();
2475 if (isset($blocksinfo[$this->name]->visible)) {
2476 if ($blocksinfo[$this->name]->visible) {
2477 return true;
2478 } else {
2479 return false;
2481 } else {
2482 return parent::is_enabled();
2486 public function get_settings_section_name() {
2487 return 'blocksetting' . $this->name;
2490 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
2491 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
2492 $ADMIN = $adminroot; // may be used in settings.php
2493 $block = $this; // also can be used inside settings.php
2494 $section = $this->get_settings_section_name();
2496 if (!$hassiteconfig || (($blockinstance = block_instance($this->name)) === false)) {
2497 return;
2500 $settings = null;
2501 if ($blockinstance->has_config()) {
2502 if (file_exists($this->full_path('settings.php'))) {
2503 $settings = new admin_settingpage($section, $this->displayname,
2504 'moodle/site:config', $this->is_enabled() === false);
2505 include($this->full_path('settings.php')); // this may also set $settings to null
2506 } else {
2507 $blocksinfo = self::get_blocks_info();
2508 $settingsurl = new moodle_url('/admin/block.php', array('block' => $blocksinfo[$this->name]->id));
2509 $settings = new admin_externalpage($section, $this->displayname,
2510 $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
2513 if ($settings) {
2514 $ADMIN->add($parentnodename, $settings);
2518 public function get_uninstall_url() {
2520 $blocksinfo = self::get_blocks_info();
2521 return new moodle_url('/admin/blocks.php', array('delete' => $blocksinfo[$this->name]->id, 'sesskey' => sesskey()));
2525 * Provides access to the records in {block} table
2527 * @param bool $disablecache do not use internal static cache
2528 * @return array array of stdClasses
2530 protected static function get_blocks_info($disablecache=false) {
2531 global $DB;
2532 static $blocksinfocache = null;
2534 if (is_null($blocksinfocache) or $disablecache) {
2535 try {
2536 $blocksinfocache = $DB->get_records('block', null, 'name', 'name,id,version,visible');
2537 } catch (dml_exception $e) {
2538 // before install
2539 $blocksinfocache = array();
2543 return $blocksinfocache;
2549 * Class for text filters
2551 class plugininfo_filter extends plugininfo_base {
2553 public static function get_plugins($type, $typerootdir, $typeclass) {
2554 global $CFG, $DB;
2556 $filters = array();
2558 // get the list of filters from both /filter and /mod location
2559 $installed = filter_get_all_installed();
2561 foreach ($installed as $filterlegacyname => $displayname) {
2562 $plugin = new $typeclass();
2563 $plugin->type = $type;
2564 $plugin->typerootdir = $typerootdir;
2565 $plugin->name = self::normalize_legacy_name($filterlegacyname);
2566 $plugin->rootdir = $CFG->dirroot . '/' . $filterlegacyname;
2567 $plugin->displayname = $displayname;
2569 $plugin->load_disk_version();
2570 $plugin->load_db_version();
2571 $plugin->load_required_main_version();
2572 $plugin->init_is_standard();
2574 $filters[$plugin->name] = $plugin;
2577 $globalstates = self::get_global_states();
2579 if ($DB->get_manager()->table_exists('filter_active')) {
2580 // if we're upgrading from 1.9, the table does not exist yet
2581 // if it does, make sure that all installed filters are registered
2582 $needsreload = false;
2583 foreach (array_keys($installed) as $filterlegacyname) {
2584 if (!isset($globalstates[self::normalize_legacy_name($filterlegacyname)])) {
2585 filter_set_global_state($filterlegacyname, TEXTFILTER_DISABLED);
2586 $needsreload = true;
2589 if ($needsreload) {
2590 $globalstates = self::get_global_states(true);
2594 // make sure that all registered filters are installed, just in case
2595 foreach ($globalstates as $name => $info) {
2596 if (!isset($filters[$name])) {
2597 // oops, there is a record in filter_active but the filter is not installed
2598 $plugin = new $typeclass();
2599 $plugin->type = $type;
2600 $plugin->typerootdir = $typerootdir;
2601 $plugin->name = $name;
2602 $plugin->rootdir = $CFG->dirroot . '/' . $info->legacyname;
2603 $plugin->displayname = $info->legacyname;
2605 $plugin->load_db_version();
2607 if (is_null($plugin->versiondb)) {
2608 // this is a hack to stimulate 'Missing from disk' error
2609 // because $plugin->versiondisk will be null !== false
2610 $plugin->versiondb = false;
2613 $filters[$plugin->name] = $plugin;
2617 return $filters;
2620 public function init_display_name() {
2621 // do nothing, the name is set in self::get_plugins()
2625 * @see load_version_php()
2627 protected function load_version_php() {
2628 if (strpos($this->name, 'mod_') === 0) {
2629 // filters bundled with modules do not have a version.php and so
2630 // do not provide their own versioning information.
2631 return new stdClass();
2633 return parent::load_version_php();
2636 public function is_enabled() {
2638 $globalstates = self::get_global_states();
2640 foreach ($globalstates as $filterlegacyname => $info) {
2641 $name = self::normalize_legacy_name($filterlegacyname);
2642 if ($name === $this->name) {
2643 if ($info->active == TEXTFILTER_DISABLED) {
2644 return false;
2645 } else {
2646 // it may be 'On' or 'Off, but available'
2647 return null;
2652 return null;
2655 public function get_settings_section_name() {
2656 $globalstates = self::get_global_states();
2657 if (!isset($globalstates[$this->name])) {
2658 return parent::get_settings_section_name();
2660 $legacyname = $globalstates[$this->name]->legacyname;
2661 return 'filtersetting' . str_replace('/', '', $legacyname);
2664 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
2665 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
2666 $ADMIN = $adminroot; // may be used in settings.php
2667 $filter = $this; // also can be used inside settings.php
2669 $globalstates = self::get_global_states();
2670 $settings = null;
2671 if ($hassiteconfig && isset($globalstates[$this->name]) && file_exists($this->full_path('filtersettings.php'))) {
2672 $section = $this->get_settings_section_name();
2673 $settings = new admin_settingpage($section, $this->displayname,
2674 'moodle/site:config', $this->is_enabled() === false);
2675 include($this->full_path('filtersettings.php')); // this may also set $settings to null
2677 if ($settings) {
2678 $ADMIN->add($parentnodename, $settings);
2682 public function get_uninstall_url() {
2684 if (strpos($this->name, 'mod_') === 0) {
2685 return null;
2686 } else {
2687 $globalstates = self::get_global_states();
2688 $legacyname = $globalstates[$this->name]->legacyname;
2689 return new moodle_url('/admin/filters.php', array('sesskey' => sesskey(), 'filterpath' => $legacyname, 'action' => 'delete'));
2694 * Convert legacy filter names like 'filter/foo' or 'mod/bar' into frankenstyle
2696 * @param string $legacyfiltername legacy filter name
2697 * @return string frankenstyle-like name
2699 protected static function normalize_legacy_name($legacyfiltername) {
2701 $name = str_replace('/', '_', $legacyfiltername);
2702 if (strpos($name, 'filter_') === 0) {
2703 $name = substr($name, 7);
2704 if (empty($name)) {
2705 throw new coding_exception('Unable to determine filter name: ' . $legacyfiltername);
2709 return $name;
2713 * Provides access to the results of {@link filter_get_global_states()}
2714 * but indexed by the normalized filter name
2716 * The legacy filter name is available as ->legacyname property.
2718 * @param bool $disablecache
2719 * @return array
2721 protected static function get_global_states($disablecache=false) {
2722 global $DB;
2723 static $globalstatescache = null;
2725 if ($disablecache or is_null($globalstatescache)) {
2727 if (!$DB->get_manager()->table_exists('filter_active')) {
2728 // we're upgrading from 1.9 and the table used by {@link filter_get_global_states()}
2729 // does not exist yet
2730 $globalstatescache = array();
2732 } else {
2733 foreach (filter_get_global_states() as $legacyname => $info) {
2734 $name = self::normalize_legacy_name($legacyname);
2735 $filterinfo = new stdClass();
2736 $filterinfo->legacyname = $legacyname;
2737 $filterinfo->active = $info->active;
2738 $filterinfo->sortorder = $info->sortorder;
2739 $globalstatescache[$name] = $filterinfo;
2744 return $globalstatescache;
2750 * Class for activity modules
2752 class plugininfo_mod extends plugininfo_base {
2754 public static function get_plugins($type, $typerootdir, $typeclass) {
2756 // get the information about plugins at the disk
2757 $modules = parent::get_plugins($type, $typerootdir, $typeclass);
2759 // add modules missing from disk
2760 $modulesinfo = self::get_modules_info();
2761 foreach ($modulesinfo as $modulename => $moduleinfo) {
2762 if (isset($modules[$modulename])) {
2763 continue;
2765 $plugin = new $typeclass();
2766 $plugin->type = $type;
2767 $plugin->typerootdir = $typerootdir;
2768 $plugin->name = $modulename;
2769 $plugin->rootdir = null;
2770 $plugin->displayname = $modulename;
2771 $plugin->versiondb = $moduleinfo->version;
2772 $plugin->init_is_standard();
2774 $modules[$modulename] = $plugin;
2777 return $modules;
2781 * Magic method getter, redirects to read only values.
2783 * For module plugins we pretend the object has 'visible' property for compatibility
2784 * with plugins developed for Moodle version below 2.4
2786 * @param string $name
2787 * @return mixed
2789 public function __get($name) {
2790 if ($name === 'visible') {
2791 debugging('This is now an instance of plugininfo_mod, please use $module->is_enabled() instead of $module->visible', DEBUG_DEVELOPER);
2792 return ($this->is_enabled() !== false);
2794 return parent::__get($name);
2797 public function init_display_name() {
2798 if (get_string_manager()->string_exists('pluginname', $this->component)) {
2799 $this->displayname = get_string('pluginname', $this->component);
2800 } else {
2801 $this->displayname = get_string('modulename', $this->component);
2806 * Load the data from version.php.
2807 * @return object the data object defined in version.php.
2809 protected function load_version_php() {
2810 $versionfile = $this->full_path('version.php');
2812 $module = new stdClass();
2813 if (is_readable($versionfile)) {
2814 include($versionfile);
2816 return $module;
2819 public function load_db_version() {
2820 global $DB;
2822 $modulesinfo = self::get_modules_info();
2823 if (isset($modulesinfo[$this->name]->version)) {
2824 $this->versiondb = $modulesinfo[$this->name]->version;
2828 public function is_enabled() {
2830 $modulesinfo = self::get_modules_info();
2831 if (isset($modulesinfo[$this->name]->visible)) {
2832 if ($modulesinfo[$this->name]->visible) {
2833 return true;
2834 } else {
2835 return false;
2837 } else {
2838 return parent::is_enabled();
2842 public function get_settings_section_name() {
2843 return 'modsetting' . $this->name;
2846 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
2847 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
2848 $ADMIN = $adminroot; // may be used in settings.php
2849 $module = $this; // also can be used inside settings.php
2850 $section = $this->get_settings_section_name();
2852 $modulesinfo = self::get_modules_info();
2853 $settings = null;
2854 if ($hassiteconfig && isset($modulesinfo[$this->name]) && file_exists($this->full_path('settings.php'))) {
2855 $settings = new admin_settingpage($section, $this->displayname,
2856 'moodle/site:config', $this->is_enabled() === false);
2857 include($this->full_path('settings.php')); // this may also set $settings to null
2859 if ($settings) {
2860 $ADMIN->add($parentnodename, $settings);
2864 public function get_uninstall_url() {
2866 if ($this->name !== 'forum') {
2867 return new moodle_url('/admin/modules.php', array('delete' => $this->name, 'sesskey' => sesskey()));
2868 } else {
2869 return null;
2874 * Provides access to the records in {modules} table
2876 * @param bool $disablecache do not use internal static cache
2877 * @return array array of stdClasses
2879 protected static function get_modules_info($disablecache=false) {
2880 global $DB;
2881 static $modulesinfocache = null;
2883 if (is_null($modulesinfocache) or $disablecache) {
2884 try {
2885 $modulesinfocache = $DB->get_records('modules', null, 'name', 'name,id,version,visible');
2886 } catch (dml_exception $e) {
2887 // before install
2888 $modulesinfocache = array();
2892 return $modulesinfocache;
2898 * Class for question behaviours.
2900 class plugininfo_qbehaviour extends plugininfo_base {
2902 public function get_uninstall_url() {
2903 return new moodle_url('/admin/qbehaviours.php',
2904 array('delete' => $this->name, 'sesskey' => sesskey()));
2910 * Class for question types
2912 class plugininfo_qtype extends plugininfo_base {
2914 public function get_uninstall_url() {
2915 return new moodle_url('/admin/qtypes.php',
2916 array('delete' => $this->name, 'sesskey' => sesskey()));
2919 public function get_settings_section_name() {
2920 return 'qtypesetting' . $this->name;
2923 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
2924 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
2925 $ADMIN = $adminroot; // may be used in settings.php
2926 $qtype = $this; // also can be used inside settings.php
2927 $section = $this->get_settings_section_name();
2929 $settings = null;
2930 $systemcontext = context_system::instance();
2931 if (($hassiteconfig || has_capability('moodle/question:config', $systemcontext)) &&
2932 file_exists($this->full_path('settings.php'))) {
2933 $settings = new admin_settingpage($section, $this->displayname,
2934 'moodle/question:config', $this->is_enabled() === false);
2935 include($this->full_path('settings.php')); // this may also set $settings to null
2937 if ($settings) {
2938 $ADMIN->add($parentnodename, $settings);
2945 * Class for authentication plugins
2947 class plugininfo_auth extends plugininfo_base {
2949 public function is_enabled() {
2950 global $CFG;
2951 /** @var null|array list of enabled authentication plugins */
2952 static $enabled = null;
2954 if (in_array($this->name, array('nologin', 'manual'))) {
2955 // these two are always enabled and can't be disabled
2956 return null;
2959 if (is_null($enabled)) {
2960 $enabled = array_flip(explode(',', $CFG->auth));
2963 return isset($enabled[$this->name]);
2966 public function get_settings_section_name() {
2967 return 'authsetting' . $this->name;
2970 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
2971 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
2972 $ADMIN = $adminroot; // may be used in settings.php
2973 $auth = $this; // also to be used inside settings.php
2974 $section = $this->get_settings_section_name();
2976 $settings = null;
2977 if ($hassiteconfig) {
2978 if (file_exists($this->full_path('settings.php'))) {
2979 // TODO: finish implementation of common settings - locking, etc.
2980 $settings = new admin_settingpage($section, $this->displayname,
2981 'moodle/site:config', $this->is_enabled() === false);
2982 include($this->full_path('settings.php')); // this may also set $settings to null
2983 } else {
2984 $settingsurl = new moodle_url('/admin/auth_config.php', array('auth' => $this->name));
2985 $settings = new admin_externalpage($section, $this->displayname,
2986 $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
2989 if ($settings) {
2990 $ADMIN->add($parentnodename, $settings);
2997 * Class for enrolment plugins
2999 class plugininfo_enrol extends plugininfo_base {
3001 public function is_enabled() {
3002 global $CFG;
3003 /** @var null|array list of enabled enrolment plugins */
3004 static $enabled = null;
3006 // We do not actually need whole enrolment classes here so we do not call
3007 // {@link enrol_get_plugins()}. Note that this may produce slightly different
3008 // results, for example if the enrolment plugin does not contain lib.php
3009 // but it is listed in $CFG->enrol_plugins_enabled
3011 if (is_null($enabled)) {
3012 $enabled = array_flip(explode(',', $CFG->enrol_plugins_enabled));
3015 return isset($enabled[$this->name]);
3018 public function get_settings_section_name() {
3019 return 'enrolsettings' . $this->name;
3022 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3023 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3024 $ADMIN = $adminroot; // may be used in settings.php
3025 $enrol = $this; // also can be used inside settings.php
3026 $section = $this->get_settings_section_name();
3028 $settings = null;
3029 if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3030 $settings = new admin_settingpage($section, $this->displayname,
3031 'moodle/site:config', $this->is_enabled() === false);
3032 include($this->full_path('settings.php')); // this may also set $settings to null
3034 if ($settings) {
3035 $ADMIN->add($parentnodename, $settings);
3039 public function get_uninstall_url() {
3040 return new moodle_url('/admin/enrol.php', array('action' => 'uninstall', 'enrol' => $this->name, 'sesskey' => sesskey()));
3046 * Class for messaging processors
3048 class plugininfo_message extends plugininfo_base {
3050 public function get_settings_section_name() {
3051 return 'messagesetting' . $this->name;
3054 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3055 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3056 $ADMIN = $adminroot; // may be used in settings.php
3057 if (!$hassiteconfig) {
3058 return;
3060 $section = $this->get_settings_section_name();
3062 $settings = null;
3063 $processors = get_message_processors();
3064 if (isset($processors[$this->name])) {
3065 $processor = $processors[$this->name];
3066 if ($processor->available && $processor->hassettings) {
3067 $settings = new admin_settingpage($section, $this->displayname,
3068 'moodle/site:config', $this->is_enabled() === false);
3069 include($this->full_path('settings.php')); // this may also set $settings to null
3072 if ($settings) {
3073 $ADMIN->add($parentnodename, $settings);
3078 * @see plugintype_interface::is_enabled()
3080 public function is_enabled() {
3081 $processors = get_message_processors();
3082 if (isset($processors[$this->name])) {
3083 return $processors[$this->name]->configured && $processors[$this->name]->enabled;
3084 } else {
3085 return parent::is_enabled();
3090 * @see plugintype_interface::get_uninstall_url()
3092 public function get_uninstall_url() {
3093 $processors = get_message_processors();
3094 if (isset($processors[$this->name])) {
3095 return new moodle_url('/admin/message.php', array('uninstall' => $processors[$this->name]->id, 'sesskey' => sesskey()));
3096 } else {
3097 return parent::get_uninstall_url();
3104 * Class for repositories
3106 class plugininfo_repository extends plugininfo_base {
3108 public function is_enabled() {
3110 $enabled = self::get_enabled_repositories();
3112 return isset($enabled[$this->name]);
3115 public function get_settings_section_name() {
3116 return 'repositorysettings'.$this->name;
3119 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3120 if ($hassiteconfig && $this->is_enabled()) {
3121 // completely no access to repository setting when it is not enabled
3122 $sectionname = $this->get_settings_section_name();
3123 $settingsurl = new moodle_url('/admin/repository.php',
3124 array('sesskey' => sesskey(), 'action' => 'edit', 'repos' => $this->name));
3125 $settings = new admin_externalpage($sectionname, $this->displayname,
3126 $settingsurl, 'moodle/site:config', false);
3127 $adminroot->add($parentnodename, $settings);
3132 * Provides access to the records in {repository} table
3134 * @param bool $disablecache do not use internal static cache
3135 * @return array array of stdClasses
3137 protected static function get_enabled_repositories($disablecache=false) {
3138 global $DB;
3139 static $repositories = null;
3141 if (is_null($repositories) or $disablecache) {
3142 $repositories = $DB->get_records('repository', null, 'type', 'type,visible,sortorder');
3145 return $repositories;
3151 * Class for portfolios
3153 class plugininfo_portfolio extends plugininfo_base {
3155 public function is_enabled() {
3157 $enabled = self::get_enabled_portfolios();
3159 return isset($enabled[$this->name]);
3163 * Provides access to the records in {portfolio_instance} table
3165 * @param bool $disablecache do not use internal static cache
3166 * @return array array of stdClasses
3168 protected static function get_enabled_portfolios($disablecache=false) {
3169 global $DB;
3170 static $portfolios = null;
3172 if (is_null($portfolios) or $disablecache) {
3173 $portfolios = array();
3174 $instances = $DB->get_recordset('portfolio_instance', null, 'plugin');
3175 foreach ($instances as $instance) {
3176 if (isset($portfolios[$instance->plugin])) {
3177 if ($instance->visible) {
3178 $portfolios[$instance->plugin]->visible = $instance->visible;
3180 } else {
3181 $portfolios[$instance->plugin] = $instance;
3186 return $portfolios;
3192 * Class for themes
3194 class plugininfo_theme extends plugininfo_base {
3196 public function is_enabled() {
3197 global $CFG;
3199 if ((!empty($CFG->theme) and $CFG->theme === $this->name) or
3200 (!empty($CFG->themelegacy) and $CFG->themelegacy === $this->name)) {
3201 return true;
3202 } else {
3203 return parent::is_enabled();
3210 * Class representing an MNet service
3212 class plugininfo_mnetservice extends plugininfo_base {
3214 public function is_enabled() {
3215 global $CFG;
3217 if (empty($CFG->mnet_dispatcher_mode) || $CFG->mnet_dispatcher_mode !== 'strict') {
3218 return false;
3219 } else {
3220 return parent::is_enabled();
3227 * Class for admin tool plugins
3229 class plugininfo_tool extends plugininfo_base {
3231 public function get_uninstall_url() {
3232 return new moodle_url('/admin/tools.php', array('delete' => $this->name, 'sesskey' => sesskey()));
3238 * Class for admin tool plugins
3240 class plugininfo_report extends plugininfo_base {
3242 public function get_uninstall_url() {
3243 return new moodle_url('/admin/reports.php', array('delete' => $this->name, 'sesskey' => sesskey()));
3249 * Class for local plugins
3251 class plugininfo_local extends plugininfo_base {
3253 public function get_uninstall_url() {
3254 return new moodle_url('/admin/localplugins.php', array('delete' => $this->name, 'sesskey' => sesskey()));
3259 * Class for HTML editors
3261 class plugininfo_editor extends plugininfo_base {
3263 public function get_settings_section_name() {
3264 return 'editorsettings' . $this->name;
3267 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3268 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3269 $ADMIN = $adminroot; // may be used in settings.php
3270 $editor = $this; // also can be used inside settings.php
3271 $section = $this->get_settings_section_name();
3273 $settings = null;
3274 if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3275 $settings = new admin_settingpage($section, $this->displayname,
3276 'moodle/site:config', $this->is_enabled() === false);
3277 include($this->full_path('settings.php')); // this may also set $settings to null
3279 if ($settings) {
3280 $ADMIN->add($parentnodename, $settings);
3285 * Returns the information about plugin availability
3287 * True means that the plugin is enabled. False means that the plugin is
3288 * disabled. Null means that the information is not available, or the
3289 * plugin does not support configurable availability or the availability
3290 * can not be changed.
3292 * @return null|bool
3294 public function is_enabled() {
3295 global $CFG;
3296 if (empty($CFG->texteditors)) {
3297 $CFG->texteditors = 'tinymce,textarea';
3299 if (in_array($this->name, explode(',', $CFG->texteditors))) {
3300 return true;
3302 return false;
3307 * Class for plagiarism plugins
3309 class plugininfo_plagiarism extends plugininfo_base {
3311 public function get_settings_section_name() {
3312 return 'plagiarism'. $this->name;
3315 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3316 // plagiarism plugin just redirect to settings.php in the plugins directory
3317 if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3318 $section = $this->get_settings_section_name();
3319 $settingsurl = new moodle_url($this->get_dir().'/settings.php');
3320 $settings = new admin_externalpage($section, $this->displayname,
3321 $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
3322 $adminroot->add($parentnodename, $settings);
3328 * Class for webservice protocols
3330 class plugininfo_webservice extends plugininfo_base {
3332 public function get_settings_section_name() {
3333 return 'webservicesetting' . $this->name;
3336 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3337 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3338 $ADMIN = $adminroot; // may be used in settings.php
3339 $webservice = $this; // also can be used inside settings.php
3340 $section = $this->get_settings_section_name();
3342 $settings = null;
3343 if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3344 $settings = new admin_settingpage($section, $this->displayname,
3345 'moodle/site:config', $this->is_enabled() === false);
3346 include($this->full_path('settings.php')); // this may also set $settings to null
3348 if ($settings) {
3349 $ADMIN->add($parentnodename, $settings);
3353 public function is_enabled() {
3354 global $CFG;
3355 if (empty($CFG->enablewebservices)) {
3356 return false;
3358 $active_webservices = empty($CFG->webserviceprotocols) ? array() : explode(',', $CFG->webserviceprotocols);
3359 if (in_array($this->name, $active_webservices)) {
3360 return true;
3362 return false;
3365 public function get_uninstall_url() {
3366 return new moodle_url('/admin/webservice/protocols.php',
3367 array('sesskey' => sesskey(), 'action' => 'uninstall', 'webservice' => $this->name));
3372 * Class for course formats
3374 class plugininfo_format extends plugininfo_base {
3377 * Gathers and returns the information about all plugins of the given type
3379 * @param string $type the name of the plugintype, eg. mod, auth or workshopform
3380 * @param string $typerootdir full path to the location of the plugin dir
3381 * @param string $typeclass the name of the actually called class
3382 * @return array of plugintype classes, indexed by the plugin name
3384 public static function get_plugins($type, $typerootdir, $typeclass) {
3385 global $CFG;
3386 $formats = parent::get_plugins($type, $typerootdir, $typeclass);
3387 require_once($CFG->dirroot.'/course/lib.php');
3388 $order = get_sorted_course_formats();
3389 $sortedformats = array();
3390 foreach ($order as $formatname) {
3391 $sortedformats[$formatname] = $formats[$formatname];
3393 return $sortedformats;
3396 public function get_settings_section_name() {
3397 return 'formatsetting' . $this->name;
3400 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3401 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3402 $ADMIN = $adminroot; // also may be used in settings.php
3403 $section = $this->get_settings_section_name();
3405 $settings = null;
3406 if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3407 $settings = new admin_settingpage($section, $this->displayname,
3408 'moodle/site:config', $this->is_enabled() === false);
3409 include($this->full_path('settings.php')); // this may also set $settings to null
3411 if ($settings) {
3412 $ADMIN->add($parentnodename, $settings);
3416 public function is_enabled() {
3417 return !get_config($this->component, 'disabled');
3420 public function get_uninstall_url() {
3421 if ($this->name !== get_config('moodlecourse', 'format') && $this->name !== 'site') {
3422 return new moodle_url('/admin/courseformats.php',
3423 array('sesskey' => sesskey(), 'action' => 'uninstall', 'format' => $this->name));
3425 return parent::get_uninstall_url();