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();
33 require_once($CFG->libdir
.'/filelib.php'); // curl class needed here
36 * Singleton class providing general plugins management functionality
38 class plugin_manager
{
40 /** the plugin is shipped with standard Moodle distribution */
41 const PLUGIN_SOURCE_STANDARD
= 'std';
42 /** the plugin is added extension */
43 const PLUGIN_SOURCE_EXTENSION
= 'ext';
45 /** the plugin uses neither database nor capabilities, no versions */
46 const PLUGIN_STATUS_NODB
= 'nodb';
47 /** the plugin is up-to-date */
48 const PLUGIN_STATUS_UPTODATE
= 'uptodate';
49 /** the plugin is about to be installed */
50 const PLUGIN_STATUS_NEW
= 'new';
51 /** the plugin is about to be upgraded */
52 const PLUGIN_STATUS_UPGRADE
= 'upgrade';
53 /** the standard plugin is about to be deleted */
54 const PLUGIN_STATUS_DELETE
= 'delete';
55 /** the version at the disk is lower than the one already installed */
56 const PLUGIN_STATUS_DOWNGRADE
= 'downgrade';
57 /** the plugin is installed but missing from disk */
58 const PLUGIN_STATUS_MISSING
= 'missing';
60 /** @var plugin_manager holds the singleton instance */
61 protected static $singletoninstance;
62 /** @var array of raw plugins information */
63 protected $pluginsinfo = null;
64 /** @var array of raw subplugins information */
65 protected $subpluginsinfo = null;
68 * Direct initiation not allowed, use the factory method {@link self::instance()}
70 protected function __construct() {
74 * Sorry, this is singleton
76 protected function __clone() {
80 * Factory method for this class
82 * @return plugin_manager the singleton instance
84 public static function instance() {
85 if (is_null(self
::$singletoninstance)) {
86 self
::$singletoninstance = new self();
88 return self
::$singletoninstance;
93 * @param bool $phpunitreset
95 public static function reset_caches($phpunitreset = false) {
97 self
::$singletoninstance = null;
102 * Returns a tree of known plugins and information about them
104 * @param bool $disablecache force reload, cache can be used otherwise
105 * @return array 2D array. The first keys are plugin type names (e.g. qtype);
106 * the second keys are the plugin local name (e.g. multichoice); and
107 * the values are the corresponding objects extending {@link plugininfo_base}
109 public function get_plugins($disablecache=false) {
112 if ($disablecache or is_null($this->pluginsinfo
)) {
113 $this->pluginsinfo
= array();
114 $plugintypes = get_plugin_types();
115 $plugintypes = $this->reorder_plugin_types($plugintypes);
116 foreach ($plugintypes as $plugintype => $plugintyperootdir) {
117 if (in_array($plugintype, array('base', 'general'))) {
118 throw new coding_exception('Illegal usage of reserved word for plugin type');
120 if (class_exists('plugininfo_' . $plugintype)) {
121 $plugintypeclass = 'plugininfo_' . $plugintype;
123 $plugintypeclass = 'plugininfo_general';
125 if (!in_array('plugininfo_base', class_parents($plugintypeclass))) {
126 throw new coding_exception('Class ' . $plugintypeclass . ' must extend plugininfo_base');
128 $plugins = call_user_func(array($plugintypeclass, 'get_plugins'), $plugintype, $plugintyperootdir, $plugintypeclass);
129 $this->pluginsinfo
[$plugintype] = $plugins;
132 if (empty($CFG->disableupdatenotifications
) and !during_initial_install()) {
133 // append the information about available updates provided by {@link available_update_checker()}
134 $provider = available_update_checker
::instance();
135 foreach ($this->pluginsinfo
as $plugintype => $plugins) {
136 foreach ($plugins as $plugininfoholder) {
137 $plugininfoholder->check_available_updates($provider);
143 return $this->pluginsinfo
;
147 * Returns list of plugins that define their subplugins and the information
148 * about them from the db/subplugins.php file.
150 * At the moment, only activity modules can define subplugins.
152 * @param bool $disablecache force reload, cache can be used otherwise
153 * @return array with keys like 'mod_quiz', and values the data from the
154 * corresponding db/subplugins.php file.
156 public function get_subplugins($disablecache=false) {
158 if ($disablecache or is_null($this->subpluginsinfo
)) {
159 $this->subpluginsinfo
= array();
160 $mods = get_plugin_list('mod');
161 foreach ($mods as $mod => $moddir) {
162 $modsubplugins = array();
163 if (file_exists($moddir . '/db/subplugins.php')) {
164 include($moddir . '/db/subplugins.php');
165 foreach ($subplugins as $subplugintype => $subplugintyperootdir) {
166 $subplugin = new stdClass();
167 $subplugin->type
= $subplugintype;
168 $subplugin->typerootdir
= $subplugintyperootdir;
169 $modsubplugins[$subplugintype] = $subplugin;
171 $this->subpluginsinfo
['mod_' . $mod] = $modsubplugins;
176 return $this->subpluginsinfo
;
180 * Returns the name of the plugin that defines the given subplugin type
182 * If the given subplugin type is not actually a subplugin, returns false.
184 * @param string $subplugintype the name of subplugin type, eg. workshopform or quiz
185 * @return false|string the name of the parent plugin, eg. mod_workshop
187 public function get_parent_of_subplugin($subplugintype) {
190 foreach ($this->get_subplugins() as $pluginname => $subplugintypes) {
191 if (isset($subplugintypes[$subplugintype])) {
192 $parent = $pluginname;
201 * Returns a localized name of a given plugin
203 * @param string $plugin name of the plugin, eg mod_workshop or auth_ldap
206 public function plugin_name($plugin) {
207 list($type, $name) = normalize_component($plugin);
208 return $this->pluginsinfo
[$type][$name]->displayname
;
212 * Returns a localized name of a plugin type in plural form
214 * Most plugin types define their names in core_plugin lang file. In case of subplugins,
215 * we try to ask the parent plugin for the name. In the worst case, we will return
216 * the value of the passed $type parameter.
218 * @param string $type the type of the plugin, e.g. mod or workshopform
221 public function plugintype_name_plural($type) {
223 if (get_string_manager()->string_exists('type_' . $type . '_plural', 'core_plugin')) {
224 // for most plugin types, their names are defined in core_plugin lang file
225 return get_string('type_' . $type . '_plural', 'core_plugin');
227 } else if ($parent = $this->get_parent_of_subplugin($type)) {
228 // if this is a subplugin, try to ask the parent plugin for the name
229 if (get_string_manager()->string_exists('subplugintype_' . $type . '_plural', $parent)) {
230 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type . '_plural', $parent);
232 return $this->plugin_name($parent) . ' / ' . $type;
241 * @param string $component frankenstyle component name.
242 * @return plugininfo_base|null the corresponding plugin information.
244 public function get_plugin_info($component) {
245 list($type, $name) = normalize_component($component);
246 $plugins = $this->get_plugins();
247 if (isset($plugins[$type][$name])) {
248 return $plugins[$type][$name];
255 * Get a list of any other plugins that require this one.
256 * @param string $component frankenstyle component name.
257 * @return array of frankensyle component names that require this one.
259 public function other_plugins_that_require($component) {
261 foreach ($this->get_plugins() as $type => $plugins) {
262 foreach ($plugins as $plugin) {
263 $required = $plugin->get_other_required_plugins();
264 if (isset($required[$component])) {
265 $others[] = $plugin->component
;
273 * Check a dependencies list against the list of installed plugins.
274 * @param array $dependencies compenent name to required version or ANY_VERSION.
275 * @return bool true if all the dependencies are satisfied.
277 public function are_dependencies_satisfied($dependencies) {
278 foreach ($dependencies as $component => $requiredversion) {
279 $otherplugin = $this->get_plugin_info($component);
280 if (is_null($otherplugin)) {
284 if ($requiredversion != ANY_VERSION
and $otherplugin->versiondisk
< $requiredversion) {
293 * Checks all dependencies for all installed plugins
295 * This is used by install and upgrade. The array passed by reference as the second
296 * argument is populated with the list of plugins that have failed dependencies (note that
297 * a single plugin can appear multiple times in the $failedplugins).
299 * @param int $moodleversion the version from version.php.
300 * @param array $failedplugins to return the list of plugins with non-satisfied dependencies
301 * @return bool true if all the dependencies are satisfied for all plugins.
303 public function all_plugins_ok($moodleversion, &$failedplugins = array()) {
306 foreach ($this->get_plugins() as $type => $plugins) {
307 foreach ($plugins as $plugin) {
309 if (!$plugin->is_core_dependency_satisfied($moodleversion)) {
311 $failedplugins[] = $plugin->component
;
314 if (!$this->are_dependencies_satisfied($plugin->get_other_required_plugins())) {
316 $failedplugins[] = $plugin->component
;
325 * Checks if there are some plugins with a known available update
327 * @return bool true if there is at least one available update
329 public function some_plugins_updatable() {
330 foreach ($this->get_plugins() as $type => $plugins) {
331 foreach ($plugins as $plugin) {
332 if ($plugin->available_updates()) {
342 * Defines a list of all plugins that were originally shipped in the standard Moodle distribution,
343 * but are not anymore and are deleted during upgrades.
345 * The main purpose of this list is to hide missing plugins during upgrade.
347 * @param string $type plugin type
348 * @param string $name plugin name
351 public static function is_deleted_standard_plugin($type, $name) {
352 static $plugins = array(
353 // do not add 1.9-2.2 plugin removals here
356 if (!isset($plugins[$type])) {
359 return in_array($name, $plugins[$type]);
363 * Defines a white list of all plugins shipped in the standard Moodle distribution
365 * @param string $type
366 * @return false|array array of standard plugins or false if the type is unknown
368 public static function standard_plugins_list($type) {
369 static $standard_plugins = array(
371 'assignment' => array(
372 'offline', 'online', 'upload', 'uploadsingle'
375 'assignsubmission' => array(
376 'comments', 'file', 'onlinetext'
379 'assignfeedback' => array(
384 'cas', 'db', 'email', 'fc', 'imap', 'ldap', 'manual', 'mnet',
385 'nntp', 'nologin', 'none', 'pam', 'pop3', 'radius',
386 'shibboleth', 'webservice'
390 'activity_modules', 'admin_bookmarks', 'blog_menu',
391 'blog_recent', 'blog_tags', 'calendar_month',
392 'calendar_upcoming', 'comments', 'community',
393 'completionstatus', 'course_list', 'course_overview',
394 'course_summary', 'feedback', 'glossary_random', 'html',
395 'login', 'mentees', 'messages', 'mnet_hosts', 'myprofile',
396 'navigation', 'news_items', 'online_users', 'participants',
397 'private_files', 'quiz_results', 'recent_activity',
398 'rss_client', 'search_forums', 'section_links',
399 'selfcompletion', 'settings', 'site_main_menu',
400 'social_activities', 'tag_flickr', 'tag_youtube', 'tags'
404 'exportimscp', 'importhtml', 'print'
407 'coursereport' => array(
411 'datafield' => array(
412 'checkbox', 'date', 'file', 'latlong', 'menu', 'multimenu',
413 'number', 'picture', 'radiobutton', 'text', 'textarea', 'url'
416 'datapreset' => array(
421 'textarea', 'tinymce'
425 'authorize', 'category', 'cohort', 'database', 'flatfile',
426 'guest', 'imsenterprise', 'ldap', 'manual', 'meta', 'mnet',
431 'activitynames', 'algebra', 'censor', 'emailprotect',
432 'emoticon', 'mediaplugin', 'multilang', 'tex', 'tidy',
433 'urltolink', 'data', 'glossary'
437 'scorm', 'social', 'topics', 'weeks'
440 'gradeexport' => array(
441 'ods', 'txt', 'xls', 'xml'
444 'gradeimport' => array(
448 'gradereport' => array(
449 'grader', 'outcomes', 'overview', 'user'
452 'gradingform' => array(
460 'email', 'jabber', 'popup'
463 'mnetservice' => array(
468 'assign', 'assignment', 'book', 'chat', 'choice', 'data', 'feedback', 'folder',
469 'forum', 'glossary', 'imscp', 'label', 'lesson', 'lti', 'page',
470 'quiz', 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop'
473 'plagiarism' => array(
476 'portfolio' => array(
477 'boxnet', 'download', 'flickr', 'googledocs', 'mahara', 'picasa'
480 'profilefield' => array(
481 'checkbox', 'datetime', 'menu', 'text', 'textarea'
484 'qbehaviour' => array(
485 'adaptive', 'adaptivenopenalty', 'deferredcbm',
486 'deferredfeedback', 'immediatecbm', 'immediatefeedback',
487 'informationitem', 'interactive', 'interactivecountback',
488 'manualgraded', 'missing'
492 'aiken', 'blackboard', 'blackboard_six', 'examview', 'gift',
493 'learnwise', 'missingword', 'multianswer', 'webct',
498 'calculated', 'calculatedmulti', 'calculatedsimple',
499 'description', 'essay', 'match', 'missingtype', 'multianswer',
500 'multichoice', 'numerical', 'random', 'randomsamatch',
501 'shortanswer', 'truefalse'
505 'grading', 'overview', 'responses', 'statistics'
508 'quizaccess' => array(
509 'delaybetweenattempts', 'ipaddress', 'numattempts', 'openclosedate',
510 'password', 'safebrowser', 'securewindow', 'timelimit'
514 'backups', 'completion', 'configlog', 'courseoverview',
515 'log', 'loglive', 'outline', 'participation', 'progress', 'questioninstances', 'security', 'stats'
518 'repository' => array(
519 'alfresco', 'boxnet', 'coursefiles', 'dropbox', 'equella', 'filesystem',
520 'flickr', 'flickr_public', 'googledocs', 'local', 'merlot',
521 'picasa', 'recent', 's3', 'upload', 'url', 'user', 'webdav',
522 'wikimedia', 'youtube'
525 'scormreport' => array(
532 'afterburner', 'anomaly', 'arialist', 'base', 'binarius',
533 'boxxie', 'brick', 'canvas', 'formal_white', 'formfactor',
534 'fusion', 'leatherbound', 'magazine', 'mymobile', 'nimble',
535 'nonzero', 'overlay', 'serenity', 'sky_high', 'splash',
536 'standard', 'standardold'
540 'assignmentupgrade', 'bloglevelupgrade', 'capability', 'customlang', 'dbtransfer', 'generator',
541 'health', 'innodb', 'langimport', 'multilangupgrade', 'phpunit', 'profiling',
542 'qeupgradehelper', 'replace', 'spamcleaner', 'timezoneimport', 'unittest',
543 'uploaduser', 'unsuproles', 'xmldb'
546 'webservice' => array(
547 'amf', 'rest', 'soap', 'xmlrpc'
550 'workshopallocation' => array(
551 'manual', 'random', 'scheduled'
554 'workshopeval' => array(
558 'workshopform' => array(
559 'accumulative', 'comments', 'numerrors', 'rubric'
563 if (isset($standard_plugins[$type])) {
564 return $standard_plugins[$type];
571 * Reorders plugin types into a sequence to be displayed
573 * For technical reasons, plugin types returned by {@link get_plugin_types()} are
574 * in a certain order that does not need to fit the expected order for the display.
575 * Particularly, activity modules should be displayed first as they represent the
576 * real heart of Moodle. They should be followed by other plugin types that are
577 * used to build the courses (as that is what one expects from LMS). After that,
578 * other supportive plugin types follow.
580 * @param array $types associative array
581 * @return array same array with altered order of items
583 protected function reorder_plugin_types(array $types) {
585 'mod' => $types['mod'],
586 'block' => $types['block'],
587 'qtype' => $types['qtype'],
588 'qbehaviour' => $types['qbehaviour'],
589 'qformat' => $types['qformat'],
590 'filter' => $types['filter'],
591 'enrol' => $types['enrol'],
593 foreach ($types as $type => $path) {
594 if (!isset($fix[$type])) {
604 * General exception thrown by the {@link available_update_checker} class
606 class available_update_checker_exception
extends moodle_exception
{
609 * @param string $errorcode exception description identifier
610 * @param mixed $debuginfo debugging data to display
612 public function __construct($errorcode, $debuginfo=null) {
613 parent
::__construct($errorcode, 'core_plugin', '', null, print_r($debuginfo, true));
619 * Singleton class that handles checking for available updates
621 class available_update_checker
{
623 /** @var available_update_checker holds the singleton instance */
624 protected static $singletoninstance;
625 /** @var null|int the timestamp of when the most recent response was fetched */
626 protected $recentfetch = null;
627 /** @var null|array the recent response from the update notification provider */
628 protected $recentresponse = null;
629 /** @var null|string the numerical version of the local Moodle code */
630 protected $currentversion = null;
631 /** @var null|string the release info of the local Moodle code */
632 protected $currentrelease = null;
633 /** @var null|string branch of the local Moodle code */
634 protected $currentbranch = null;
635 /** @var array of (string)frankestyle => (string)version list of additional plugins deployed at this site */
636 protected $currentplugins = array();
639 * Direct initiation not allowed, use the factory method {@link self::instance()}
641 protected function __construct() {
645 * Sorry, this is singleton
647 protected function __clone() {
651 * Factory method for this class
653 * @return available_update_checker the singleton instance
655 public static function instance() {
656 if (is_null(self
::$singletoninstance)) {
657 self
::$singletoninstance = new self();
659 return self
::$singletoninstance;
664 * @param bool $phpunitreset
666 public static function reset_caches($phpunitreset = false) {
668 self
::$singletoninstance = null;
673 * Returns the timestamp of the last execution of {@link fetch()}
675 * @return int|null null if it has never been executed or we don't known
677 public function get_last_timefetched() {
679 $this->restore_response();
681 if (!empty($this->recentfetch
)) {
682 return $this->recentfetch
;
690 * Fetches the available update status from the remote site
692 * @throws available_update_checker_exception
694 public function fetch() {
695 $response = $this->get_response();
696 $this->validate_response($response);
697 $this->store_response($response);
701 * Returns the available update information for the given component
703 * This method returns null if the most recent response does not contain any information
704 * about it. The returned structure is an array of available updates for the given
705 * component. Each update info is an object with at least one property called
706 * 'version'. Other possible properties are 'release', 'maturity', 'url' and 'downloadurl'.
708 * For the 'core' component, the method returns real updates only (those with higher version).
709 * For all other components, the list of all known remote updates is returned and the caller
710 * (usually the {@link plugin_manager}) is supposed to make the actual comparison of versions.
712 * @param string $component frankenstyle
713 * @param array $options with supported keys 'minmaturity' and/or 'notifybuilds'
714 * @return null|array null or array of available_update_info objects
716 public function get_update_info($component, array $options = array()) {
718 if (!isset($options['minmaturity'])) {
719 $options['minmaturity'] = 0;
722 if (!isset($options['notifybuilds'])) {
723 $options['notifybuilds'] = false;
726 if ($component == 'core') {
727 $this->load_current_environment();
730 $this->restore_response();
732 if (empty($this->recentresponse
['updates'][$component])) {
737 foreach ($this->recentresponse
['updates'][$component] as $info) {
738 $update = new available_update_info($component, $info);
739 if (isset($update->maturity
) and ($update->maturity
< $options['minmaturity'])) {
742 if ($component == 'core') {
743 if ($update->version
<= $this->currentversion
) {
746 if (empty($options['notifybuilds']) and $this->is_same_release($update->release
)) {
750 $updates[] = $update;
753 if (empty($updates)) {
761 * The method being run via cron.php
763 public function cron() {
766 if (!$this->cron_autocheck_enabled()) {
767 $this->cron_mtrace('Automatic check for available updates not enabled, skipping.');
771 $now = $this->cron_current_timestamp();
773 if ($this->cron_has_fresh_fetch($now)) {
774 $this->cron_mtrace('Recently fetched info about available updates is still fresh enough, skipping.');
778 if ($this->cron_has_outdated_fetch($now)) {
779 $this->cron_mtrace('Outdated or missing info about available updates, forced fetching ... ', '');
780 $this->cron_execute();
784 $offset = $this->cron_execution_offset();
785 $start = mktime(1, 0, 0, date('n', $now), date('j', $now), date('Y', $now)); // 01:00 AM today local time
786 if ($now > $start +
$offset) {
787 $this->cron_mtrace('Regular daily check for available updates ... ', '');
788 $this->cron_execute();
793 /// end of public API //////////////////////////////////////////////////////
796 * Makes cURL request to get data from the remote site
798 * @return string raw request result
799 * @throws available_update_checker_exception
801 protected function get_response() {
802 $curl = new curl(array('proxy' => true));
803 $response = $curl->post($this->prepare_request_url(), $this->prepare_request_params());
804 $curlinfo = $curl->get_info();
805 if ($curlinfo['http_code'] != 200) {
806 throw new available_update_checker_exception('err_response_http_code', $curlinfo['http_code']);
812 * Makes sure the response is valid, has correct API format etc.
814 * @param string $response raw response as returned by the {@link self::get_response()}
815 * @throws available_update_checker_exception
817 protected function validate_response($response) {
819 $response = $this->decode_response($response);
821 if (empty($response)) {
822 throw new available_update_checker_exception('err_response_empty');
825 if (empty($response['status']) or $response['status'] !== 'OK') {
826 throw new available_update_checker_exception('err_response_status', $response['status']);
829 if (empty($response['apiver']) or $response['apiver'] !== '1.0') {
830 throw new available_update_checker_exception('err_response_format_version', $response['apiver']);
833 if (empty($response['forbranch']) or $response['forbranch'] !== moodle_major_version(true)) {
834 throw new available_update_checker_exception('err_response_target_version', $response['forbranch']);
839 * Decodes the raw string response from the update notifications provider
841 * @param string $response as returned by {@link self::get_response()}
842 * @return array decoded response structure
844 protected function decode_response($response) {
845 return json_decode($response, true);
849 * Stores the valid fetched response for later usage
851 * This implementation uses the config_plugins table as the permanent storage.
853 * @param string $response raw valid data returned by {@link self::get_response()}
855 protected function store_response($response) {
857 set_config('recentfetch', time(), 'core_plugin');
858 set_config('recentresponse', $response, 'core_plugin');
860 $this->restore_response(true);
864 * Loads the most recent raw response record we have fetched
866 * After this method is called, $this->recentresponse is set to an array. If the
867 * array is empty, then either no data have been fetched yet or the fetched data
868 * do not have expected format (and thence they are ignored and a debugging
869 * message is displayed).
871 * This implementation uses the config_plugins table as the permanent storage.
873 * @param bool $forcereload reload even if it was already loaded
875 protected function restore_response($forcereload = false) {
877 if (!$forcereload and !is_null($this->recentresponse
)) {
878 // we already have it, nothing to do
882 $config = get_config('core_plugin');
884 if (!empty($config->recentresponse
) and !empty($config->recentfetch
)) {
886 $this->validate_response($config->recentresponse
);
887 $this->recentfetch
= $config->recentfetch
;
888 $this->recentresponse
= $this->decode_response($config->recentresponse
);
889 } catch (available_update_checker_exception
$e) {
890 debugging('Invalid info about available updates detected and will be ignored: '.$e->getMessage(), DEBUG_ALL
);
891 $this->recentresponse
= array();
895 $this->recentresponse
= array();
900 * Compares two raw {@link $recentresponse} records and returns the list of changed updates
902 * This method is used to populate potential update info to be sent to site admins.
906 * @throws available_update_checker_exception
907 * @return array parts of $new['updates'] that have changed
909 protected function compare_responses(array $old, array $new) {
915 if (!array_key_exists('updates', $new)) {
916 throw new available_update_checker_exception('err_response_format');
920 return $new['updates'];
923 if (!array_key_exists('updates', $old)) {
924 throw new available_update_checker_exception('err_response_format');
929 foreach ($new['updates'] as $newcomponent => $newcomponentupdates) {
930 if (empty($old['updates'][$newcomponent])) {
931 $changes[$newcomponent] = $newcomponentupdates;
934 foreach ($newcomponentupdates as $newcomponentupdate) {
936 foreach ($old['updates'][$newcomponent] as $oldcomponentupdate) {
937 if ($newcomponentupdate['version'] == $oldcomponentupdate['version']) {
942 if (!isset($changes[$newcomponent])) {
943 $changes[$newcomponent] = array();
945 $changes[$newcomponent][] = $newcomponentupdate;
954 * Returns the URL to send update requests to
956 * During the development or testing, you can set $CFG->alternativeupdateproviderurl
957 * to a custom URL that will be used. Otherwise the standard URL will be returned.
961 protected function prepare_request_url() {
964 if (!empty($CFG->alternativeupdateproviderurl
)) {
965 return $CFG->alternativeupdateproviderurl
;
967 return 'http://download.moodle.org/api/1.0/updates.php';
972 * Sets the properties currentversion, currentrelease, currentbranch and currentplugins
974 * @param bool $forcereload
976 protected function load_current_environment($forcereload=false) {
979 if (!is_null($this->currentversion
) and !$forcereload) {
984 require($CFG->dirroot
.'/version.php');
985 $this->currentversion
= $version;
986 $this->currentrelease
= $release;
987 $this->currentbranch
= moodle_major_version(true);
989 $pluginman = plugin_manager
::instance();
990 foreach ($pluginman->get_plugins() as $type => $plugins) {
991 foreach ($plugins as $plugin) {
992 if (!$plugin->is_standard()) {
993 $this->currentplugins
[$plugin->component
] = $plugin->versiondisk
;
1000 * Returns the list of HTTP params to be sent to the updates provider URL
1002 * @return array of (string)param => (string)value
1004 protected function prepare_request_params() {
1007 $this->load_current_environment();
1008 $this->restore_response();
1011 $params['format'] = 'json';
1013 if (isset($this->recentresponse
['ticket'])) {
1014 $params['ticket'] = $this->recentresponse
['ticket'];
1017 if (isset($this->currentversion
)) {
1018 $params['version'] = $this->currentversion
;
1020 throw new coding_exception('Main Moodle version must be already known here');
1023 if (isset($this->currentbranch
)) {
1024 $params['branch'] = $this->currentbranch
;
1026 throw new coding_exception('Moodle release must be already known here');
1030 foreach ($this->currentplugins
as $plugin => $version) {
1031 $plugins[] = $plugin.'@'.$version;
1033 if (!empty($plugins)) {
1034 $params['plugins'] = implode(',', $plugins);
1041 * Returns the current timestamp
1043 * @return int the timestamp
1045 protected function cron_current_timestamp() {
1050 * Output cron debugging info
1053 * @param string $msg output message
1054 * @param string $eol end of line
1056 protected function cron_mtrace($msg, $eol = PHP_EOL
) {
1061 * Decide if the autocheck feature is disabled in the server setting
1063 * @return bool true if autocheck enabled, false if disabled
1065 protected function cron_autocheck_enabled() {
1068 if (empty($CFG->updateautocheck
)) {
1076 * Decide if the recently fetched data are still fresh enough
1078 * @param int $now current timestamp
1079 * @return bool true if no need to re-fetch, false otherwise
1081 protected function cron_has_fresh_fetch($now) {
1082 $recent = $this->get_last_timefetched();
1084 if (empty($recent)) {
1088 if ($now < $recent) {
1089 $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1093 if ($now - $recent > 24 * HOURSECS
) {
1101 * Decide if the fetch is outadated or even missing
1103 * @param int $now current timestamp
1104 * @return bool false if no need to re-fetch, true otherwise
1106 protected function cron_has_outdated_fetch($now) {
1107 $recent = $this->get_last_timefetched();
1109 if (empty($recent)) {
1113 if ($now < $recent) {
1114 $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1118 if ($now - $recent > 48 * HOURSECS
) {
1126 * Returns the cron execution offset for this site
1128 * The main {@link self::cron()} is supposed to run every night in some random time
1129 * between 01:00 and 06:00 AM (local time). The exact moment is defined by so called
1130 * execution offset, that is the amount of time after 01:00 AM. The offset value is
1131 * initially generated randomly and then used consistently at the site. This way, the
1132 * regular checks against the download.moodle.org server are spread in time.
1134 * @return int the offset number of seconds from range 1 sec to 5 hours
1136 protected function cron_execution_offset() {
1139 if (empty($CFG->updatecronoffset
)) {
1140 set_config('updatecronoffset', rand(1, 5 * HOURSECS
));
1143 return $CFG->updatecronoffset
;
1147 * Fetch available updates info and eventually send notification to site admins
1149 protected function cron_execute() {
1152 $this->restore_response();
1153 $previous = $this->recentresponse
;
1155 $this->restore_response(true);
1156 $current = $this->recentresponse
;
1157 $changes = $this->compare_responses($previous, $current);
1158 $notifications = $this->cron_notifications($changes);
1159 $this->cron_notify($notifications);
1160 $this->cron_mtrace('done');
1161 } catch (available_update_checker_exception
$e) {
1162 $this->cron_mtrace('FAILED!');
1167 * Given the list of changes in available updates, pick those to send to site admins
1169 * @param array $changes as returned by {@link self::compare_responses()}
1170 * @return array of available_update_info objects to send to site admins
1172 protected function cron_notifications(array $changes) {
1175 $notifications = array();
1176 $pluginman = plugin_manager
::instance();
1177 $plugins = $pluginman->get_plugins(true);
1179 foreach ($changes as $component => $componentchanges) {
1180 if (empty($componentchanges)) {
1183 $componentupdates = $this->get_update_info($component,
1184 array('minmaturity' => $CFG->updateminmaturity
, 'notifybuilds' => $CFG->updatenotifybuilds
));
1185 if (empty($componentupdates)) {
1188 // notify only about those $componentchanges that are present in $componentupdates
1189 // to respect the preferences
1190 foreach ($componentchanges as $componentchange) {
1191 foreach ($componentupdates as $componentupdate) {
1192 if ($componentupdate->version
== $componentchange['version']) {
1193 if ($component == 'core') {
1194 // In case of 'core', we already know that the $componentupdate
1195 // is a real update with higher version ({@see self::get_update_info()}).
1196 // We just perform additional check for the release property as there
1197 // can be two Moodle releases having the same version (e.g. 2.4.0 and 2.5dev shortly
1198 // after the release). We can do that because we have the release info
1199 // always available for the core.
1200 if ((string)$componentupdate->release
=== (string)$componentchange['release']) {
1201 $notifications[] = $componentupdate;
1204 // Use the plugin_manager to check if the detected $componentchange
1205 // is a real update with higher version. That is, the $componentchange
1206 // is present in the array of {@link available_update_info} objects
1207 // returned by the plugin's available_updates() method.
1208 list($plugintype, $pluginname) = normalize_component($component);
1209 if (!empty($plugins[$plugintype][$pluginname])) {
1210 $availableupdates = $plugins[$plugintype][$pluginname]->available_updates();
1211 if (!empty($availableupdates)) {
1212 foreach ($availableupdates as $availableupdate) {
1213 if ($availableupdate->version
== $componentchange['version']) {
1214 $notifications[] = $componentupdate;
1225 return $notifications;
1229 * Sends the given notifications to site admins via messaging API
1231 * @param array $notifications array of available_update_info objects to send
1233 protected function cron_notify(array $notifications) {
1236 if (empty($notifications)) {
1240 $admins = get_admins();
1242 if (empty($admins)) {
1246 $this->cron_mtrace('sending notifications ... ', '');
1248 $text = get_string('updatenotifications', 'core_admin') . PHP_EOL
;
1249 $html = html_writer
::tag('h1', get_string('updatenotifications', 'core_admin')) . PHP_EOL
;
1251 $coreupdates = array();
1252 $pluginupdates = array();
1254 foreach ($notifications as $notification) {
1255 if ($notification->component
== 'core') {
1256 $coreupdates[] = $notification;
1258 $pluginupdates[] = $notification;
1262 if (!empty($coreupdates)) {
1263 $text .= PHP_EOL
. get_string('updateavailable', 'core_admin') . PHP_EOL
;
1264 $html .= html_writer
::tag('h2', get_string('updateavailable', 'core_admin')) . PHP_EOL
;
1265 $html .= html_writer
::start_tag('ul') . PHP_EOL
;
1266 foreach ($coreupdates as $coreupdate) {
1267 $html .= html_writer
::start_tag('li');
1268 if (isset($coreupdate->release
)) {
1269 $text .= get_string('updateavailable_release', 'core_admin', $coreupdate->release
);
1270 $html .= html_writer
::tag('strong', get_string('updateavailable_release', 'core_admin', $coreupdate->release
));
1272 if (isset($coreupdate->version
)) {
1273 $text .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version
);
1274 $html .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version
);
1276 if (isset($coreupdate->maturity
)) {
1277 $text .= ' ('.get_string('maturity'.$coreupdate->maturity
, 'core_admin').')';
1278 $html .= ' ('.get_string('maturity'.$coreupdate->maturity
, 'core_admin').')';
1281 $html .= html_writer
::end_tag('li') . PHP_EOL
;
1284 $html .= html_writer
::end_tag('ul') . PHP_EOL
;
1286 $a = array('url' => $CFG->wwwroot
.'/'.$CFG->admin
.'/index.php');
1287 $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL
;
1288 $a = array('url' => html_writer
::link($CFG->wwwroot
.'/'.$CFG->admin
.'/index.php', $CFG->wwwroot
.'/'.$CFG->admin
.'/index.php'));
1289 $html .= html_writer
::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL
;
1292 if (!empty($pluginupdates)) {
1293 $text .= PHP_EOL
. get_string('updateavailableforplugin', 'core_admin') . PHP_EOL
;
1294 $html .= html_writer
::tag('h2', get_string('updateavailableforplugin', 'core_admin')) . PHP_EOL
;
1296 $html .= html_writer
::start_tag('ul') . PHP_EOL
;
1297 foreach ($pluginupdates as $pluginupdate) {
1298 $html .= html_writer
::start_tag('li');
1299 $text .= get_string('pluginname', $pluginupdate->component
);
1300 $html .= html_writer
::tag('strong', get_string('pluginname', $pluginupdate->component
));
1302 $text .= ' ('.$pluginupdate->component
.')';
1303 $html .= ' ('.$pluginupdate->component
.')';
1305 $text .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version
);
1306 $html .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version
);
1309 $html .= html_writer
::end_tag('li') . PHP_EOL
;
1312 $html .= html_writer
::end_tag('ul') . PHP_EOL
;
1314 $a = array('url' => $CFG->wwwroot
.'/'.$CFG->admin
.'/plugins.php');
1315 $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL
;
1316 $a = array('url' => html_writer
::link($CFG->wwwroot
.'/'.$CFG->admin
.'/plugins.php', $CFG->wwwroot
.'/'.$CFG->admin
.'/plugins.php'));
1317 $html .= html_writer
::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL
;
1320 $a = array('siteurl' => $CFG->wwwroot
);
1321 $text .= get_string('updatenotificationfooter', 'core_admin', $a) . PHP_EOL
;
1322 $a = array('siteurl' => html_writer
::link($CFG->wwwroot
, $CFG->wwwroot
));
1323 $html .= html_writer
::tag('footer', html_writer
::tag('p', get_string('updatenotificationfooter', 'core_admin', $a),
1324 array('style' => 'font-size:smaller; color:#333;')));
1326 $mainadmin = reset($admins);
1328 foreach ($admins as $admin) {
1329 $message = new stdClass();
1330 $message->component
= 'moodle';
1331 $message->name
= 'availableupdate';
1332 $message->userfrom
= $mainadmin;
1333 $message->userto
= $admin;
1334 $message->subject
= get_string('updatenotificationsubject', 'core_admin', array('siteurl' => $CFG->wwwroot
));
1335 $message->fullmessage
= $text;
1336 $message->fullmessageformat
= FORMAT_PLAIN
;
1337 $message->fullmessagehtml
= $html;
1338 $message->smallmessage
= get_string('updatenotifications', 'core_admin');
1339 $message->notification
= 1;
1340 message_send($message);
1345 * Compare two release labels and decide if they are the same
1347 * @param string $remote release info of the available update
1348 * @param null|string $local release info of the local code, defaults to $release defined in version.php
1349 * @return boolean true if the releases declare the same minor+major version
1351 protected function is_same_release($remote, $local=null) {
1353 if (is_null($local)) {
1354 $this->load_current_environment();
1355 $local = $this->currentrelease
;
1358 $pattern = '/^([0-9\.\+]+)([^(]*)/';
1360 preg_match($pattern, $remote, $remotematches);
1361 preg_match($pattern, $local, $localmatches);
1363 $remotematches[1] = str_replace('+', '', $remotematches[1]);
1364 $localmatches[1] = str_replace('+', '', $localmatches[1]);
1366 if ($remotematches[1] === $localmatches[1] and rtrim($remotematches[2]) === rtrim($localmatches[2])) {
1376 * Defines the structure of objects returned by {@link available_update_checker::get_update_info()}
1378 class available_update_info
{
1380 /** @var string frankenstyle component name */
1382 /** @var int the available version of the component */
1384 /** @var string|null optional release name */
1385 public $release = null;
1386 /** @var int|null optional maturity info, eg {@link MATURITY_STABLE} */
1387 public $maturity = null;
1388 /** @var string|null optional URL of a page with more info about the update */
1390 /** @var string|null optional URL of a ZIP package that can be downloaded and installed */
1391 public $download = null;
1394 * Creates new instance of the class
1396 * The $info array must provide at least the 'version' value and optionally all other
1397 * values to populate the object's properties.
1399 * @param string $name the frankenstyle component name
1400 * @param array $info associative array with other properties
1402 public function __construct($name, array $info) {
1403 $this->component
= $name;
1404 foreach ($info as $k => $v) {
1405 if (property_exists('available_update_info', $k) and $k != 'component') {
1414 * Factory class producing required subclasses of {@link plugininfo_base}
1416 class plugininfo_default_factory
{
1419 * Makes a new instance of the plugininfo class
1421 * @param string $type the plugin type, eg. 'mod'
1422 * @param string $typerootdir full path to the location of all the plugins of this type
1423 * @param string $name the plugin name, eg. 'workshop'
1424 * @param string $namerootdir full path to the location of the plugin
1425 * @param string $typeclass the name of class that holds the info about the plugin
1426 * @return plugininfo_base the instance of $typeclass
1428 public static function make($type, $typerootdir, $name, $namerootdir, $typeclass) {
1429 $plugin = new $typeclass();
1430 $plugin->type
= $type;
1431 $plugin->typerootdir
= $typerootdir;
1432 $plugin->name
= $name;
1433 $plugin->rootdir
= $namerootdir;
1435 $plugin->init_display_name();
1436 $plugin->load_disk_version();
1437 $plugin->load_db_version();
1438 $plugin->load_required_main_version();
1439 $plugin->init_is_standard();
1447 * Base class providing access to the information about a plugin
1449 * @property-read string component the component name, type_name
1451 abstract class plugininfo_base
{
1453 /** @var string the plugintype name, eg. mod, auth or workshopform */
1455 /** @var string full path to the location of all the plugins of this type */
1456 public $typerootdir;
1457 /** @var string the plugin name, eg. assignment, ldap */
1459 /** @var string the localized plugin name */
1460 public $displayname;
1461 /** @var string the plugin source, one of plugin_manager::PLUGIN_SOURCE_xxx constants */
1463 /** @var fullpath to the location of this plugin */
1465 /** @var int|string the version of the plugin's source code */
1466 public $versiondisk;
1467 /** @var int|string the version of the installed plugin */
1469 /** @var int|float|string required version of Moodle core */
1470 public $versionrequires;
1471 /** @var array other plugins that this one depends on, lazy-loaded by {@link get_other_required_plugins()} */
1472 public $dependencies;
1473 /** @var int number of instances of the plugin - not supported yet */
1475 /** @var int order of the plugin among other plugins of the same type - not supported yet */
1477 /** @var array|null array of {@link available_update_info} for this plugin */
1478 public $availableupdates;
1481 * Gathers and returns the information about all plugins of the given type
1483 * @param string $type the name of the plugintype, eg. mod, auth or workshopform
1484 * @param string $typerootdir full path to the location of the plugin dir
1485 * @param string $typeclass the name of the actually called class
1486 * @return array of plugintype classes, indexed by the plugin name
1488 public static function get_plugins($type, $typerootdir, $typeclass) {
1490 // get the information about plugins at the disk
1491 $plugins = get_plugin_list($type);
1493 foreach ($plugins as $pluginname => $pluginrootdir) {
1494 $ondisk[$pluginname] = plugininfo_default_factory
::make($type, $typerootdir,
1495 $pluginname, $pluginrootdir, $typeclass);
1501 * Sets {@link $displayname} property to a localized name of the plugin
1503 public function init_display_name() {
1504 if (!get_string_manager()->string_exists('pluginname', $this->component
)) {
1505 $this->displayname
= '[pluginname,' . $this->component
. ']';
1507 $this->displayname
= get_string('pluginname', $this->component
);
1512 * Magic method getter, redirects to read only values.
1514 * @param string $name
1517 public function __get($name) {
1519 case 'component': return $this->type
. '_' . $this->name
;
1522 debugging('Invalid plugin property accessed! '.$name);
1528 * Return the full path name of a file within the plugin.
1530 * No check is made to see if the file exists.
1532 * @param string $relativepath e.g. 'version.php'.
1533 * @return string e.g. $CFG->dirroot . '/mod/quiz/version.php'.
1535 public function full_path($relativepath) {
1536 if (empty($this->rootdir
)) {
1539 return $this->rootdir
. '/' . $relativepath;
1543 * Load the data from version.php.
1545 * @return stdClass the object called $plugin defined in version.php
1547 protected function load_version_php() {
1548 $versionfile = $this->full_path('version.php');
1550 $plugin = new stdClass();
1551 if (is_readable($versionfile)) {
1552 include($versionfile);
1558 * Sets {@link $versiondisk} property to a numerical value representing the
1559 * version of the plugin's source code.
1561 * If the value is null after calling this method, either the plugin
1562 * does not use versioning (typically does not have any database
1563 * data) or is missing from disk.
1565 public function load_disk_version() {
1566 $plugin = $this->load_version_php();
1567 if (isset($plugin->version
)) {
1568 $this->versiondisk
= $plugin->version
;
1573 * Sets {@link $versionrequires} property to a numerical value representing
1574 * the version of Moodle core that this plugin requires.
1576 public function load_required_main_version() {
1577 $plugin = $this->load_version_php();
1578 if (isset($plugin->requires
)) {
1579 $this->versionrequires
= $plugin->requires
;
1584 * Initialise {@link $dependencies} to the list of other plugins (in any)
1585 * that this one requires to be installed.
1587 protected function load_other_required_plugins() {
1588 $plugin = $this->load_version_php();
1589 if (!empty($plugin->dependencies
)) {
1590 $this->dependencies
= $plugin->dependencies
;
1592 $this->dependencies
= array(); // By default, no dependencies.
1597 * Get the list of other plugins that this plugin requires to be installed.
1599 * @return array with keys the frankenstyle plugin name, and values either
1600 * a version string (like '2011101700') or the constant ANY_VERSION.
1602 public function get_other_required_plugins() {
1603 if (is_null($this->dependencies
)) {
1604 $this->load_other_required_plugins();
1606 return $this->dependencies
;
1610 * Sets {@link $versiondb} property to a numerical value representing the
1611 * currently installed version of the plugin.
1613 * If the value is null after calling this method, either the plugin
1614 * does not use versioning (typically does not have any database
1615 * data) or has not been installed yet.
1617 public function load_db_version() {
1618 if ($ver = self
::get_version_from_config_plugins($this->component
)) {
1619 $this->versiondb
= $ver;
1624 * Sets {@link $source} property to one of plugin_manager::PLUGIN_SOURCE_xxx
1627 * If the property's value is null after calling this method, then
1628 * the type of the plugin has not been recognized and you should throw
1631 public function init_is_standard() {
1633 $standard = plugin_manager
::standard_plugins_list($this->type
);
1635 if ($standard !== false) {
1636 $standard = array_flip($standard);
1637 if (isset($standard[$this->name
])) {
1638 $this->source
= plugin_manager
::PLUGIN_SOURCE_STANDARD
;
1639 } else if (!is_null($this->versiondb
) and is_null($this->versiondisk
)
1640 and plugin_manager
::is_deleted_standard_plugin($this->type
, $this->name
)) {
1641 $this->source
= plugin_manager
::PLUGIN_SOURCE_STANDARD
; // to be deleted
1643 $this->source
= plugin_manager
::PLUGIN_SOURCE_EXTENSION
;
1649 * Returns true if the plugin is shipped with the official distribution
1650 * of the current Moodle version, false otherwise.
1654 public function is_standard() {
1655 return $this->source
=== plugin_manager
::PLUGIN_SOURCE_STANDARD
;
1659 * Returns true if the the given Moodle version is enough to run this plugin
1661 * @param string|int|double $moodleversion
1664 public function is_core_dependency_satisfied($moodleversion) {
1666 if (empty($this->versionrequires
)) {
1670 return (double)$this->versionrequires
<= (double)$moodleversion;
1675 * Returns the status of the plugin
1677 * @return string one of plugin_manager::PLUGIN_STATUS_xxx constants
1679 public function get_status() {
1681 if (is_null($this->versiondb
) and is_null($this->versiondisk
)) {
1682 return plugin_manager
::PLUGIN_STATUS_NODB
;
1684 } else if (is_null($this->versiondb
) and !is_null($this->versiondisk
)) {
1685 return plugin_manager
::PLUGIN_STATUS_NEW
;
1687 } else if (!is_null($this->versiondb
) and is_null($this->versiondisk
)) {
1688 if (plugin_manager
::is_deleted_standard_plugin($this->type
, $this->name
)) {
1689 return plugin_manager
::PLUGIN_STATUS_DELETE
;
1691 return plugin_manager
::PLUGIN_STATUS_MISSING
;
1694 } else if ((string)$this->versiondb
=== (string)$this->versiondisk
) {
1695 return plugin_manager
::PLUGIN_STATUS_UPTODATE
;
1697 } else if ($this->versiondb
< $this->versiondisk
) {
1698 return plugin_manager
::PLUGIN_STATUS_UPGRADE
;
1700 } else if ($this->versiondb
> $this->versiondisk
) {
1701 return plugin_manager
::PLUGIN_STATUS_DOWNGRADE
;
1704 // $version = pi(); and similar funny jokes - hopefully Donald E. Knuth will never contribute to Moodle ;-)
1705 throw new coding_exception('Unable to determine plugin state, check the plugin versions');
1710 * Returns the information about plugin availability
1712 * True means that the plugin is enabled. False means that the plugin is
1713 * disabled. Null means that the information is not available, or the
1714 * plugin does not support configurable availability or the availability
1715 * can not be changed.
1719 public function is_enabled() {
1724 * Populates the property {@link $availableupdates} with the information provided by
1725 * available update checker
1727 * @param available_update_checker $provider the class providing the available update info
1729 public function check_available_updates(available_update_checker
$provider) {
1732 if (isset($CFG->updateminmaturity
)) {
1733 $minmaturity = $CFG->updateminmaturity
;
1735 // this can happen during the very first upgrade to 2.3
1736 $minmaturity = MATURITY_STABLE
;
1739 $this->availableupdates
= $provider->get_update_info($this->component
,
1740 array('minmaturity' => $minmaturity));
1744 * If there are updates for this plugin available, returns them.
1746 * Returns array of {@link available_update_info} objects, if some update
1747 * is available. Returns null if there is no update available or if the update
1748 * availability is unknown.
1750 * @return array|null
1752 public function available_updates() {
1754 if (empty($this->availableupdates
) or !is_array($this->availableupdates
)) {
1760 foreach ($this->availableupdates
as $availableupdate) {
1761 if ($availableupdate->version
> $this->versiondisk
) {
1762 $updates[] = $availableupdate;
1766 if (empty($updates)) {
1774 * Returns the URL of the plugin settings screen
1776 * Null value means that the plugin either does not have the settings screen
1777 * or its location is not available via this library.
1779 * @return null|moodle_url
1781 public function get_settings_url() {
1786 * Returns the URL of the screen where this plugin can be uninstalled
1788 * Visiting that URL must be safe, that is a manual confirmation is needed
1789 * for actual uninstallation of the plugin. Null value means that the
1790 * plugin either does not support uninstallation, or does not require any
1791 * database cleanup or the location of the screen is not available via this
1794 * @return null|moodle_url
1796 public function get_uninstall_url() {
1801 * Returns relative directory of the plugin with heading '/'
1805 public function get_dir() {
1808 return substr($this->rootdir
, strlen($CFG->dirroot
));
1812 * Provides access to plugin versions from {config_plugins}
1814 * @param string $plugin plugin name
1815 * @param double $disablecache optional, defaults to false
1816 * @return int|false the stored value or false if not found
1818 protected function get_version_from_config_plugins($plugin, $disablecache=false) {
1820 static $pluginversions = null;
1822 if (is_null($pluginversions) or $disablecache) {
1824 $pluginversions = $DB->get_records_menu('config_plugins', array('name' => 'version'), 'plugin', 'plugin,value');
1825 } catch (dml_exception
$e) {
1827 $pluginversions = array();
1831 if (!array_key_exists($plugin, $pluginversions)) {
1835 return $pluginversions[$plugin];
1841 * General class for all plugin types that do not have their own class
1843 class plugininfo_general
extends plugininfo_base
{
1848 * Class for page side blocks
1850 class plugininfo_block
extends plugininfo_base
{
1852 public static function get_plugins($type, $typerootdir, $typeclass) {
1854 // get the information about blocks at the disk
1855 $blocks = parent
::get_plugins($type, $typerootdir, $typeclass);
1857 // add blocks missing from disk
1858 $blocksinfo = self
::get_blocks_info();
1859 foreach ($blocksinfo as $blockname => $blockinfo) {
1860 if (isset($blocks[$blockname])) {
1863 $plugin = new $typeclass();
1864 $plugin->type
= $type;
1865 $plugin->typerootdir
= $typerootdir;
1866 $plugin->name
= $blockname;
1867 $plugin->rootdir
= null;
1868 $plugin->displayname
= $blockname;
1869 $plugin->versiondb
= $blockinfo->version
;
1870 $plugin->init_is_standard();
1872 $blocks[$blockname] = $plugin;
1878 public function init_display_name() {
1880 if (get_string_manager()->string_exists('pluginname', 'block_' . $this->name
)) {
1881 $this->displayname
= get_string('pluginname', 'block_' . $this->name
);
1883 } else if (($block = block_instance($this->name
)) !== false) {
1884 $this->displayname
= $block->get_title();
1887 parent
::init_display_name();
1891 public function load_db_version() {
1894 $blocksinfo = self
::get_blocks_info();
1895 if (isset($blocksinfo[$this->name
]->version
)) {
1896 $this->versiondb
= $blocksinfo[$this->name
]->version
;
1900 public function is_enabled() {
1902 $blocksinfo = self
::get_blocks_info();
1903 if (isset($blocksinfo[$this->name
]->visible
)) {
1904 if ($blocksinfo[$this->name
]->visible
) {
1910 return parent
::is_enabled();
1914 public function get_settings_url() {
1916 if (($block = block_instance($this->name
)) === false) {
1917 return parent
::get_settings_url();
1919 } else if ($block->has_config()) {
1920 if (file_exists($this->full_path('settings.php'))) {
1921 return new moodle_url('/admin/settings.php', array('section' => 'blocksetting' . $this->name
));
1923 $blocksinfo = self
::get_blocks_info();
1924 return new moodle_url('/admin/block.php', array('block' => $blocksinfo[$this->name
]->id
));
1928 return parent
::get_settings_url();
1932 public function get_uninstall_url() {
1934 $blocksinfo = self
::get_blocks_info();
1935 return new moodle_url('/admin/blocks.php', array('delete' => $blocksinfo[$this->name
]->id
, 'sesskey' => sesskey()));
1939 * Provides access to the records in {block} table
1941 * @param bool $disablecache do not use internal static cache
1942 * @return array array of stdClasses
1944 protected static function get_blocks_info($disablecache=false) {
1946 static $blocksinfocache = null;
1948 if (is_null($blocksinfocache) or $disablecache) {
1950 $blocksinfocache = $DB->get_records('block', null, 'name', 'name,id,version,visible');
1951 } catch (dml_exception
$e) {
1953 $blocksinfocache = array();
1957 return $blocksinfocache;
1963 * Class for text filters
1965 class plugininfo_filter
extends plugininfo_base
{
1967 public static function get_plugins($type, $typerootdir, $typeclass) {
1972 // get the list of filters from both /filter and /mod location
1973 $installed = filter_get_all_installed();
1975 foreach ($installed as $filterlegacyname => $displayname) {
1976 $plugin = new $typeclass();
1977 $plugin->type
= $type;
1978 $plugin->typerootdir
= $typerootdir;
1979 $plugin->name
= self
::normalize_legacy_name($filterlegacyname);
1980 $plugin->rootdir
= $CFG->dirroot
. '/' . $filterlegacyname;
1981 $plugin->displayname
= $displayname;
1983 $plugin->load_disk_version();
1984 $plugin->load_db_version();
1985 $plugin->load_required_main_version();
1986 $plugin->init_is_standard();
1988 $filters[$plugin->name
] = $plugin;
1991 $globalstates = self
::get_global_states();
1993 if ($DB->get_manager()->table_exists('filter_active')) {
1994 // if we're upgrading from 1.9, the table does not exist yet
1995 // if it does, make sure that all installed filters are registered
1996 $needsreload = false;
1997 foreach (array_keys($installed) as $filterlegacyname) {
1998 if (!isset($globalstates[self
::normalize_legacy_name($filterlegacyname)])) {
1999 filter_set_global_state($filterlegacyname, TEXTFILTER_DISABLED
);
2000 $needsreload = true;
2004 $globalstates = self
::get_global_states(true);
2008 // make sure that all registered filters are installed, just in case
2009 foreach ($globalstates as $name => $info) {
2010 if (!isset($filters[$name])) {
2011 // oops, there is a record in filter_active but the filter is not installed
2012 $plugin = new $typeclass();
2013 $plugin->type
= $type;
2014 $plugin->typerootdir
= $typerootdir;
2015 $plugin->name
= $name;
2016 $plugin->rootdir
= $CFG->dirroot
. '/' . $info->legacyname
;
2017 $plugin->displayname
= $info->legacyname
;
2019 $plugin->load_db_version();
2021 if (is_null($plugin->versiondb
)) {
2022 // this is a hack to stimulate 'Missing from disk' error
2023 // because $plugin->versiondisk will be null !== false
2024 $plugin->versiondb
= false;
2027 $filters[$plugin->name
] = $plugin;
2034 public function init_display_name() {
2035 // do nothing, the name is set in self::get_plugins()
2039 * @see load_version_php()
2041 protected function load_version_php() {
2042 if (strpos($this->name
, 'mod_') === 0) {
2043 // filters bundled with modules do not have a version.php and so
2044 // do not provide their own versioning information.
2045 return new stdClass();
2047 return parent
::load_version_php();
2050 public function is_enabled() {
2052 $globalstates = self
::get_global_states();
2054 foreach ($globalstates as $filterlegacyname => $info) {
2055 $name = self
::normalize_legacy_name($filterlegacyname);
2056 if ($name === $this->name
) {
2057 if ($info->active
== TEXTFILTER_DISABLED
) {
2060 // it may be 'On' or 'Off, but available'
2069 public function get_settings_url() {
2071 $globalstates = self
::get_global_states();
2072 $legacyname = $globalstates[$this->name
]->legacyname
;
2073 if (filter_has_global_settings($legacyname)) {
2074 return new moodle_url('/admin/settings.php', array('section' => 'filtersetting' . str_replace('/', '', $legacyname)));
2080 public function get_uninstall_url() {
2082 if (strpos($this->name
, 'mod_') === 0) {
2085 $globalstates = self
::get_global_states();
2086 $legacyname = $globalstates[$this->name
]->legacyname
;
2087 return new moodle_url('/admin/filters.php', array('sesskey' => sesskey(), 'filterpath' => $legacyname, 'action' => 'delete'));
2092 * Convert legacy filter names like 'filter/foo' or 'mod/bar' into frankenstyle
2094 * @param string $legacyfiltername legacy filter name
2095 * @return string frankenstyle-like name
2097 protected static function normalize_legacy_name($legacyfiltername) {
2099 $name = str_replace('/', '_', $legacyfiltername);
2100 if (strpos($name, 'filter_') === 0) {
2101 $name = substr($name, 7);
2103 throw new coding_exception('Unable to determine filter name: ' . $legacyfiltername);
2111 * Provides access to the results of {@link filter_get_global_states()}
2112 * but indexed by the normalized filter name
2114 * The legacy filter name is available as ->legacyname property.
2116 * @param bool $disablecache
2119 protected static function get_global_states($disablecache=false) {
2121 static $globalstatescache = null;
2123 if ($disablecache or is_null($globalstatescache)) {
2125 if (!$DB->get_manager()->table_exists('filter_active')) {
2126 // we're upgrading from 1.9 and the table used by {@link filter_get_global_states()}
2127 // does not exist yet
2128 $globalstatescache = array();
2131 foreach (filter_get_global_states() as $legacyname => $info) {
2132 $name = self
::normalize_legacy_name($legacyname);
2133 $filterinfo = new stdClass();
2134 $filterinfo->legacyname
= $legacyname;
2135 $filterinfo->active
= $info->active
;
2136 $filterinfo->sortorder
= $info->sortorder
;
2137 $globalstatescache[$name] = $filterinfo;
2142 return $globalstatescache;
2148 * Class for activity modules
2150 class plugininfo_mod
extends plugininfo_base
{
2152 public static function get_plugins($type, $typerootdir, $typeclass) {
2154 // get the information about plugins at the disk
2155 $modules = parent
::get_plugins($type, $typerootdir, $typeclass);
2157 // add modules missing from disk
2158 $modulesinfo = self
::get_modules_info();
2159 foreach ($modulesinfo as $modulename => $moduleinfo) {
2160 if (isset($modules[$modulename])) {
2163 $plugin = new $typeclass();
2164 $plugin->type
= $type;
2165 $plugin->typerootdir
= $typerootdir;
2166 $plugin->name
= $modulename;
2167 $plugin->rootdir
= null;
2168 $plugin->displayname
= $modulename;
2169 $plugin->versiondb
= $moduleinfo->version
;
2170 $plugin->init_is_standard();
2172 $modules[$modulename] = $plugin;
2178 public function init_display_name() {
2179 if (get_string_manager()->string_exists('pluginname', $this->component
)) {
2180 $this->displayname
= get_string('pluginname', $this->component
);
2182 $this->displayname
= get_string('modulename', $this->component
);
2187 * Load the data from version.php.
2188 * @return object the data object defined in version.php.
2190 protected function load_version_php() {
2191 $versionfile = $this->full_path('version.php');
2193 $module = new stdClass();
2194 if (is_readable($versionfile)) {
2195 include($versionfile);
2200 public function load_db_version() {
2203 $modulesinfo = self
::get_modules_info();
2204 if (isset($modulesinfo[$this->name
]->version
)) {
2205 $this->versiondb
= $modulesinfo[$this->name
]->version
;
2209 public function is_enabled() {
2211 $modulesinfo = self
::get_modules_info();
2212 if (isset($modulesinfo[$this->name
]->visible
)) {
2213 if ($modulesinfo[$this->name
]->visible
) {
2219 return parent
::is_enabled();
2223 public function get_settings_url() {
2225 if (file_exists($this->full_path('settings.php')) or file_exists($this->full_path('settingstree.php'))) {
2226 return new moodle_url('/admin/settings.php', array('section' => 'modsetting' . $this->name
));
2228 return parent
::get_settings_url();
2232 public function get_uninstall_url() {
2234 if ($this->name
!== 'forum') {
2235 return new moodle_url('/admin/modules.php', array('delete' => $this->name
, 'sesskey' => sesskey()));
2242 * Provides access to the records in {modules} table
2244 * @param bool $disablecache do not use internal static cache
2245 * @return array array of stdClasses
2247 protected static function get_modules_info($disablecache=false) {
2249 static $modulesinfocache = null;
2251 if (is_null($modulesinfocache) or $disablecache) {
2253 $modulesinfocache = $DB->get_records('modules', null, 'name', 'name,id,version,visible');
2254 } catch (dml_exception
$e) {
2256 $modulesinfocache = array();
2260 return $modulesinfocache;
2266 * Class for question behaviours.
2268 class plugininfo_qbehaviour
extends plugininfo_base
{
2270 public function get_uninstall_url() {
2271 return new moodle_url('/admin/qbehaviours.php',
2272 array('delete' => $this->name
, 'sesskey' => sesskey()));
2278 * Class for question types
2280 class plugininfo_qtype
extends plugininfo_base
{
2282 public function get_uninstall_url() {
2283 return new moodle_url('/admin/qtypes.php',
2284 array('delete' => $this->name
, 'sesskey' => sesskey()));
2290 * Class for authentication plugins
2292 class plugininfo_auth
extends plugininfo_base
{
2294 public function is_enabled() {
2296 /** @var null|array list of enabled authentication plugins */
2297 static $enabled = null;
2299 if (in_array($this->name
, array('nologin', 'manual'))) {
2300 // these two are always enabled and can't be disabled
2304 if (is_null($enabled)) {
2305 $enabled = array_flip(explode(',', $CFG->auth
));
2308 return isset($enabled[$this->name
]);
2311 public function get_settings_url() {
2312 if (file_exists($this->full_path('settings.php'))) {
2313 return new moodle_url('/admin/settings.php', array('section' => 'authsetting' . $this->name
));
2315 return new moodle_url('/admin/auth_config.php', array('auth' => $this->name
));
2322 * Class for enrolment plugins
2324 class plugininfo_enrol
extends plugininfo_base
{
2326 public function is_enabled() {
2328 /** @var null|array list of enabled enrolment plugins */
2329 static $enabled = null;
2331 // We do not actually need whole enrolment classes here so we do not call
2332 // {@link enrol_get_plugins()}. Note that this may produce slightly different
2333 // results, for example if the enrolment plugin does not contain lib.php
2334 // but it is listed in $CFG->enrol_plugins_enabled
2336 if (is_null($enabled)) {
2337 $enabled = array_flip(explode(',', $CFG->enrol_plugins_enabled
));
2340 return isset($enabled[$this->name
]);
2343 public function get_settings_url() {
2345 if ($this->is_enabled() or file_exists($this->full_path('settings.php'))) {
2346 return new moodle_url('/admin/settings.php', array('section' => 'enrolsettings' . $this->name
));
2348 return parent
::get_settings_url();
2352 public function get_uninstall_url() {
2353 return new moodle_url('/admin/enrol.php', array('action' => 'uninstall', 'enrol' => $this->name
, 'sesskey' => sesskey()));
2359 * Class for messaging processors
2361 class plugininfo_message
extends plugininfo_base
{
2363 public function get_settings_url() {
2364 $processors = get_message_processors();
2365 if (isset($processors[$this->name
])) {
2366 $processor = $processors[$this->name
];
2367 if ($processor->available
&& $processor->hassettings
) {
2368 return new moodle_url('settings.php', array('section' => 'messagesetting'.$processor->name
));
2371 return parent
::get_settings_url();
2375 * @see plugintype_interface::is_enabled()
2377 public function is_enabled() {
2378 $processors = get_message_processors();
2379 if (isset($processors[$this->name
])) {
2380 return $processors[$this->name
]->configured
&& $processors[$this->name
]->enabled
;
2382 return parent
::is_enabled();
2387 * @see plugintype_interface::get_uninstall_url()
2389 public function get_uninstall_url() {
2390 $processors = get_message_processors();
2391 if (isset($processors[$this->name
])) {
2392 return new moodle_url('message.php', array('uninstall' => $processors[$this->name
]->id
, 'sesskey' => sesskey()));
2394 return parent
::get_uninstall_url();
2401 * Class for repositories
2403 class plugininfo_repository
extends plugininfo_base
{
2405 public function is_enabled() {
2407 $enabled = self
::get_enabled_repositories();
2409 return isset($enabled[$this->name
]);
2412 public function get_settings_url() {
2414 if ($this->is_enabled()) {
2415 return new moodle_url('/admin/repository.php', array('sesskey' => sesskey(), 'action' => 'edit', 'repos' => $this->name
));
2417 return parent
::get_settings_url();
2422 * Provides access to the records in {repository} table
2424 * @param bool $disablecache do not use internal static cache
2425 * @return array array of stdClasses
2427 protected static function get_enabled_repositories($disablecache=false) {
2429 static $repositories = null;
2431 if (is_null($repositories) or $disablecache) {
2432 $repositories = $DB->get_records('repository', null, 'type', 'type,visible,sortorder');
2435 return $repositories;
2441 * Class for portfolios
2443 class plugininfo_portfolio
extends plugininfo_base
{
2445 public function is_enabled() {
2447 $enabled = self
::get_enabled_portfolios();
2449 return isset($enabled[$this->name
]);
2453 * Provides access to the records in {portfolio_instance} table
2455 * @param bool $disablecache do not use internal static cache
2456 * @return array array of stdClasses
2458 protected static function get_enabled_portfolios($disablecache=false) {
2460 static $portfolios = null;
2462 if (is_null($portfolios) or $disablecache) {
2463 $portfolios = array();
2464 $instances = $DB->get_recordset('portfolio_instance', null, 'plugin');
2465 foreach ($instances as $instance) {
2466 if (isset($portfolios[$instance->plugin
])) {
2467 if ($instance->visible
) {
2468 $portfolios[$instance->plugin
]->visible
= $instance->visible
;
2471 $portfolios[$instance->plugin
] = $instance;
2484 class plugininfo_theme
extends plugininfo_base
{
2486 public function is_enabled() {
2489 if ((!empty($CFG->theme
) and $CFG->theme
=== $this->name
) or
2490 (!empty($CFG->themelegacy
) and $CFG->themelegacy
=== $this->name
)) {
2493 return parent
::is_enabled();
2500 * Class representing an MNet service
2502 class plugininfo_mnetservice
extends plugininfo_base
{
2504 public function is_enabled() {
2507 if (empty($CFG->mnet_dispatcher_mode
) ||
$CFG->mnet_dispatcher_mode
!== 'strict') {
2510 return parent
::is_enabled();
2517 * Class for admin tool plugins
2519 class plugininfo_tool
extends plugininfo_base
{
2521 public function get_uninstall_url() {
2522 return new moodle_url('/admin/tools.php', array('delete' => $this->name
, 'sesskey' => sesskey()));
2528 * Class for admin tool plugins
2530 class plugininfo_report
extends plugininfo_base
{
2532 public function get_uninstall_url() {
2533 return new moodle_url('/admin/reports.php', array('delete' => $this->name
, 'sesskey' => sesskey()));
2539 * Class for local plugins
2541 class plugininfo_local
extends plugininfo_base
{
2543 public function get_uninstall_url() {
2544 return new moodle_url('/admin/localplugins.php', array('delete' => $this->name
, 'sesskey' => sesskey()));