3 // This file is part of Moodle - http://moodle.org/
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.
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/>.
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.
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();
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;
66 * Direct initiation not allowed, use the factory method {@link self::instance()}
68 protected function __construct() {
72 * Sorry, this is singleton
74 protected function __clone() {
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;
90 * Returns a tree of known plugins and information about them
92 * @param bool $disablecache force reload, cache can be used otherwise
93 * @return array 2D array. The first keys are plugin type names (e.g. qtype);
94 * the second keys are the plugin local name (e.g. multichoice); and
95 * the values are the corresponding objects extending {@link plugininfo_base}
97 public function get_plugins($disablecache=false) {
100 if ($disablecache or is_null($this->pluginsinfo)) {
101 // Hack: include mod and editor subplugin management classes first,
102 // the adminlib.php is supposed to contain extra admin settings too.
103 require_once($CFG->libdir.'/adminlib.php');
104 foreach(array('mod', 'editor') as $type) {
105 foreach (get_plugin_list($type) as $dir) {
106 if (file_exists("$dir/adminlib.php")) {
107 include_once("$dir/adminlib.php");
111 $this->pluginsinfo = array();
112 $plugintypes = get_plugin_types();
113 $plugintypes = $this->reorder_plugin_types($plugintypes);
114 foreach ($plugintypes as $plugintype => $plugintyperootdir) {
115 if (in_array($plugintype, array('base', 'general'))) {
116 throw new coding_exception('Illegal usage of reserved word for plugin type');
118 if (class_exists('plugininfo_' . $plugintype)) {
119 $plugintypeclass = 'plugininfo_' . $plugintype;
121 $plugintypeclass = 'plugininfo_general';
123 if (!in_array('plugininfo_base', class_parents($plugintypeclass))) {
124 throw new coding_exception('Class ' . $plugintypeclass . ' must extend plugininfo_base');
126 $plugins = call_user_func(array($plugintypeclass, 'get_plugins'), $plugintype, $plugintyperootdir, $plugintypeclass);
127 $this->pluginsinfo[$plugintype] = $plugins;
130 if (empty($CFG->disableupdatenotifications) and !during_initial_install()) {
131 // append the information about available updates provided by {@link available_update_checker()}
132 $provider = available_update_checker::instance();
133 foreach ($this->pluginsinfo as $plugintype => $plugins) {
134 foreach ($plugins as $plugininfoholder) {
135 $plugininfoholder->check_available_updates($provider);
141 return $this->pluginsinfo;
145 * Returns list of plugins that define their subplugins and the information
146 * about them from the db/subplugins.php file.
148 * At the moment, only activity modules and editors can define subplugins.
150 * @param bool $disablecache force reload, cache can be used otherwise
151 * @return array with keys like 'mod_quiz', and values the data from the
152 * corresponding db/subplugins.php file.
154 public function get_subplugins($disablecache=false) {
156 if ($disablecache or is_null($this->subpluginsinfo)) {
157 $this->subpluginsinfo = array();
158 foreach (array('mod', 'editor') as $type) {
159 $owners = get_plugin_list($type);
160 foreach ($owners as $component => $ownerdir) {
161 $componentsubplugins = array();
162 if (file_exists($ownerdir . '/db/subplugins.php')) {
163 $subplugins = array();
164 include($ownerdir . '/db/subplugins.php');
165 foreach ($subplugins as $subplugintype => $subplugintyperootdir) {
166 $subplugin = new stdClass();
167 $subplugin->type = $subplugintype;
168 $subplugin->typerootdir = $subplugintyperootdir;
169 $componentsubplugins[$subplugintype] = $subplugin;
171 $this->subpluginsinfo[$type . '_' . $component] = $componentsubplugins;
177 return $this->subpluginsinfo;
181 * Returns the name of the plugin that defines the given subplugin type
183 * If the given subplugin type is not actually a subplugin, returns false.
185 * @param string $subplugintype the name of subplugin type, eg. workshopform or quiz
186 * @return false|string the name of the parent plugin, eg. mod_workshop
188 public function get_parent_of_subplugin($subplugintype) {
191 foreach ($this->get_subplugins() as $pluginname => $subplugintypes) {
192 if (isset($subplugintypes[$subplugintype])) {
193 $parent = $pluginname;
202 * Returns a localized name of a given plugin
204 * @param string $plugin name of the plugin, eg mod_workshop or auth_ldap
207 public function plugin_name($plugin) {
208 list($type, $name) = normalize_component($plugin);
209 return $this->pluginsinfo[$type][$name]->displayname;
213 * Returns a localized name of a plugin type in plural form
215 * Most plugin types define their names in core_plugin lang file. In case of subplugins,
216 * we try to ask the parent plugin for the name. In the worst case, we will return
217 * the value of the passed $type parameter.
219 * @param string $type the type of the plugin, e.g. mod or workshopform
222 public function plugintype_name_plural($type) {
224 if (get_string_manager()->string_exists('type_' . $type . '_plural', 'core_plugin')) {
225 // for most plugin types, their names are defined in core_plugin lang file
226 return get_string('type_' . $type . '_plural', 'core_plugin');
228 } else if ($parent = $this->get_parent_of_subplugin($type)) {
229 // if this is a subplugin, try to ask the parent plugin for the name
230 if (get_string_manager()->string_exists('subplugintype_' . $type . '_plural', $parent)) {
231 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type . '_plural', $parent);
233 return $this->plugin_name($parent) . ' / ' . $type;
242 * @param string $component frankenstyle component name.
243 * @return plugininfo_base|null the corresponding plugin information.
245 public function get_plugin_info($component) {
246 list($type, $name) = normalize_component($component);
247 $plugins = $this->get_plugins();
248 if (isset($plugins[$type][$name])) {
249 return $plugins[$type][$name];
256 * Get a list of any other plugins that require this one.
257 * @param string $component frankenstyle component name.
258 * @return array of frankensyle component names that require this one.
260 public function other_plugins_that_require($component) {
262 foreach ($this->get_plugins() as $type => $plugins) {
263 foreach ($plugins as $plugin) {
264 $required = $plugin->get_other_required_plugins();
265 if (isset($required[$component])) {
266 $others[] = $plugin->component;
274 * Check a dependencies list against the list of installed plugins.
275 * @param array $dependencies compenent name to required version or ANY_VERSION.
276 * @return bool true if all the dependencies are satisfied.
278 public function are_dependencies_satisfied($dependencies) {
279 foreach ($dependencies as $component => $requiredversion) {
280 $otherplugin = $this->get_plugin_info($component);
281 if (is_null($otherplugin)) {
285 if ($requiredversion != ANY_VERSION and $otherplugin->versiondisk < $requiredversion) {
294 * Checks all dependencies for all installed plugins
296 * This is used by install and upgrade. The array passed by reference as the second
297 * argument is populated with the list of plugins that have failed dependencies (note that
298 * a single plugin can appear multiple times in the $failedplugins).
300 * @param int $moodleversion the version from version.php.
301 * @param array $failedplugins to return the list of plugins with non-satisfied dependencies
302 * @return bool true if all the dependencies are satisfied for all plugins.
304 public function all_plugins_ok($moodleversion, &$failedplugins = array()) {
307 foreach ($this->get_plugins() as $type => $plugins) {
308 foreach ($plugins as $plugin) {
310 if (!$plugin->is_core_dependency_satisfied($moodleversion)) {
312 $failedplugins[] = $plugin->component;
315 if (!$this->are_dependencies_satisfied($plugin->get_other_required_plugins())) {
317 $failedplugins[] = $plugin->component;
326 * Checks if there are some plugins with a known available update
328 * @return bool true if there is at least one available update
330 public function some_plugins_updatable() {
331 foreach ($this->get_plugins() as $type => $plugins) {
332 foreach ($plugins as $plugin) {
333 if ($plugin->available_updates()) {
343 * Defines a list of all plugins that were originally shipped in the standard Moodle distribution,
344 * but are not anymore and are deleted during upgrades.
346 * The main purpose of this list is to hide missing plugins during upgrade.
348 * @param string $type plugin type
349 * @param string $name plugin name
352 public static function is_deleted_standard_plugin($type, $name) {
353 static $plugins = array(
354 // do not add 1.9-2.2 plugin removals here
357 if (!isset($plugins[$type])) {
360 return in_array($name, $plugins[$type]);
364 * Defines a white list of all plugins shipped in the standard Moodle distribution
366 * @param string $type
367 * @return false|array array of standard plugins or false if the type is unknown
369 public static function standard_plugins_list($type) {
370 static $standard_plugins = array(
372 'assignment' => array(
373 'offline', 'online', 'upload', 'uploadsingle'
376 'assignsubmission' => array(
377 'comments', 'file', 'onlinetext'
380 'assignfeedback' => array(
385 'cas', 'db', 'email', 'fc', 'imap', 'ldap', 'manual', 'mnet',
386 'nntp', 'nologin', 'none', 'pam', 'pop3', 'radius',
387 'shibboleth', 'webservice'
391 'activity_modules', 'admin_bookmarks', 'blog_menu',
392 'blog_recent', 'blog_tags', 'calendar_month',
393 'calendar_upcoming', 'comments', 'community',
394 'completionstatus', 'course_list', 'course_overview',
395 'course_summary', 'feedback', 'glossary_random', 'html',
396 'login', 'mentees', 'messages', 'mnet_hosts', 'myprofile',
397 'navigation', 'news_items', 'online_users', 'participants',
398 'private_files', 'quiz_results', 'recent_activity',
399 'rss_client', 'search_forums', 'section_links',
400 'selfcompletion', 'settings', 'site_main_menu',
401 'social_activities', 'tag_flickr', 'tag_youtube', 'tags'
405 'exportimscp', 'importhtml', 'print'
408 'coursereport' => array(
412 'datafield' => array(
413 'checkbox', 'date', 'file', 'latlong', 'menu', 'multimenu',
414 'number', 'picture', 'radiobutton', 'text', 'textarea', 'url'
417 'datapreset' => array(
422 'textarea', 'tinymce'
426 'authorize', 'category', 'cohort', 'database', 'flatfile',
427 'guest', 'imsenterprise', 'ldap', 'manual', 'meta', 'mnet',
432 'activitynames', 'algebra', 'censor', 'emailprotect',
433 'emoticon', 'mediaplugin', 'multilang', 'tex', 'tidy',
434 'urltolink', 'data', 'glossary'
438 'scorm', 'social', 'topics', 'weeks'
441 'gradeexport' => array(
442 'ods', 'txt', 'xls', 'xml'
445 'gradeimport' => array(
449 'gradereport' => array(
450 'grader', 'outcomes', 'overview', 'user'
453 'gradingform' => array(
461 'email', 'jabber', 'popup'
464 'mnetservice' => array(
469 'assign', 'assignment', 'book', 'chat', 'choice', 'data', 'feedback', 'folder',
470 'forum', 'glossary', 'imscp', 'label', 'lesson', 'lti', 'page',
471 'quiz', 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop'
474 'plagiarism' => array(
477 'portfolio' => array(
478 'boxnet', 'download', 'flickr', 'googledocs', 'mahara', 'picasa'
481 'profilefield' => array(
482 'checkbox', 'datetime', 'menu', 'text', 'textarea'
485 'qbehaviour' => array(
486 'adaptive', 'adaptivenopenalty', 'deferredcbm',
487 'deferredfeedback', 'immediatecbm', 'immediatefeedback',
488 'informationitem', 'interactive', 'interactivecountback',
489 'manualgraded', 'missing'
493 'aiken', 'blackboard', 'blackboard_six', 'examview', 'gift',
494 'learnwise', 'missingword', 'multianswer', 'webct',
499 'calculated', 'calculatedmulti', 'calculatedsimple',
500 'description', 'essay', 'match', 'missingtype', 'multianswer',
501 'multichoice', 'numerical', 'random', 'randomsamatch',
502 'shortanswer', 'truefalse'
506 'grading', 'overview', 'responses', 'statistics'
509 'quizaccess' => array(
510 'delaybetweenattempts', 'ipaddress', 'numattempts', 'openclosedate',
511 'password', 'safebrowser', 'securewindow', 'timelimit'
515 'backups', 'completion', 'configlog', 'courseoverview',
516 'log', 'loglive', 'outline', 'participation', 'progress', 'questioninstances', 'security', 'stats'
519 'repository' => array(
520 'alfresco', 'boxnet', 'coursefiles', 'dropbox', 'equella', 'filesystem',
521 'flickr', 'flickr_public', 'googledocs', 'local', 'merlot',
522 'picasa', 'recent', 's3', 'upload', 'url', 'user', 'webdav',
523 'wikimedia', 'youtube'
526 'scormreport' => array(
533 'dragmath', 'moodleemoticon', 'moodleimage', 'moodlemedia', 'moodlenolink', 'spellchecker',
537 'afterburner', 'anomaly', 'arialist', 'base', 'binarius',
538 'boxxie', 'brick', 'canvas', 'formal_white', 'formfactor',
539 'fusion', 'leatherbound', 'magazine', 'mymobile', 'nimble',
540 'nonzero', 'overlay', 'serenity', 'sky_high', 'splash',
541 'standard', 'standardold'
545 'assignmentupgrade', 'capability', 'customlang', 'dbtransfer', 'generator',
546 'health', 'innodb', 'langimport', 'multilangupgrade', 'phpunit', 'profiling',
547 'qeupgradehelper', 'replace', 'spamcleaner', 'timezoneimport', 'unittest',
548 'uploaduser', 'unsuproles', 'xmldb'
551 'webservice' => array(
552 'amf', 'rest', 'soap', 'xmlrpc'
555 'workshopallocation' => array(
556 'manual', 'random', 'scheduled'
559 'workshopeval' => array(
563 'workshopform' => array(
564 'accumulative', 'comments', 'numerrors', 'rubric'
568 if (isset($standard_plugins[$type])) {
569 return $standard_plugins[$type];
576 * Reorders plugin types into a sequence to be displayed
578 * For technical reasons, plugin types returned by {@link get_plugin_types()} are
579 * in a certain order that does not need to fit the expected order for the display.
580 * Particularly, activity modules should be displayed first as they represent the
581 * real heart of Moodle. They should be followed by other plugin types that are
582 * used to build the courses (as that is what one expects from LMS). After that,
583 * other supportive plugin types follow.
585 * @param array $types associative array
586 * @return array same array with altered order of items
588 protected function reorder_plugin_types(array $types) {
590 'mod' => $types['mod'],
591 'block' => $types['block'],
592 'qtype' => $types['qtype'],
593 'qbehaviour' => $types['qbehaviour'],
594 'qformat' => $types['qformat'],
595 'filter' => $types['filter'],
596 'enrol' => $types['enrol'],
598 foreach ($types as $type => $path) {
599 if (!isset($fix[$type])) {
609 * General exception thrown by the {@link available_update_checker} class
611 class available_update_checker_exception extends moodle_exception {
614 * @param string $errorcode exception description identifier
615 * @param mixed $debuginfo debugging data to display
617 public function __construct($errorcode, $debuginfo=null) {
618 parent::__construct($errorcode, 'core_plugin', '', null, print_r($debuginfo, true));
624 * Singleton class that handles checking for available updates
626 class available_update_checker {
628 /** @var available_update_checker holds the singleton instance */
629 protected static $singletoninstance;
630 /** @var null|int the timestamp of when the most recent response was fetched */
631 protected $recentfetch = null;
632 /** @var null|array the recent response from the update notification provider */
633 protected $recentresponse = null;
634 /** @var null|string the numerical version of the local Moodle code */
635 protected $currentversion = null;
636 /** @var null|string the release info of the local Moodle code */
637 protected $currentrelease = null;
638 /** @var null|string branch of the local Moodle code */
639 protected $currentbranch = null;
640 /** @var array of (string)frankestyle => (string)version list of additional plugins deployed at this site */
641 protected $currentplugins = array();
644 * Direct initiation not allowed, use the factory method {@link self::instance()}
646 protected function __construct() {
650 * Sorry, this is singleton
652 protected function __clone() {
656 * Factory method for this class
658 * @return available_update_checker the singleton instance
660 public static function instance() {
661 if (is_null(self::$singletoninstance)) {
662 self::$singletoninstance = new self();
664 return self::$singletoninstance;
668 * Returns the timestamp of the last execution of {@link fetch()}
670 * @return int|null null if it has never been executed or we don't known
672 public function get_last_timefetched() {
674 $this->restore_response();
676 if (!empty($this->recentfetch)) {
677 return $this->recentfetch;
685 * Fetches the available update status from the remote site
687 * @throws available_update_checker_exception
689 public function fetch() {
690 $response = $this->get_response();
691 $this->validate_response($response);
692 $this->store_response($response);
696 * Returns the available update information for the given component
698 * This method returns null if the most recent response does not contain any information
699 * about it. The returned structure is an array of available updates for the given
700 * component. Each update info is an object with at least one property called
701 * 'version'. Other possible properties are 'release', 'maturity', 'url' and 'downloadurl'.
703 * For the 'core' component, the method returns real updates only (those with higher version).
704 * For all other components, the list of all known remote updates is returned and the caller
705 * (usually the {@link plugin_manager}) is supposed to make the actual comparison of versions.
707 * @param string $component frankenstyle
708 * @param array $options with supported keys 'minmaturity' and/or 'notifybuilds'
709 * @return null|array null or array of available_update_info objects
711 public function get_update_info($component, array $options = array()) {
713 if (!isset($options['minmaturity'])) {
714 $options['minmaturity'] = 0;
717 if (!isset($options['notifybuilds'])) {
718 $options['notifybuilds'] = false;
721 if ($component == 'core') {
722 $this->load_current_environment();
725 $this->restore_response();
727 if (empty($this->recentresponse['updates'][$component])) {
732 foreach ($this->recentresponse['updates'][$component] as $info) {
733 $update = new available_update_info($component, $info);
734 if (isset($update->maturity) and ($update->maturity < $options['minmaturity'])) {
737 if ($component == 'core') {
738 if ($update->version <= $this->currentversion) {
741 if (empty($options['notifybuilds']) and $this->is_same_release($update->release)) {
745 $updates[] = $update;
748 if (empty($updates)) {
756 * The method being run via cron.php
758 public function cron() {
761 if (!$this->cron_autocheck_enabled()) {
762 $this->cron_mtrace('Automatic check for available updates not enabled, skipping.');
766 $now = $this->cron_current_timestamp();
768 if ($this->cron_has_fresh_fetch($now)) {
769 $this->cron_mtrace('Recently fetched info about available updates is still fresh enough, skipping.');
773 if ($this->cron_has_outdated_fetch($now)) {
774 $this->cron_mtrace('Outdated or missing info about available updates, forced fetching ... ', '');
775 $this->cron_execute();
779 $offset = $this->cron_execution_offset();
780 $start = mktime(1, 0, 0, date('n', $now), date('j', $now), date('Y', $now)); // 01:00 AM today local time
781 if ($now > $start + $offset) {
782 $this->cron_mtrace('Regular daily check for available updates ... ', '');
783 $this->cron_execute();
788 /// end of public API //////////////////////////////////////////////////////
791 * Makes cURL request to get data from the remote site
793 * @return string raw request result
794 * @throws available_update_checker_exception
796 protected function get_response() {
798 require_once($CFG->libdir.'/filelib.php');
800 $curl = new curl(array('proxy' => true));
801 $response = $curl->post($this->prepare_request_url(), $this->prepare_request_params());
802 $curlinfo = $curl->get_info();
803 if ($curlinfo['http_code'] != 200) {
804 throw new available_update_checker_exception('err_response_http_code', $curlinfo['http_code']);
810 * Makes sure the response is valid, has correct API format etc.
812 * @param string $response raw response as returned by the {@link self::get_response()}
813 * @throws available_update_checker_exception
815 protected function validate_response($response) {
817 $response = $this->decode_response($response);
819 if (empty($response)) {
820 throw new available_update_checker_exception('err_response_empty');
823 if (empty($response['status']) or $response['status'] !== 'OK') {
824 throw new available_update_checker_exception('err_response_status', $response['status']);
827 if (empty($response['apiver']) or $response['apiver'] !== '1.0') {
828 throw new available_update_checker_exception('err_response_format_version', $response['apiver']);
831 if (empty($response['forbranch']) or $response['forbranch'] !== moodle_major_version(true)) {
832 throw new available_update_checker_exception('err_response_target_version', $response['forbranch']);
837 * Decodes the raw string response from the update notifications provider
839 * @param string $response as returned by {@link self::get_response()}
840 * @return array decoded response structure
842 protected function decode_response($response) {
843 return json_decode($response, true);
847 * Stores the valid fetched response for later usage
849 * This implementation uses the config_plugins table as the permanent storage.
851 * @param string $response raw valid data returned by {@link self::get_response()}
853 protected function store_response($response) {
855 set_config('recentfetch', time(), 'core_plugin');
856 set_config('recentresponse', $response, 'core_plugin');
858 $this->restore_response(true);
862 * Loads the most recent raw response record we have fetched
864 * This implementation uses the config_plugins table as the permanent storage.
866 * @param bool $forcereload reload even if it was already loaded
868 protected function restore_response($forcereload = false) {
870 if (!$forcereload and !is_null($this->recentresponse)) {
871 // we already have it, nothing to do
875 $config = get_config('core_plugin');
877 if (!empty($config->recentresponse) and !empty($config->recentfetch)) {
879 $this->validate_response($config->recentresponse);
880 $this->recentfetch = $config->recentfetch;
881 $this->recentresponse = $this->decode_response($config->recentresponse);
882 } catch (available_update_checker_exception $e) {
883 // do not set recentresponse if the validation fails
887 $this->recentresponse = array();
892 * Compares two raw {@link $recentresponse} records and returns the list of changed updates
894 * This method is used to populate potential update info to be sent to site admins.
898 * @throws available_update_checker_exception
899 * @return array parts of $new['updates'] that have changed
901 protected function compare_responses(array $old, array $new) {
907 if (!array_key_exists('updates', $new)) {
908 throw new available_update_checker_exception('err_response_format');
912 return $new['updates'];
915 if (!array_key_exists('updates', $old)) {
916 throw new available_update_checker_exception('err_response_format');
921 foreach ($new['updates'] as $newcomponent => $newcomponentupdates) {
922 if (empty($old['updates'][$newcomponent])) {
923 $changes[$newcomponent] = $newcomponentupdates;
926 foreach ($newcomponentupdates as $newcomponentupdate) {
928 foreach ($old['updates'][$newcomponent] as $oldcomponentupdate) {
929 if ($newcomponentupdate['version'] == $oldcomponentupdate['version']) {
934 if (!isset($changes[$newcomponent])) {
935 $changes[$newcomponent] = array();
937 $changes[$newcomponent][] = $newcomponentupdate;
946 * Returns the URL to send update requests to
948 * During the development or testing, you can set $CFG->alternativeupdateproviderurl
949 * to a custom URL that will be used. Otherwise the standard URL will be returned.
953 protected function prepare_request_url() {
956 if (!empty($CFG->alternativeupdateproviderurl)) {
957 return $CFG->alternativeupdateproviderurl;
959 return 'http://download.moodle.org/api/1.0/updates.php';
964 * Sets the properties currentversion, currentrelease, currentbranch and currentplugins
966 * @param bool $forcereload
968 protected function load_current_environment($forcereload=false) {
971 if (!is_null($this->currentversion) and !$forcereload) {
979 require($CFG->dirroot.'/version.php');
980 $this->currentversion = $version;
981 $this->currentrelease = $release;
982 $this->currentbranch = moodle_major_version(true);
984 $pluginman = plugin_manager::instance();
985 foreach ($pluginman->get_plugins() as $type => $plugins) {
986 foreach ($plugins as $plugin) {
987 if (!$plugin->is_standard()) {
988 $this->currentplugins[$plugin->component] = $plugin->versiondisk;
995 * Returns the list of HTTP params to be sent to the updates provider URL
997 * @return array of (string)param => (string)value
999 protected function prepare_request_params() {
1002 $this->load_current_environment();
1003 $this->restore_response();
1006 $params['format'] = 'json';
1008 if (isset($this->recentresponse['ticket'])) {
1009 $params['ticket'] = $this->recentresponse['ticket'];
1012 if (isset($this->currentversion)) {
1013 $params['version'] = $this->currentversion;
1015 throw new coding_exception('Main Moodle version must be already known here');
1018 if (isset($this->currentbranch)) {
1019 $params['branch'] = $this->currentbranch;
1021 throw new coding_exception('Moodle release must be already known here');
1025 foreach ($this->currentplugins as $plugin => $version) {
1026 $plugins[] = $plugin.'@'.$version;
1028 if (!empty($plugins)) {
1029 $params['plugins'] = implode(',', $plugins);
1036 * Returns the current timestamp
1038 * @return int the timestamp
1040 protected function cron_current_timestamp() {
1045 * Output cron debugging info
1048 * @param string $msg output message
1049 * @param string $eol end of line
1051 protected function cron_mtrace($msg, $eol = PHP_EOL) {
1056 * Decide if the autocheck feature is disabled in the server setting
1058 * @return bool true if autocheck enabled, false if disabled
1060 protected function cron_autocheck_enabled() {
1063 if (empty($CFG->updateautocheck)) {
1071 * Decide if the recently fetched data are still fresh enough
1073 * @param int $now current timestamp
1074 * @return bool true if no need to re-fetch, false otherwise
1076 protected function cron_has_fresh_fetch($now) {
1077 $recent = $this->get_last_timefetched();
1079 if (empty($recent)) {
1083 if ($now < $recent) {
1084 $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1088 if ($now - $recent > HOURSECS) {
1096 * Decide if the fetch is outadated or even missing
1098 * @param int $now current timestamp
1099 * @return bool false if no need to re-fetch, true otherwise
1101 protected function cron_has_outdated_fetch($now) {
1102 $recent = $this->get_last_timefetched();
1104 if (empty($recent)) {
1108 if ($now < $recent) {
1109 $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1113 if ($now - $recent > 48 * HOURSECS) {
1121 * Returns the cron execution offset for this site
1123 * The main {@link self::cron()} is supposed to run every night in some random time
1124 * between 01:00 and 06:00 AM (local time). The exact moment is defined by so called
1125 * execution offset, that is the amount of time after 01:00 AM. The offset value is
1126 * initially generated randomly and then used consistently at the site. This way, the
1127 * regular checks against the download.moodle.org server are spread in time.
1129 * @return int the offset number of seconds from range 1 sec to 5 hours
1131 protected function cron_execution_offset() {
1134 if (empty($CFG->updatecronoffset)) {
1135 set_config('updatecronoffset', rand(1, 5 * HOURSECS));
1138 return $CFG->updatecronoffset;
1142 * Fetch available updates info and eventually send notification to site admins
1144 protected function cron_execute() {
1147 $this->restore_response();
1148 $previous = $this->recentresponse;
1150 $this->restore_response(true);
1151 $current = $this->recentresponse;
1152 $changes = $this->compare_responses($previous, $current);
1153 $notifications = $this->cron_notifications($changes);
1154 $this->cron_notify($notifications);
1155 $this->cron_mtrace('done');
1156 } catch (available_update_checker_exception $e) {
1157 $this->cron_mtrace('FAILED!');
1162 * Given the list of changes in available updates, pick those to send to site admins
1164 * @param array $changes as returned by {@link self::compare_responses()}
1165 * @return array of available_update_info objects to send to site admins
1167 protected function cron_notifications(array $changes) {
1170 $notifications = array();
1171 $pluginman = plugin_manager::instance();
1172 $plugins = $pluginman->get_plugins(true);
1174 foreach ($changes as $component => $componentchanges) {
1175 if (empty($componentchanges)) {
1178 $componentupdates = $this->get_update_info($component,
1179 array('minmaturity' => $CFG->updateminmaturity, 'notifybuilds' => $CFG->updatenotifybuilds));
1180 if (empty($componentupdates)) {
1183 // notify only about those $componentchanges that are present in $componentupdates
1184 // to respect the preferences
1185 foreach ($componentchanges as $componentchange) {
1186 foreach ($componentupdates as $componentupdate) {
1187 if ($componentupdate->version == $componentchange['version']) {
1188 if ($component == 'core') {
1189 // in case of 'core' this is enough, we already know that the
1190 // $componentupdate is a real update with higher version
1191 $notifications[] = $componentupdate;
1193 // use the plugin_manager to check if the reported $componentchange
1194 // is a real update with higher version. such a real update must be
1195 // present in the 'availableupdates' property of one of the component's
1196 // available_update_info object
1197 list($plugintype, $pluginname) = normalize_component($component);
1198 if (!empty($plugins[$plugintype][$pluginname]->availableupdates)) {
1199 foreach ($plugins[$plugintype][$pluginname]->availableupdates as $availableupdate) {
1200 if ($availableupdate->version == $componentchange['version']) {
1201 $notifications[] = $componentupdate;
1211 return $notifications;
1215 * Sends the given notifications to site admins via messaging API
1217 * @param array $notifications array of available_update_info objects to send
1219 protected function cron_notify(array $notifications) {
1222 if (empty($notifications)) {
1226 $admins = get_admins();
1228 if (empty($admins)) {
1232 $this->cron_mtrace('sending notifications ... ', '');
1234 $text = get_string('updatenotifications', 'core_admin') . PHP_EOL;
1235 $html = html_writer::tag('h1', get_string('updatenotifications', 'core_admin')) . PHP_EOL;
1237 $coreupdates = array();
1238 $pluginupdates = array();
1240 foreach ($notifications as $notification) {
1241 if ($notification->component == 'core') {
1242 $coreupdates[] = $notification;
1244 $pluginupdates[] = $notification;
1248 if (!empty($coreupdates)) {
1249 $text .= PHP_EOL . get_string('updateavailable', 'core_admin') . PHP_EOL;
1250 $html .= html_writer::tag('h2', get_string('updateavailable', 'core_admin')) . PHP_EOL;
1251 $html .= html_writer::start_tag('ul') . PHP_EOL;
1252 foreach ($coreupdates as $coreupdate) {
1253 $html .= html_writer::start_tag('li');
1254 if (isset($coreupdate->release)) {
1255 $text .= get_string('updateavailable_release', 'core_admin', $coreupdate->release);
1256 $html .= html_writer::tag('strong', get_string('updateavailable_release', 'core_admin', $coreupdate->release));
1258 if (isset($coreupdate->version)) {
1259 $text .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
1260 $html .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
1262 if (isset($coreupdate->maturity)) {
1263 $text .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
1264 $html .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
1267 $html .= html_writer::end_tag('li') . PHP_EOL;
1270 $html .= html_writer::end_tag('ul') . PHP_EOL;
1272 $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/index.php');
1273 $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
1274 $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/index.php', $CFG->wwwroot.'/'.$CFG->admin.'/index.php'));
1275 $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
1278 if (!empty($pluginupdates)) {
1279 $text .= PHP_EOL . get_string('updateavailableforplugin', 'core_admin') . PHP_EOL;
1280 $html .= html_writer::tag('h2', get_string('updateavailableforplugin', 'core_admin')) . PHP_EOL;
1282 $html .= html_writer::start_tag('ul') . PHP_EOL;
1283 foreach ($pluginupdates as $pluginupdate) {
1284 $html .= html_writer::start_tag('li');
1285 $text .= get_string('pluginname', $pluginupdate->component);
1286 $html .= html_writer::tag('strong', get_string('pluginname', $pluginupdate->component));
1288 $text .= ' ('.$pluginupdate->component.')';
1289 $html .= ' ('.$pluginupdate->component.')';
1291 $text .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
1292 $html .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
1295 $html .= html_writer::end_tag('li') . PHP_EOL;
1298 $html .= html_writer::end_tag('ul') . PHP_EOL;
1300 $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php');
1301 $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
1302 $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/plugins.php', $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php'));
1303 $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
1306 $a = array('siteurl' => $CFG->wwwroot);
1307 $text .= get_string('updatenotificationfooter', 'core_admin', $a) . PHP_EOL;
1308 $a = array('siteurl' => html_writer::link($CFG->wwwroot, $CFG->wwwroot));
1309 $html .= html_writer::tag('footer', html_writer::tag('p', get_string('updatenotificationfooter', 'core_admin', $a),
1310 array('style' => 'font-size:smaller; color:#333;')));
1312 $mainadmin = reset($admins);
1314 foreach ($admins as $admin) {
1315 $message = new stdClass();
1316 $message->component = 'moodle';
1317 $message->name = 'availableupdate';
1318 $message->userfrom = $mainadmin;
1319 $message->userto = $admin;
1320 $message->subject = get_string('updatenotifications', 'core_admin');
1321 $message->fullmessage = $text;
1322 $message->fullmessageformat = FORMAT_PLAIN;
1323 $message->fullmessagehtml = $html;
1324 $message->smallmessage = get_string('updatenotifications', 'core_admin');
1325 $message->notification = 1;
1326 message_send($message);
1331 * Compare two release labels and decide if they are the same
1333 * @param string $remote release info of the available update
1334 * @param null|string $local release info of the local code, defaults to $release defined in version.php
1335 * @return boolean true if the releases declare the same minor+major version
1337 protected function is_same_release($remote, $local=null) {
1339 if (is_null($local)) {
1340 $this->load_current_environment();
1341 $local = $this->currentrelease;
1344 $pattern = '/^([0-9\.\+]+)([^(]*)/';
1346 preg_match($pattern, $remote, $remotematches);
1347 preg_match($pattern, $local, $localmatches);
1349 $remotematches[1] = str_replace('+', '', $remotematches[1]);
1350 $localmatches[1] = str_replace('+', '', $localmatches[1]);
1352 if ($remotematches[1] === $localmatches[1] and rtrim($remotematches[2]) === rtrim($localmatches[2])) {
1362 * Defines the structure of objects returned by {@link available_update_checker::get_update_info()}
1364 class available_update_info {
1366 /** @var string frankenstyle component name */
1368 /** @var int the available version of the component */
1370 /** @var string|null optional release name */
1371 public $release = null;
1372 /** @var int|null optional maturity info, eg {@link MATURITY_STABLE} */
1373 public $maturity = null;
1374 /** @var string|null optional URL of a page with more info about the update */
1376 /** @var string|null optional URL of a ZIP package that can be downloaded and installed */
1377 public $download = null;
1380 * Creates new instance of the class
1382 * The $info array must provide at least the 'version' value and optionally all other
1383 * values to populate the object's properties.
1385 * @param string $name the frankenstyle component name
1386 * @param array $info associative array with other properties
1388 public function __construct($name, array $info) {
1389 $this->component = $name;
1390 foreach ($info as $k => $v) {
1391 if (property_exists('available_update_info', $k) and $k != 'component') {
1400 * Factory class producing required subclasses of {@link plugininfo_base}
1402 class plugininfo_default_factory {
1405 * Makes a new instance of the plugininfo class
1407 * @param string $type the plugin type, eg. 'mod'
1408 * @param string $typerootdir full path to the location of all the plugins of this type
1409 * @param string $name the plugin name, eg. 'workshop'
1410 * @param string $namerootdir full path to the location of the plugin
1411 * @param string $typeclass the name of class that holds the info about the plugin
1412 * @return plugininfo_base the instance of $typeclass
1414 public static function make($type, $typerootdir, $name, $namerootdir, $typeclass) {
1415 $plugin = new $typeclass();
1416 $plugin->type = $type;
1417 $plugin->typerootdir = $typerootdir;
1418 $plugin->name = $name;
1419 $plugin->rootdir = $namerootdir;
1421 $plugin->init_display_name();
1422 $plugin->load_disk_version();
1423 $plugin->load_db_version();
1424 $plugin->load_required_main_version();
1425 $plugin->init_is_standard();
1433 * Base class providing access to the information about a plugin
1435 * @property-read string component the component name, type_name
1437 abstract class plugininfo_base {
1439 /** @var string the plugintype name, eg. mod, auth or workshopform */
1441 /** @var string full path to the location of all the plugins of this type */
1442 public $typerootdir;
1443 /** @var string the plugin name, eg. assignment, ldap */
1445 /** @var string the localized plugin name */
1446 public $displayname;
1447 /** @var string the plugin source, one of plugin_manager::PLUGIN_SOURCE_xxx constants */
1449 /** @var fullpath to the location of this plugin */
1451 /** @var int|string the version of the plugin's source code */
1452 public $versiondisk;
1453 /** @var int|string the version of the installed plugin */
1455 /** @var int|float|string required version of Moodle core */
1456 public $versionrequires;
1457 /** @var array other plugins that this one depends on, lazy-loaded by {@link get_other_required_plugins()} */
1458 public $dependencies;
1459 /** @var int number of instances of the plugin - not supported yet */
1461 /** @var int order of the plugin among other plugins of the same type - not supported yet */
1463 /** @var array|null array of {@link available_update_info} for this plugin */
1464 public $availableupdates;
1467 * Gathers and returns the information about all plugins of the given type
1469 * @param string $type the name of the plugintype, eg. mod, auth or workshopform
1470 * @param string $typerootdir full path to the location of the plugin dir
1471 * @param string $typeclass the name of the actually called class
1472 * @return array of plugintype classes, indexed by the plugin name
1474 public static function get_plugins($type, $typerootdir, $typeclass) {
1476 // get the information about plugins at the disk
1477 $plugins = get_plugin_list($type);
1479 foreach ($plugins as $pluginname => $pluginrootdir) {
1480 $ondisk[$pluginname] = plugininfo_default_factory::make($type, $typerootdir,
1481 $pluginname, $pluginrootdir, $typeclass);
1487 * Sets {@link $displayname} property to a localized name of the plugin
1489 public function init_display_name() {
1490 if (!get_string_manager()->string_exists('pluginname', $this->component)) {
1491 $this->displayname = '[pluginname,' . $this->component . ']';
1493 $this->displayname = get_string('pluginname', $this->component);
1498 * Magic method getter, redirects to read only values.
1500 * @param string $name
1503 public function __get($name) {
1505 case 'component': return $this->type . '_' . $this->name;
1508 debugging('Invalid plugin property accessed! '.$name);
1514 * Return the full path name of a file within the plugin.
1516 * No check is made to see if the file exists.
1518 * @param string $relativepath e.g. 'version.php'.
1519 * @return string e.g. $CFG->dirroot . '/mod/quiz/version.php'.
1521 public function full_path($relativepath) {
1522 if (empty($this->rootdir)) {
1525 return $this->rootdir . '/' . $relativepath;
1529 * Load the data from version.php.
1531 * @return stdClass the object called $plugin defined in version.php
1533 protected function load_version_php() {
1534 $versionfile = $this->full_path('version.php');
1536 $plugin = new stdClass();
1537 if (is_readable($versionfile)) {
1538 include($versionfile);
1544 * Sets {@link $versiondisk} property to a numerical value representing the
1545 * version of the plugin's source code.
1547 * If the value is null after calling this method, either the plugin
1548 * does not use versioning (typically does not have any database
1549 * data) or is missing from disk.
1551 public function load_disk_version() {
1552 $plugin = $this->load_version_php();
1553 if (isset($plugin->version)) {
1554 $this->versiondisk = $plugin->version;
1559 * Sets {@link $versionrequires} property to a numerical value representing
1560 * the version of Moodle core that this plugin requires.
1562 public function load_required_main_version() {
1563 $plugin = $this->load_version_php();
1564 if (isset($plugin->requires)) {
1565 $this->versionrequires = $plugin->requires;
1570 * Initialise {@link $dependencies} to the list of other plugins (in any)
1571 * that this one requires to be installed.
1573 protected function load_other_required_plugins() {
1574 $plugin = $this->load_version_php();
1575 if (!empty($plugin->dependencies)) {
1576 $this->dependencies = $plugin->dependencies;
1578 $this->dependencies = array(); // By default, no dependencies.
1583 * Get the list of other plugins that this plugin requires to be installed.
1585 * @return array with keys the frankenstyle plugin name, and values either
1586 * a version string (like '2011101700') or the constant ANY_VERSION.
1588 public function get_other_required_plugins() {
1589 if (is_null($this->dependencies)) {
1590 $this->load_other_required_plugins();
1592 return $this->dependencies;
1596 * Sets {@link $versiondb} property to a numerical value representing the
1597 * currently installed version of the plugin.
1599 * If the value is null after calling this method, either the plugin
1600 * does not use versioning (typically does not have any database
1601 * data) or has not been installed yet.
1603 public function load_db_version() {
1604 if ($ver = self::get_version_from_config_plugins($this->component)) {
1605 $this->versiondb = $ver;
1610 * Sets {@link $source} property to one of plugin_manager::PLUGIN_SOURCE_xxx
1613 * If the property's value is null after calling this method, then
1614 * the type of the plugin has not been recognized and you should throw
1617 public function init_is_standard() {
1619 $standard = plugin_manager::standard_plugins_list($this->type);
1621 if ($standard !== false) {
1622 $standard = array_flip($standard);
1623 if (isset($standard[$this->name])) {
1624 $this->source = plugin_manager::PLUGIN_SOURCE_STANDARD;
1625 } else if (!is_null($this->versiondb) and is_null($this->versiondisk)
1626 and plugin_manager::is_deleted_standard_plugin($this->type, $this->name)) {
1627 $this->source = plugin_manager::PLUGIN_SOURCE_STANDARD; // to be deleted
1629 $this->source = plugin_manager::PLUGIN_SOURCE_EXTENSION;
1635 * Returns true if the plugin is shipped with the official distribution
1636 * of the current Moodle version, false otherwise.
1640 public function is_standard() {
1641 return $this->source === plugin_manager::PLUGIN_SOURCE_STANDARD;
1645 * Returns true if the the given Moodle version is enough to run this plugin
1647 * @param string|int|double $moodleversion
1650 public function is_core_dependency_satisfied($moodleversion) {
1652 if (empty($this->versionrequires)) {
1656 return (double)$this->versionrequires <= (double)$moodleversion;
1661 * Returns the status of the plugin
1663 * @return string one of plugin_manager::PLUGIN_STATUS_xxx constants
1665 public function get_status() {
1667 if (is_null($this->versiondb) and is_null($this->versiondisk)) {
1668 return plugin_manager::PLUGIN_STATUS_NODB;
1670 } else if (is_null($this->versiondb) and !is_null($this->versiondisk)) {
1671 return plugin_manager::PLUGIN_STATUS_NEW;
1673 } else if (!is_null($this->versiondb) and is_null($this->versiondisk)) {
1674 if (plugin_manager::is_deleted_standard_plugin($this->type, $this->name)) {
1675 return plugin_manager::PLUGIN_STATUS_DELETE;
1677 return plugin_manager::PLUGIN_STATUS_MISSING;
1680 } else if ((string)$this->versiondb === (string)$this->versiondisk) {
1681 return plugin_manager::PLUGIN_STATUS_UPTODATE;
1683 } else if ($this->versiondb < $this->versiondisk) {
1684 return plugin_manager::PLUGIN_STATUS_UPGRADE;
1686 } else if ($this->versiondb > $this->versiondisk) {
1687 return plugin_manager::PLUGIN_STATUS_DOWNGRADE;
1690 // $version = pi(); and similar funny jokes - hopefully Donald E. Knuth will never contribute to Moodle ;-)
1691 throw new coding_exception('Unable to determine plugin state, check the plugin versions');
1696 * Returns the information about plugin availability
1698 * True means that the plugin is enabled. False means that the plugin is
1699 * disabled. Null means that the information is not available, or the
1700 * plugin does not support configurable availability or the availability
1701 * can not be changed.
1705 public function is_enabled() {
1710 * Populates the property {@link $availableupdates} with the information provided by
1711 * available update checker
1713 * @param available_update_checker $provider the class providing the available update info
1715 public function check_available_updates(available_update_checker $provider) {
1718 if (isset($CFG->updateminmaturity)) {
1719 $minmaturity = $CFG->updateminmaturity;
1721 // this can happen during the very first upgrade to 2.3
1722 $minmaturity = MATURITY_STABLE;
1725 $this->availableupdates = $provider->get_update_info($this->component,
1726 array('minmaturity' => $minmaturity));
1730 * If there are updates for this plugin available, returns them.
1732 * Returns array of {@link available_update_info} objects, if some update
1733 * is available. Returns null if there is no update available or if the update
1734 * availability is unknown.
1736 * @return array|null
1738 public function available_updates() {
1740 if (empty($this->availableupdates) or !is_array($this->availableupdates)) {
1746 foreach ($this->availableupdates as $availableupdate) {
1747 if ($availableupdate->version > $this->versiondisk) {
1748 $updates[] = $availableupdate;
1752 if (empty($updates)) {
1760 * Returns the URL of the plugin settings screen
1762 * Null value means that the plugin either does not have the settings screen
1763 * or its location is not available via this library.
1765 * @return null|moodle_url
1767 public function get_settings_url() {
1772 * Returns the URL of the screen where this plugin can be uninstalled
1774 * Visiting that URL must be safe, that is a manual confirmation is needed
1775 * for actual uninstallation of the plugin. Null value means that the
1776 * plugin either does not support uninstallation, or does not require any
1777 * database cleanup or the location of the screen is not available via this
1780 * @return null|moodle_url
1782 public function get_uninstall_url() {
1787 * Returns relative directory of the plugin with heading '/'
1791 public function get_dir() {
1794 return substr($this->rootdir, strlen($CFG->dirroot));
1798 * Provides access to plugin versions from {config_plugins}
1800 * @param string $plugin plugin name
1801 * @param double $disablecache optional, defaults to false
1802 * @return int|false the stored value or false if not found
1804 protected function get_version_from_config_plugins($plugin, $disablecache=false) {
1806 static $pluginversions = null;
1808 if (is_null($pluginversions) or $disablecache) {
1810 $pluginversions = $DB->get_records_menu('config_plugins', array('name' => 'version'), 'plugin', 'plugin,value');
1811 } catch (dml_exception $e) {
1813 $pluginversions = array();
1817 if (!array_key_exists($plugin, $pluginversions)) {
1821 return $pluginversions[$plugin];
1827 * General class for all plugin types that do not have their own class
1829 class plugininfo_general extends plugininfo_base {
1834 * Class for page side blocks
1836 class plugininfo_block extends plugininfo_base {
1838 public static function get_plugins($type, $typerootdir, $typeclass) {
1840 // get the information about blocks at the disk
1841 $blocks = parent::get_plugins($type, $typerootdir, $typeclass);
1843 // add blocks missing from disk
1844 $blocksinfo = self::get_blocks_info();
1845 foreach ($blocksinfo as $blockname => $blockinfo) {
1846 if (isset($blocks[$blockname])) {
1849 $plugin = new $typeclass();
1850 $plugin->type = $type;
1851 $plugin->typerootdir = $typerootdir;
1852 $plugin->name = $blockname;
1853 $plugin->rootdir = null;
1854 $plugin->displayname = $blockname;
1855 $plugin->versiondb = $blockinfo->version;
1856 $plugin->init_is_standard();
1858 $blocks[$blockname] = $plugin;
1864 public function init_display_name() {
1866 if (get_string_manager()->string_exists('pluginname', 'block_' . $this->name)) {
1867 $this->displayname = get_string('pluginname', 'block_' . $this->name);
1869 } else if (($block = block_instance($this->name)) !== false) {
1870 $this->displayname = $block->get_title();
1873 parent::init_display_name();
1877 public function load_db_version() {
1880 $blocksinfo = self::get_blocks_info();
1881 if (isset($blocksinfo[$this->name]->version)) {
1882 $this->versiondb = $blocksinfo[$this->name]->version;
1886 public function is_enabled() {
1888 $blocksinfo = self::get_blocks_info();
1889 if (isset($blocksinfo[$this->name]->visible)) {
1890 if ($blocksinfo[$this->name]->visible) {
1896 return parent::is_enabled();
1900 public function get_settings_url() {
1902 if (($block = block_instance($this->name)) === false) {
1903 return parent::get_settings_url();
1905 } else if ($block->has_config()) {
1906 if (file_exists($this->full_path('settings.php'))) {
1907 return new moodle_url('/admin/settings.php', array('section' => 'blocksetting' . $this->name));
1909 $blocksinfo = self::get_blocks_info();
1910 return new moodle_url('/admin/block.php', array('block' => $blocksinfo[$this->name]->id));
1914 return parent::get_settings_url();
1918 public function get_uninstall_url() {
1920 $blocksinfo = self::get_blocks_info();
1921 return new moodle_url('/admin/blocks.php', array('delete' => $blocksinfo[$this->name]->id, 'sesskey' => sesskey()));
1925 * Provides access to the records in {block} table
1927 * @param bool $disablecache do not use internal static cache
1928 * @return array array of stdClasses
1930 protected static function get_blocks_info($disablecache=false) {
1932 static $blocksinfocache = null;
1934 if (is_null($blocksinfocache) or $disablecache) {
1936 $blocksinfocache = $DB->get_records('block', null, 'name', 'name,id,version,visible');
1937 } catch (dml_exception $e) {
1939 $blocksinfocache = array();
1943 return $blocksinfocache;
1949 * Class for text filters
1951 class plugininfo_filter extends plugininfo_base {
1953 public static function get_plugins($type, $typerootdir, $typeclass) {
1958 // get the list of filters from both /filter and /mod location
1959 $installed = filter_get_all_installed();
1961 foreach ($installed as $filterlegacyname => $displayname) {
1962 $plugin = new $typeclass();
1963 $plugin->type = $type;
1964 $plugin->typerootdir = $typerootdir;
1965 $plugin->name = self::normalize_legacy_name($filterlegacyname);
1966 $plugin->rootdir = $CFG->dirroot . '/' . $filterlegacyname;
1967 $plugin->displayname = $displayname;
1969 $plugin->load_disk_version();
1970 $plugin->load_db_version();
1971 $plugin->load_required_main_version();
1972 $plugin->init_is_standard();
1974 $filters[$plugin->name] = $plugin;
1977 $globalstates = self::get_global_states();
1979 if ($DB->get_manager()->table_exists('filter_active')) {
1980 // if we're upgrading from 1.9, the table does not exist yet
1981 // if it does, make sure that all installed filters are registered
1982 $needsreload = false;
1983 foreach (array_keys($installed) as $filterlegacyname) {
1984 if (!isset($globalstates[self::normalize_legacy_name($filterlegacyname)])) {
1985 filter_set_global_state($filterlegacyname, TEXTFILTER_DISABLED);
1986 $needsreload = true;
1990 $globalstates = self::get_global_states(true);
1994 // make sure that all registered filters are installed, just in case
1995 foreach ($globalstates as $name => $info) {
1996 if (!isset($filters[$name])) {
1997 // oops, there is a record in filter_active but the filter is not installed
1998 $plugin = new $typeclass();
1999 $plugin->type = $type;
2000 $plugin->typerootdir = $typerootdir;
2001 $plugin->name = $name;
2002 $plugin->rootdir = $CFG->dirroot . '/' . $info->legacyname;
2003 $plugin->displayname = $info->legacyname;
2005 $plugin->load_db_version();
2007 if (is_null($plugin->versiondb)) {
2008 // this is a hack to stimulate 'Missing from disk' error
2009 // because $plugin->versiondisk will be null !== false
2010 $plugin->versiondb = false;
2013 $filters[$plugin->name] = $plugin;
2020 public function init_display_name() {
2021 // do nothing, the name is set in self::get_plugins()
2025 * @see load_version_php()
2027 protected function load_version_php() {
2028 if (strpos($this->name, 'mod_') === 0) {
2029 // filters bundled with modules do not have a version.php and so
2030 // do not provide their own versioning information.
2031 return new stdClass();
2033 return parent::load_version_php();
2036 public function is_enabled() {
2038 $globalstates = self::get_global_states();
2040 foreach ($globalstates as $filterlegacyname => $info) {
2041 $name = self::normalize_legacy_name($filterlegacyname);
2042 if ($name === $this->name) {
2043 if ($info->active == TEXTFILTER_DISABLED) {
2046 // it may be 'On' or 'Off, but available'
2055 public function get_settings_url() {
2057 $globalstates = self::get_global_states();
2058 $legacyname = $globalstates[$this->name]->legacyname;
2059 if (filter_has_global_settings($legacyname)) {
2060 return new moodle_url('/admin/settings.php', array('section' => 'filtersetting' . str_replace('/', '', $legacyname)));
2066 public function get_uninstall_url() {
2068 if (strpos($this->name, 'mod_') === 0) {
2071 $globalstates = self::get_global_states();
2072 $legacyname = $globalstates[$this->name]->legacyname;
2073 return new moodle_url('/admin/filters.php', array('sesskey' => sesskey(), 'filterpath' => $legacyname, 'action' => 'delete'));
2078 * Convert legacy filter names like 'filter/foo' or 'mod/bar' into frankenstyle
2080 * @param string $legacyfiltername legacy filter name
2081 * @return string frankenstyle-like name
2083 protected static function normalize_legacy_name($legacyfiltername) {
2085 $name = str_replace('/', '_', $legacyfiltername);
2086 if (strpos($name, 'filter_') === 0) {
2087 $name = substr($name, 7);
2089 throw new coding_exception('Unable to determine filter name: ' . $legacyfiltername);
2097 * Provides access to the results of {@link filter_get_global_states()}
2098 * but indexed by the normalized filter name
2100 * The legacy filter name is available as ->legacyname property.
2102 * @param bool $disablecache
2105 protected static function get_global_states($disablecache=false) {
2107 static $globalstatescache = null;
2109 if ($disablecache or is_null($globalstatescache)) {
2111 if (!$DB->get_manager()->table_exists('filter_active')) {
2112 // we're upgrading from 1.9 and the table used by {@link filter_get_global_states()}
2113 // does not exist yet
2114 $globalstatescache = array();
2117 foreach (filter_get_global_states() as $legacyname => $info) {
2118 $name = self::normalize_legacy_name($legacyname);
2119 $filterinfo = new stdClass();
2120 $filterinfo->legacyname = $legacyname;
2121 $filterinfo->active = $info->active;
2122 $filterinfo->sortorder = $info->sortorder;
2123 $globalstatescache[$name] = $filterinfo;
2128 return $globalstatescache;
2134 * Class for activity modules
2136 class plugininfo_mod extends plugininfo_base {
2138 public static function get_plugins($type, $typerootdir, $typeclass) {
2140 // get the information about plugins at the disk
2141 $modules = parent::get_plugins($type, $typerootdir, $typeclass);
2143 // add modules missing from disk
2144 $modulesinfo = self::get_modules_info();
2145 foreach ($modulesinfo as $modulename => $moduleinfo) {
2146 if (isset($modules[$modulename])) {
2149 $plugin = new $typeclass();
2150 $plugin->type = $type;
2151 $plugin->typerootdir = $typerootdir;
2152 $plugin->name = $modulename;
2153 $plugin->rootdir = null;
2154 $plugin->displayname = $modulename;
2155 $plugin->versiondb = $moduleinfo->version;
2156 $plugin->init_is_standard();
2158 $modules[$modulename] = $plugin;
2164 public function init_display_name() {
2165 if (get_string_manager()->string_exists('pluginname', $this->component)) {
2166 $this->displayname = get_string('pluginname', $this->component);
2168 $this->displayname = get_string('modulename', $this->component);
2173 * Load the data from version.php.
2174 * @return object the data object defined in version.php.
2176 protected function load_version_php() {
2177 $versionfile = $this->full_path('version.php');
2179 $module = new stdClass();
2180 if (is_readable($versionfile)) {
2181 include($versionfile);
2186 public function load_db_version() {
2189 $modulesinfo = self::get_modules_info();
2190 if (isset($modulesinfo[$this->name]->version)) {
2191 $this->versiondb = $modulesinfo[$this->name]->version;
2195 public function is_enabled() {
2197 $modulesinfo = self::get_modules_info();
2198 if (isset($modulesinfo[$this->name]->visible)) {
2199 if ($modulesinfo[$this->name]->visible) {
2205 return parent::is_enabled();
2209 public function get_settings_url() {
2211 if (file_exists($this->full_path('settings.php')) or file_exists($this->full_path('settingstree.php'))) {
2212 return new moodle_url('/admin/settings.php', array('section' => 'modsetting' . $this->name));
2214 return parent::get_settings_url();
2218 public function get_uninstall_url() {
2220 if ($this->name !== 'forum') {
2221 return new moodle_url('/admin/modules.php', array('delete' => $this->name, 'sesskey' => sesskey()));
2228 * Provides access to the records in {modules} table
2230 * @param bool $disablecache do not use internal static cache
2231 * @return array array of stdClasses
2233 protected static function get_modules_info($disablecache=false) {
2235 static $modulesinfocache = null;
2237 if (is_null($modulesinfocache) or $disablecache) {
2239 $modulesinfocache = $DB->get_records('modules', null, 'name', 'name,id,version,visible');
2240 } catch (dml_exception $e) {
2242 $modulesinfocache = array();
2246 return $modulesinfocache;
2252 * Class for question behaviours.
2254 class plugininfo_qbehaviour extends plugininfo_base {
2256 public function get_uninstall_url() {
2257 return new moodle_url('/admin/qbehaviours.php',
2258 array('delete' => $this->name, 'sesskey' => sesskey()));
2264 * Class for question types
2266 class plugininfo_qtype extends plugininfo_base {
2268 public function get_uninstall_url() {
2269 return new moodle_url('/admin/qtypes.php',
2270 array('delete' => $this->name, 'sesskey' => sesskey()));
2276 * Class for authentication plugins
2278 class plugininfo_auth extends plugininfo_base {
2280 public function is_enabled() {
2282 /** @var null|array list of enabled authentication plugins */
2283 static $enabled = null;
2285 if (in_array($this->name, array('nologin', 'manual'))) {
2286 // these two are always enabled and can't be disabled
2290 if (is_null($enabled)) {
2291 $enabled = array_flip(explode(',', $CFG->auth));
2294 return isset($enabled[$this->name]);
2297 public function get_settings_url() {
2298 if (file_exists($this->full_path('settings.php'))) {
2299 return new moodle_url('/admin/settings.php', array('section' => 'authsetting' . $this->name));
2301 return new moodle_url('/admin/auth_config.php', array('auth' => $this->name));
2308 * Class for enrolment plugins
2310 class plugininfo_enrol extends plugininfo_base {
2312 public function is_enabled() {
2314 /** @var null|array list of enabled enrolment plugins */
2315 static $enabled = null;
2317 // We do not actually need whole enrolment classes here so we do not call
2318 // {@link enrol_get_plugins()}. Note that this may produce slightly different
2319 // results, for example if the enrolment plugin does not contain lib.php
2320 // but it is listed in $CFG->enrol_plugins_enabled
2322 if (is_null($enabled)) {
2323 $enabled = array_flip(explode(',', $CFG->enrol_plugins_enabled));
2326 return isset($enabled[$this->name]);
2329 public function get_settings_url() {
2331 if ($this->is_enabled() or file_exists($this->full_path('settings.php'))) {
2332 return new moodle_url('/admin/settings.php', array('section' => 'enrolsettings' . $this->name));
2334 return parent::get_settings_url();
2338 public function get_uninstall_url() {
2339 return new moodle_url('/admin/enrol.php', array('action' => 'uninstall', 'enrol' => $this->name, 'sesskey' => sesskey()));
2345 * Class for messaging processors
2347 class plugininfo_message extends plugininfo_base {
2349 public function get_settings_url() {
2350 $processors = get_message_processors();
2351 if (isset($processors[$this->name])) {
2352 $processor = $processors[$this->name];
2353 if ($processor->available && $processor->hassettings) {
2354 return new moodle_url('settings.php', array('section' => 'messagesetting'.$processor->name));
2357 return parent::get_settings_url();
2361 * @see plugintype_interface::is_enabled()
2363 public function is_enabled() {
2364 $processors = get_message_processors();
2365 if (isset($processors[$this->name])) {
2366 return $processors[$this->name]->configured && $processors[$this->name]->enabled;
2368 return parent::is_enabled();
2373 * @see plugintype_interface::get_uninstall_url()
2375 public function get_uninstall_url() {
2376 $processors = get_message_processors();
2377 if (isset($processors[$this->name])) {
2378 return new moodle_url('message.php', array('uninstall' => $processors[$this->name]->id, 'sesskey' => sesskey()));
2380 return parent::get_uninstall_url();
2387 * Class for repositories
2389 class plugininfo_repository extends plugininfo_base {
2391 public function is_enabled() {
2393 $enabled = self::get_enabled_repositories();
2395 return isset($enabled[$this->name]);
2398 public function get_settings_url() {
2400 if ($this->is_enabled()) {
2401 return new moodle_url('/admin/repository.php', array('sesskey' => sesskey(), 'action' => 'edit', 'repos' => $this->name));
2403 return parent::get_settings_url();
2408 * Provides access to the records in {repository} table
2410 * @param bool $disablecache do not use internal static cache
2411 * @return array array of stdClasses
2413 protected static function get_enabled_repositories($disablecache=false) {
2415 static $repositories = null;
2417 if (is_null($repositories) or $disablecache) {
2418 $repositories = $DB->get_records('repository', null, 'type', 'type,visible,sortorder');
2421 return $repositories;
2427 * Class for portfolios
2429 class plugininfo_portfolio extends plugininfo_base {
2431 public function is_enabled() {
2433 $enabled = self::get_enabled_portfolios();
2435 return isset($enabled[$this->name]);
2439 * Provides access to the records in {portfolio_instance} table
2441 * @param bool $disablecache do not use internal static cache
2442 * @return array array of stdClasses
2444 protected static function get_enabled_portfolios($disablecache=false) {
2446 static $portfolios = null;
2448 if (is_null($portfolios) or $disablecache) {
2449 $portfolios = array();
2450 $instances = $DB->get_recordset('portfolio_instance', null, 'plugin');
2451 foreach ($instances as $instance) {
2452 if (isset($portfolios[$instance->plugin])) {
2453 if ($instance->visible) {
2454 $portfolios[$instance->plugin]->visible = $instance->visible;
2457 $portfolios[$instance->plugin] = $instance;
2470 class plugininfo_theme extends plugininfo_base {
2472 public function is_enabled() {
2475 if ((!empty($CFG->theme) and $CFG->theme === $this->name) or
2476 (!empty($CFG->themelegacy) and $CFG->themelegacy === $this->name)) {
2479 return parent::is_enabled();
2486 * Class representing an MNet service
2488 class plugininfo_mnetservice extends plugininfo_base {
2490 public function is_enabled() {
2493 if (empty($CFG->mnet_dispatcher_mode) || $CFG->mnet_dispatcher_mode !== 'strict') {
2496 return parent::is_enabled();
2503 * Class for admin tool plugins
2505 class plugininfo_tool extends plugininfo_base {
2507 public function get_uninstall_url() {
2508 return new moodle_url('/admin/tools.php', array('delete' => $this->name, 'sesskey' => sesskey()));
2514 * Class for admin tool plugins
2516 class plugininfo_report extends plugininfo_base {
2518 public function get_uninstall_url() {
2519 return new moodle_url('/admin/reports.php', array('delete' => $this->name, 'sesskey' => sesskey()));