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;
92 * Returns a tree of known plugins and information about them
94 * @param bool $disablecache force reload, cache can be used otherwise
95 * @return array 2D array. The first keys are plugin type names (e.g. qtype);
96 * the second keys are the plugin local name (e.g. multichoice); and
97 * the values are the corresponding objects extending {@link plugininfo_base}
99 public function get_plugins($disablecache=false) {
102 if ($disablecache or is_null($this->pluginsinfo
)) {
103 $this->pluginsinfo
= array();
104 $plugintypes = get_plugin_types();
105 $plugintypes = $this->reorder_plugin_types($plugintypes);
106 foreach ($plugintypes as $plugintype => $plugintyperootdir) {
107 if (in_array($plugintype, array('base', 'general'))) {
108 throw new coding_exception('Illegal usage of reserved word for plugin type');
110 if (class_exists('plugininfo_' . $plugintype)) {
111 $plugintypeclass = 'plugininfo_' . $plugintype;
113 $plugintypeclass = 'plugininfo_general';
115 if (!in_array('plugininfo_base', class_parents($plugintypeclass))) {
116 throw new coding_exception('Class ' . $plugintypeclass . ' must extend plugininfo_base');
118 $plugins = call_user_func(array($plugintypeclass, 'get_plugins'), $plugintype, $plugintyperootdir, $plugintypeclass);
119 $this->pluginsinfo
[$plugintype] = $plugins;
122 if (empty($CFG->disableupdatenotifications
) and !during_initial_install()) {
123 // append the information about available updates provided by {@link available_update_checker()}
124 $provider = available_update_checker
::instance();
125 foreach ($this->pluginsinfo
as $plugintype => $plugins) {
126 foreach ($plugins as $plugininfoholder) {
127 $plugininfoholder->check_available_updates($provider);
133 return $this->pluginsinfo
;
137 * Returns list of plugins that define their subplugins and the information
138 * about them from the db/subplugins.php file.
140 * At the moment, only activity modules can define subplugins.
142 * @param bool $disablecache force reload, cache can be used otherwise
143 * @return array with keys like 'mod_quiz', and values the data from the
144 * corresponding db/subplugins.php file.
146 public function get_subplugins($disablecache=false) {
148 if ($disablecache or is_null($this->subpluginsinfo
)) {
149 $this->subpluginsinfo
= array();
150 $mods = get_plugin_list('mod');
151 foreach ($mods as $mod => $moddir) {
152 $modsubplugins = array();
153 if (file_exists($moddir . '/db/subplugins.php')) {
154 include($moddir . '/db/subplugins.php');
155 foreach ($subplugins as $subplugintype => $subplugintyperootdir) {
156 $subplugin = new stdClass();
157 $subplugin->type
= $subplugintype;
158 $subplugin->typerootdir
= $subplugintyperootdir;
159 $modsubplugins[$subplugintype] = $subplugin;
161 $this->subpluginsinfo
['mod_' . $mod] = $modsubplugins;
166 return $this->subpluginsinfo
;
170 * Returns the name of the plugin that defines the given subplugin type
172 * If the given subplugin type is not actually a subplugin, returns false.
174 * @param string $subplugintype the name of subplugin type, eg. workshopform or quiz
175 * @return false|string the name of the parent plugin, eg. mod_workshop
177 public function get_parent_of_subplugin($subplugintype) {
180 foreach ($this->get_subplugins() as $pluginname => $subplugintypes) {
181 if (isset($subplugintypes[$subplugintype])) {
182 $parent = $pluginname;
191 * Returns a localized name of a given plugin
193 * @param string $plugin name of the plugin, eg mod_workshop or auth_ldap
196 public function plugin_name($plugin) {
197 list($type, $name) = normalize_component($plugin);
198 return $this->pluginsinfo
[$type][$name]->displayname
;
202 * Returns a localized name of a plugin type in plural form
204 * Most plugin types define their names in core_plugin lang file. In case of subplugins,
205 * we try to ask the parent plugin for the name. In the worst case, we will return
206 * the value of the passed $type parameter.
208 * @param string $type the type of the plugin, e.g. mod or workshopform
211 public function plugintype_name_plural($type) {
213 if (get_string_manager()->string_exists('type_' . $type . '_plural', 'core_plugin')) {
214 // for most plugin types, their names are defined in core_plugin lang file
215 return get_string('type_' . $type . '_plural', 'core_plugin');
217 } else if ($parent = $this->get_parent_of_subplugin($type)) {
218 // if this is a subplugin, try to ask the parent plugin for the name
219 if (get_string_manager()->string_exists('subplugintype_' . $type . '_plural', $parent)) {
220 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type . '_plural', $parent);
222 return $this->plugin_name($parent) . ' / ' . $type;
231 * @param string $component frankenstyle component name.
232 * @return plugininfo_base|null the corresponding plugin information.
234 public function get_plugin_info($component) {
235 list($type, $name) = normalize_component($component);
236 $plugins = $this->get_plugins();
237 if (isset($plugins[$type][$name])) {
238 return $plugins[$type][$name];
245 * Get a list of any other plugins that require this one.
246 * @param string $component frankenstyle component name.
247 * @return array of frankensyle component names that require this one.
249 public function other_plugins_that_require($component) {
251 foreach ($this->get_plugins() as $type => $plugins) {
252 foreach ($plugins as $plugin) {
253 $required = $plugin->get_other_required_plugins();
254 if (isset($required[$component])) {
255 $others[] = $plugin->component
;
263 * Check a dependencies list against the list of installed plugins.
264 * @param array $dependencies compenent name to required version or ANY_VERSION.
265 * @return bool true if all the dependencies are satisfied.
267 public function are_dependencies_satisfied($dependencies) {
268 foreach ($dependencies as $component => $requiredversion) {
269 $otherplugin = $this->get_plugin_info($component);
270 if (is_null($otherplugin)) {
274 if ($requiredversion != ANY_VERSION
and $otherplugin->versiondisk
< $requiredversion) {
283 * Checks all dependencies for all installed plugins
285 * This is used by install and upgrade. The array passed by reference as the second
286 * argument is populated with the list of plugins that have failed dependencies (note that
287 * a single plugin can appear multiple times in the $failedplugins).
289 * @param int $moodleversion the version from version.php.
290 * @param array $failedplugins to return the list of plugins with non-satisfied dependencies
291 * @return bool true if all the dependencies are satisfied for all plugins.
293 public function all_plugins_ok($moodleversion, &$failedplugins = array()) {
296 foreach ($this->get_plugins() as $type => $plugins) {
297 foreach ($plugins as $plugin) {
299 if (!$plugin->is_core_dependency_satisfied($moodleversion)) {
301 $failedplugins[] = $plugin->component
;
304 if (!$this->are_dependencies_satisfied($plugin->get_other_required_plugins())) {
306 $failedplugins[] = $plugin->component
;
315 * Checks if there are some plugins with a known available update
317 * @return bool true if there is at least one available update
319 public function some_plugins_updatable() {
320 foreach ($this->get_plugins() as $type => $plugins) {
321 foreach ($plugins as $plugin) {
322 if ($plugin->available_updates()) {
332 * Defines a list of all plugins that were originally shipped in the standard Moodle distribution,
333 * but are not anymore and are deleted during upgrades.
335 * The main purpose of this list is to hide missing plugins during upgrade.
337 * @param string $type plugin type
338 * @param string $name plugin name
341 public static function is_deleted_standard_plugin($type, $name) {
342 static $plugins = array(
343 // do not add 1.9-2.2 plugin removals here
346 if (!isset($plugins[$type])) {
349 return in_array($name, $plugins[$type]);
353 * Defines a white list of all plugins shipped in the standard Moodle distribution
355 * @param string $type
356 * @return false|array array of standard plugins or false if the type is unknown
358 public static function standard_plugins_list($type) {
359 static $standard_plugins = array(
361 'assignment' => array(
362 'offline', 'online', 'upload', 'uploadsingle'
365 'assignsubmission' => array(
366 'comments', 'file', 'onlinetext'
369 'assignfeedback' => array(
374 'cas', 'db', 'email', 'fc', 'imap', 'ldap', 'manual', 'mnet',
375 'nntp', 'nologin', 'none', 'pam', 'pop3', 'radius',
376 'shibboleth', 'webservice'
380 'activity_modules', 'admin_bookmarks', 'blog_menu',
381 'blog_recent', 'blog_tags', 'calendar_month',
382 'calendar_upcoming', 'comments', 'community',
383 'completionstatus', 'course_list', 'course_overview',
384 'course_summary', 'feedback', 'glossary_random', 'html',
385 'login', 'mentees', 'messages', 'mnet_hosts', 'myprofile',
386 'navigation', 'news_items', 'online_users', 'participants',
387 'private_files', 'quiz_results', 'recent_activity',
388 'rss_client', 'search_forums', 'section_links',
389 'selfcompletion', 'settings', 'site_main_menu',
390 'social_activities', 'tag_flickr', 'tag_youtube', 'tags'
394 'exportimscp', 'importhtml', 'print'
397 'coursereport' => array(
401 'datafield' => array(
402 'checkbox', 'date', 'file', 'latlong', 'menu', 'multimenu',
403 'number', 'picture', 'radiobutton', 'text', 'textarea', 'url'
406 'datapreset' => array(
411 'textarea', 'tinymce'
415 'authorize', 'category', 'cohort', 'database', 'flatfile',
416 'guest', 'imsenterprise', 'ldap', 'manual', 'meta', 'mnet',
421 'activitynames', 'algebra', 'censor', 'emailprotect',
422 'emoticon', 'mediaplugin', 'multilang', 'tex', 'tidy',
423 'urltolink', 'data', 'glossary'
427 'scorm', 'social', 'topics', 'weeks'
430 'gradeexport' => array(
431 'ods', 'txt', 'xls', 'xml'
434 'gradeimport' => array(
438 'gradereport' => array(
439 'grader', 'outcomes', 'overview', 'user'
442 'gradingform' => array(
450 'email', 'jabber', 'popup'
453 'mnetservice' => array(
458 'assign', 'assignment', 'book', 'chat', 'choice', 'data', 'feedback', 'folder',
459 'forum', 'glossary', 'imscp', 'label', 'lesson', 'lti', 'page',
460 'quiz', 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop'
463 'plagiarism' => array(
466 'portfolio' => array(
467 'boxnet', 'download', 'flickr', 'googledocs', 'mahara', 'picasa'
470 'profilefield' => array(
471 'checkbox', 'datetime', 'menu', 'text', 'textarea'
474 'qbehaviour' => array(
475 'adaptive', 'adaptivenopenalty', 'deferredcbm',
476 'deferredfeedback', 'immediatecbm', 'immediatefeedback',
477 'informationitem', 'interactive', 'interactivecountback',
478 'manualgraded', 'missing'
482 'aiken', 'blackboard', 'blackboard_six', 'examview', 'gift',
483 'learnwise', 'missingword', 'multianswer', 'webct',
488 'calculated', 'calculatedmulti', 'calculatedsimple',
489 'description', 'essay', 'match', 'missingtype', 'multianswer',
490 'multichoice', 'numerical', 'random', 'randomsamatch',
491 'shortanswer', 'truefalse'
495 'grading', 'overview', 'responses', 'statistics'
498 'quizaccess' => array(
499 'delaybetweenattempts', 'ipaddress', 'numattempts', 'openclosedate',
500 'password', 'safebrowser', 'securewindow', 'timelimit'
504 'backups', 'completion', 'configlog', 'courseoverview',
505 'log', 'loglive', 'outline', 'participation', 'progress', 'questioninstances', 'security', 'stats'
508 'repository' => array(
509 'alfresco', 'boxnet', 'coursefiles', 'dropbox', 'equella', 'filesystem',
510 'flickr', 'flickr_public', 'googledocs', 'local', 'merlot',
511 'picasa', 'recent', 's3', 'upload', 'url', 'user', 'webdav',
512 'wikimedia', 'youtube'
515 'scormreport' => array(
522 'afterburner', 'anomaly', 'arialist', 'base', 'binarius',
523 'boxxie', 'brick', 'canvas', 'formal_white', 'formfactor',
524 'fusion', 'leatherbound', 'magazine', 'mymobile', 'nimble',
525 'nonzero', 'overlay', 'serenity', 'sky_high', 'splash',
526 'standard', 'standardold'
530 'assignmentupgrade', 'bloglevelupgrade', 'capability', 'customlang', 'dbtransfer', 'generator',
531 'health', 'innodb', 'langimport', 'multilangupgrade', 'phpunit', 'profiling',
532 'qeupgradehelper', 'replace', 'spamcleaner', 'timezoneimport', 'unittest',
533 'uploaduser', 'unsuproles', 'xmldb'
536 'webservice' => array(
537 'amf', 'rest', 'soap', 'xmlrpc'
540 'workshopallocation' => array(
541 'manual', 'random', 'scheduled'
544 'workshopeval' => array(
548 'workshopform' => array(
549 'accumulative', 'comments', 'numerrors', 'rubric'
553 if (isset($standard_plugins[$type])) {
554 return $standard_plugins[$type];
561 * Reorders plugin types into a sequence to be displayed
563 * For technical reasons, plugin types returned by {@link get_plugin_types()} are
564 * in a certain order that does not need to fit the expected order for the display.
565 * Particularly, activity modules should be displayed first as they represent the
566 * real heart of Moodle. They should be followed by other plugin types that are
567 * used to build the courses (as that is what one expects from LMS). After that,
568 * other supportive plugin types follow.
570 * @param array $types associative array
571 * @return array same array with altered order of items
573 protected function reorder_plugin_types(array $types) {
575 'mod' => $types['mod'],
576 'block' => $types['block'],
577 'qtype' => $types['qtype'],
578 'qbehaviour' => $types['qbehaviour'],
579 'qformat' => $types['qformat'],
580 'filter' => $types['filter'],
581 'enrol' => $types['enrol'],
583 foreach ($types as $type => $path) {
584 if (!isset($fix[$type])) {
594 * General exception thrown by the {@link available_update_checker} class
596 class available_update_checker_exception
extends moodle_exception
{
599 * @param string $errorcode exception description identifier
600 * @param mixed $debuginfo debugging data to display
602 public function __construct($errorcode, $debuginfo=null) {
603 parent
::__construct($errorcode, 'core_plugin', '', null, print_r($debuginfo, true));
609 * Singleton class that handles checking for available updates
611 class available_update_checker
{
613 /** @var available_update_checker holds the singleton instance */
614 protected static $singletoninstance;
615 /** @var null|int the timestamp of when the most recent response was fetched */
616 protected $recentfetch = null;
617 /** @var null|array the recent response from the update notification provider */
618 protected $recentresponse = null;
619 /** @var null|string the numerical version of the local Moodle code */
620 protected $currentversion = null;
621 /** @var null|string the release info of the local Moodle code */
622 protected $currentrelease = null;
623 /** @var null|string branch of the local Moodle code */
624 protected $currentbranch = null;
625 /** @var array of (string)frankestyle => (string)version list of additional plugins deployed at this site */
626 protected $currentplugins = array();
629 * Direct initiation not allowed, use the factory method {@link self::instance()}
631 protected function __construct() {
635 * Sorry, this is singleton
637 protected function __clone() {
641 * Factory method for this class
643 * @return available_update_checker the singleton instance
645 public static function instance() {
646 if (is_null(self
::$singletoninstance)) {
647 self
::$singletoninstance = new self();
649 return self
::$singletoninstance;
653 * Returns the timestamp of the last execution of {@link fetch()}
655 * @return int|null null if it has never been executed or we don't known
657 public function get_last_timefetched() {
659 $this->restore_response();
661 if (!empty($this->recentfetch
)) {
662 return $this->recentfetch
;
670 * Fetches the available update status from the remote site
672 * @throws available_update_checker_exception
674 public function fetch() {
675 $response = $this->get_response();
676 $this->validate_response($response);
677 $this->store_response($response);
681 * Returns the available update information for the given component
683 * This method returns null if the most recent response does not contain any information
684 * about it. The returned structure is an array of available updates for the given
685 * component. Each update info is an object with at least one property called
686 * 'version'. Other possible properties are 'release', 'maturity', 'url' and 'downloadurl'.
688 * For the 'core' component, the method returns real updates only (those with higher version).
689 * For all other components, the list of all known remote updates is returned and the caller
690 * (usually the {@link plugin_manager}) is supposed to make the actual comparison of versions.
692 * @param string $component frankenstyle
693 * @param array $options with supported keys 'minmaturity' and/or 'notifybuilds'
694 * @return null|array null or array of available_update_info objects
696 public function get_update_info($component, array $options = array()) {
698 if (!isset($options['minmaturity'])) {
699 $options['minmaturity'] = 0;
702 if (!isset($options['notifybuilds'])) {
703 $options['notifybuilds'] = false;
706 if ($component == 'core') {
707 $this->load_current_environment();
710 $this->restore_response();
712 if (empty($this->recentresponse
['updates'][$component])) {
717 foreach ($this->recentresponse
['updates'][$component] as $info) {
718 $update = new available_update_info($component, $info);
719 if (isset($update->maturity
) and ($update->maturity
< $options['minmaturity'])) {
722 if ($component == 'core') {
723 if ($update->version
<= $this->currentversion
) {
726 if (empty($options['notifybuilds']) and $this->is_same_release($update->release
)) {
730 $updates[] = $update;
733 if (empty($updates)) {
741 * The method being run via cron.php
743 public function cron() {
746 if (!$this->cron_autocheck_enabled()) {
747 $this->cron_mtrace('Automatic check for available updates not enabled, skipping.');
751 $now = $this->cron_current_timestamp();
753 if ($this->cron_has_fresh_fetch($now)) {
754 $this->cron_mtrace('Recently fetched info about available updates is still fresh enough, skipping.');
758 if ($this->cron_has_outdated_fetch($now)) {
759 $this->cron_mtrace('Outdated or missing info about available updates, forced fetching ... ', '');
760 $this->cron_execute();
764 $offset = $this->cron_execution_offset();
765 $start = mktime(1, 0, 0, date('n', $now), date('j', $now), date('Y', $now)); // 01:00 AM today local time
766 if ($now > $start +
$offset) {
767 $this->cron_mtrace('Regular daily check for available updates ... ', '');
768 $this->cron_execute();
773 /// end of public API //////////////////////////////////////////////////////
776 * Makes cURL request to get data from the remote site
778 * @return string raw request result
779 * @throws available_update_checker_exception
781 protected function get_response() {
782 $curl = new curl(array('proxy' => true));
783 $response = $curl->post($this->prepare_request_url(), $this->prepare_request_params());
784 $curlinfo = $curl->get_info();
785 if ($curlinfo['http_code'] != 200) {
786 throw new available_update_checker_exception('err_response_http_code', $curlinfo['http_code']);
792 * Makes sure the response is valid, has correct API format etc.
794 * @param string $response raw response as returned by the {@link self::get_response()}
795 * @throws available_update_checker_exception
797 protected function validate_response($response) {
799 $response = $this->decode_response($response);
801 if (empty($response)) {
802 throw new available_update_checker_exception('err_response_empty');
805 if (empty($response['status']) or $response['status'] !== 'OK') {
806 throw new available_update_checker_exception('err_response_status', $response['status']);
809 if (empty($response['apiver']) or $response['apiver'] !== '1.0') {
810 throw new available_update_checker_exception('err_response_format_version', $response['apiver']);
813 if (empty($response['forbranch']) or $response['forbranch'] !== moodle_major_version(true)) {
814 throw new available_update_checker_exception('err_response_target_version', $response['target']);
819 * Decodes the raw string response from the update notifications provider
821 * @param string $response as returned by {@link self::get_response()}
822 * @return array decoded response structure
824 protected function decode_response($response) {
825 return json_decode($response, true);
829 * Stores the valid fetched response for later usage
831 * This implementation uses the config_plugins table as the permanent storage.
833 * @param string $response raw valid data returned by {@link self::get_response()}
835 protected function store_response($response) {
837 set_config('recentfetch', time(), 'core_plugin');
838 set_config('recentresponse', $response, 'core_plugin');
840 $this->restore_response(true);
844 * Loads the most recent raw response record we have fetched
846 * This implementation uses the config_plugins table as the permanent storage.
848 * @param bool $forcereload reload even if it was already loaded
850 protected function restore_response($forcereload = false) {
852 if (!$forcereload and !is_null($this->recentresponse
)) {
853 // we already have it, nothing to do
857 $config = get_config('core_plugin');
859 if (!empty($config->recentresponse
) and !empty($config->recentfetch
)) {
861 $this->validate_response($config->recentresponse
);
862 $this->recentfetch
= $config->recentfetch
;
863 $this->recentresponse
= $this->decode_response($config->recentresponse
);
864 } catch (available_update_checker_exception
$e) {
865 // do not set recentresponse if the validation fails
869 $this->recentresponse
= array();
874 * Compares two raw {@link $recentresponse} records and returns the list of changed updates
876 * This method is used to populate potential update info to be sent to site admins.
880 * @throws available_update_checker_exception
881 * @return array parts of $new['updates'] that have changed
883 protected function compare_responses(array $old, array $new) {
889 if (!array_key_exists('updates', $new)) {
890 throw new available_update_checker_exception('err_response_format');
894 return $new['updates'];
897 if (!array_key_exists('updates', $old)) {
898 throw new available_update_checker_exception('err_response_format');
903 foreach ($new['updates'] as $newcomponent => $newcomponentupdates) {
904 if (empty($old['updates'][$newcomponent])) {
905 $changes[$newcomponent] = $newcomponentupdates;
908 foreach ($newcomponentupdates as $newcomponentupdate) {
910 foreach ($old['updates'][$newcomponent] as $oldcomponentupdate) {
911 if ($newcomponentupdate['version'] == $oldcomponentupdate['version']) {
916 if (!isset($changes[$newcomponent])) {
917 $changes[$newcomponent] = array();
919 $changes[$newcomponent][] = $newcomponentupdate;
928 * Returns the URL to send update requests to
930 * During the development or testing, you can set $CFG->alternativeupdateproviderurl
931 * to a custom URL that will be used. Otherwise the standard URL will be returned.
935 protected function prepare_request_url() {
938 if (!empty($CFG->alternativeupdateproviderurl
)) {
939 return $CFG->alternativeupdateproviderurl
;
941 return 'http://download.moodle.org/api/1.0/updates.php';
946 * Sets the properties currentversion, currentrelease, currentbranch and currentplugins
948 * @param bool $forcereload
950 protected function load_current_environment($forcereload=false) {
953 if (!is_null($this->currentversion
) and !$forcereload) {
958 require($CFG->dirroot
.'/version.php');
959 $this->currentversion
= $version;
960 $this->currentrelease
= $release;
961 $this->currentbranch
= moodle_major_version(true);
963 $pluginman = plugin_manager
::instance();
964 foreach ($pluginman->get_plugins() as $type => $plugins) {
965 foreach ($plugins as $plugin) {
966 if (!$plugin->is_standard()) {
967 $this->currentplugins
[$plugin->component
] = $plugin->versiondisk
;
974 * Returns the list of HTTP params to be sent to the updates provider URL
976 * @return array of (string)param => (string)value
978 protected function prepare_request_params() {
981 $this->load_current_environment();
982 $this->restore_response();
985 $params['format'] = 'json';
987 if (isset($this->recentresponse
['ticket'])) {
988 $params['ticket'] = $this->recentresponse
['ticket'];
991 if (isset($this->currentversion
)) {
992 $params['version'] = $this->currentversion
;
994 throw new coding_exception('Main Moodle version must be already known here');
997 if (isset($this->currentbranch
)) {
998 $params['branch'] = $this->currentbranch
;
1000 throw new coding_exception('Moodle release must be already known here');
1004 foreach ($this->currentplugins
as $plugin => $version) {
1005 $plugins[] = $plugin.'@'.$version;
1007 if (!empty($plugins)) {
1008 $params['plugins'] = implode(',', $plugins);
1015 * Returns the current timestamp
1017 * @return int the timestamp
1019 protected function cron_current_timestamp() {
1024 * Output cron debugging info
1027 * @param string $msg output message
1028 * @param string $eol end of line
1030 protected function cron_mtrace($msg, $eol = PHP_EOL
) {
1035 * Decide if the autocheck feature is disabled in the server setting
1037 * @return bool true if autocheck enabled, false if disabled
1039 protected function cron_autocheck_enabled() {
1042 if (empty($CFG->updateautocheck
)) {
1050 * Decide if the recently fetched data are still fresh enough
1052 * @param int $now current timestamp
1053 * @return bool true if no need to re-fetch, false otherwise
1055 protected function cron_has_fresh_fetch($now) {
1056 $recent = $this->get_last_timefetched();
1058 if (empty($recent)) {
1062 if ($now < $recent) {
1063 $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1067 if ($now - $recent > HOURSECS
) {
1075 * Decide if the fetch is outadated or even missing
1077 * @param int $now current timestamp
1078 * @return bool false if no need to re-fetch, true otherwise
1080 protected function cron_has_outdated_fetch($now) {
1081 $recent = $this->get_last_timefetched();
1083 if (empty($recent)) {
1087 if ($now < $recent) {
1088 $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1092 if ($now - $recent > 48 * HOURSECS
) {
1100 * Returns the cron execution offset for this site
1102 * The main {@link self::cron()} is supposed to run every night in some random time
1103 * between 01:00 and 06:00 AM (local time). The exact moment is defined by so called
1104 * execution offset, that is the amount of time after 01:00 AM. The offset value is
1105 * initially generated randomly and then used consistently at the site. This way, the
1106 * regular checks against the download.moodle.org server are spread in time.
1108 * @return int the offset number of seconds from range 1 sec to 5 hours
1110 protected function cron_execution_offset() {
1113 if (empty($CFG->updatecronoffset
)) {
1114 set_config('updatecronoffset', rand(1, 5 * HOURSECS
));
1117 return $CFG->updatecronoffset
;
1121 * Fetch available updates info and eventually send notification to site admins
1123 protected function cron_execute() {
1126 $this->restore_response();
1127 $previous = $this->recentresponse
;
1129 $this->restore_response(true);
1130 $current = $this->recentresponse
;
1131 $changes = $this->compare_responses($previous, $current);
1132 $notifications = $this->cron_notifications($changes);
1133 $this->cron_notify($notifications);
1134 $this->cron_mtrace('done');
1135 } catch (available_update_checker_exception
$e) {
1136 $this->cron_mtrace('FAILED!');
1141 * Given the list of changes in available updates, pick those to send to site admins
1143 * @param array $changes as returned by {@link self::compare_responses()}
1144 * @return array of available_update_info objects to send to site admins
1146 protected function cron_notifications(array $changes) {
1149 $notifications = array();
1150 $pluginman = plugin_manager
::instance();
1151 $plugins = $pluginman->get_plugins(true);
1153 foreach ($changes as $component => $componentchanges) {
1154 if (empty($componentchanges)) {
1157 $componentupdates = $this->get_update_info($component,
1158 array('minmaturity' => $CFG->updateminmaturity
, 'notifybuilds' => $CFG->updatenotifybuilds
));
1159 if (empty($componentupdates)) {
1162 // notify only about those $componentchanges that are present in $componentupdates
1163 // to respect the preferences
1164 foreach ($componentchanges as $componentchange) {
1165 foreach ($componentupdates as $componentupdate) {
1166 if ($componentupdate->version
== $componentchange['version']) {
1167 if ($component == 'core') {
1168 // in case of 'core' this is enough, we already know that the
1169 // $componentupdate is a real update with higher version
1170 $notifications[] = $componentupdate;
1172 // use the plugin_manager to check if the reported $componentchange
1173 // is a real update with higher version. such a real update must be
1174 // present in the 'availableupdates' property of one of the component's
1175 // available_update_info object
1176 list($plugintype, $pluginname) = normalize_component($component);
1177 if (!empty($plugins[$plugintype][$pluginname]->availableupdates
)) {
1178 foreach ($plugins[$plugintype][$pluginname]->availableupdates
as $availableupdate) {
1179 if ($availableupdate->version
== $componentchange['version']) {
1180 $notifications[] = $componentupdate;
1190 return $notifications;
1194 * Sends the given notifications to site admins via messaging API
1196 * @param array $notifications array of available_update_info objects to send
1198 protected function cron_notify(array $notifications) {
1201 if (empty($notifications)) {
1205 $admins = get_admins();
1207 if (empty($admins)) {
1211 $this->cron_mtrace('sending notifications ... ', '');
1213 $text = get_string('updatenotifications', 'core_admin') . PHP_EOL
;
1214 $html = html_writer
::tag('h1', get_string('updatenotifications', 'core_admin')) . PHP_EOL
;
1216 $coreupdates = array();
1217 $pluginupdates = array();
1219 foreach ($notifications as $notification) {
1220 if ($notification->component
== 'core') {
1221 $coreupdates[] = $notification;
1223 $pluginupdates[] = $notification;
1227 if (!empty($coreupdates)) {
1228 $text .= PHP_EOL
. get_string('updateavailable', 'core_admin') . PHP_EOL
;
1229 $html .= html_writer
::tag('h2', get_string('updateavailable', 'core_admin')) . PHP_EOL
;
1230 $html .= html_writer
::start_tag('ul') . PHP_EOL
;
1231 foreach ($coreupdates as $coreupdate) {
1232 $html .= html_writer
::start_tag('li');
1233 if (isset($coreupdate->release
)) {
1234 $text .= get_string('updateavailable_release', 'core_admin', $coreupdate->release
);
1235 $html .= html_writer
::tag('strong', get_string('updateavailable_release', 'core_admin', $coreupdate->release
));
1237 if (isset($coreupdate->version
)) {
1238 $text .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version
);
1239 $html .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version
);
1241 if (isset($coreupdate->maturity
)) {
1242 $text .= ' ('.get_string('maturity'.$coreupdate->maturity
, 'core_admin').')';
1243 $html .= ' ('.get_string('maturity'.$coreupdate->maturity
, 'core_admin').')';
1246 $html .= html_writer
::end_tag('li') . PHP_EOL
;
1249 $html .= html_writer
::end_tag('ul') . PHP_EOL
;
1251 $a = array('url' => $CFG->wwwroot
.'/'.$CFG->admin
.'/index.php');
1252 $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL
;
1253 $a = array('url' => html_writer
::link($CFG->wwwroot
.'/'.$CFG->admin
.'/index.php', $CFG->wwwroot
.'/'.$CFG->admin
.'/index.php'));
1254 $html .= html_writer
::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL
;
1257 if (!empty($pluginupdates)) {
1258 $text .= PHP_EOL
. get_string('updateavailableforplugin', 'core_admin') . PHP_EOL
;
1259 $html .= html_writer
::tag('h2', get_string('updateavailableforplugin', 'core_admin')) . PHP_EOL
;
1261 $html .= html_writer
::start_tag('ul') . PHP_EOL
;
1262 foreach ($pluginupdates as $pluginupdate) {
1263 $html .= html_writer
::start_tag('li');
1264 $text .= get_string('pluginname', $pluginupdate->component
);
1265 $html .= html_writer
::tag('strong', get_string('pluginname', $pluginupdate->component
));
1267 $text .= ' ('.$pluginupdate->component
.')';
1268 $html .= ' ('.$pluginupdate->component
.')';
1270 $text .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version
);
1271 $html .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version
);
1274 $html .= html_writer
::end_tag('li') . PHP_EOL
;
1277 $html .= html_writer
::end_tag('ul') . PHP_EOL
;
1279 $a = array('url' => $CFG->wwwroot
.'/'.$CFG->admin
.'/plugins.php');
1280 $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL
;
1281 $a = array('url' => html_writer
::link($CFG->wwwroot
.'/'.$CFG->admin
.'/plugins.php', $CFG->wwwroot
.'/'.$CFG->admin
.'/plugins.php'));
1282 $html .= html_writer
::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL
;
1285 $a = array('siteurl' => $CFG->wwwroot
);
1286 $text .= get_string('updatenotificationfooter', 'core_admin', $a) . PHP_EOL
;
1287 $a = array('siteurl' => html_writer
::link($CFG->wwwroot
, $CFG->wwwroot
));
1288 $html .= html_writer
::tag('footer', html_writer
::tag('p', get_string('updatenotificationfooter', 'core_admin', $a),
1289 array('style' => 'font-size:smaller; color:#333;')));
1291 $mainadmin = reset($admins);
1293 foreach ($admins as $admin) {
1294 $message = new stdClass();
1295 $message->component
= 'moodle';
1296 $message->name
= 'availableupdate';
1297 $message->userfrom
= $mainadmin;
1298 $message->userto
= $admin;
1299 $message->subject
= get_string('updatenotifications', 'core_admin');
1300 $message->fullmessage
= $text;
1301 $message->fullmessageformat
= FORMAT_PLAIN
;
1302 $message->fullmessagehtml
= $html;
1303 $message->smallmessage
= get_string('updatenotifications', 'core_admin');
1304 $message->notification
= 1;
1305 message_send($message);
1310 * Compare two release labels and decide if they are the same
1312 * @param string $remote release info of the available update
1313 * @param null|string $local release info of the local code, defaults to $release defined in version.php
1314 * @return boolean true if the releases declare the same minor+major version
1316 protected function is_same_release($remote, $local=null) {
1318 if (is_null($local)) {
1319 $this->load_current_environment();
1320 $local = $this->currentrelease
;
1323 $pattern = '/^([0-9\.\+]+)([^(]*)/';
1325 preg_match($pattern, $remote, $remotematches);
1326 preg_match($pattern, $local, $localmatches);
1328 $remotematches[1] = str_replace('+', '', $remotematches[1]);
1329 $localmatches[1] = str_replace('+', '', $localmatches[1]);
1331 if ($remotematches[1] === $localmatches[1] and rtrim($remotematches[2]) === rtrim($localmatches[2])) {
1341 * Defines the structure of objects returned by {@link available_update_checker::get_update_info()}
1343 class available_update_info
{
1345 /** @var string frankenstyle component name */
1347 /** @var int the available version of the component */
1349 /** @var string|null optional release name */
1350 public $release = null;
1351 /** @var int|null optional maturity info, eg {@link MATURITY_STABLE} */
1352 public $maturity = null;
1353 /** @var string|null optional URL of a page with more info about the update */
1355 /** @var string|null optional URL of a ZIP package that can be downloaded and installed */
1356 public $download = null;
1359 * Creates new instance of the class
1361 * The $info array must provide at least the 'version' value and optionally all other
1362 * values to populate the object's properties.
1364 * @param string $name the frankenstyle component name
1365 * @param array $info associative array with other properties
1367 public function __construct($name, array $info) {
1368 $this->component
= $name;
1369 foreach ($info as $k => $v) {
1370 if (property_exists('available_update_info', $k) and $k != 'component') {
1379 * Factory class producing required subclasses of {@link plugininfo_base}
1381 class plugininfo_default_factory
{
1384 * Makes a new instance of the plugininfo class
1386 * @param string $type the plugin type, eg. 'mod'
1387 * @param string $typerootdir full path to the location of all the plugins of this type
1388 * @param string $name the plugin name, eg. 'workshop'
1389 * @param string $namerootdir full path to the location of the plugin
1390 * @param string $typeclass the name of class that holds the info about the plugin
1391 * @return plugininfo_base the instance of $typeclass
1393 public static function make($type, $typerootdir, $name, $namerootdir, $typeclass) {
1394 $plugin = new $typeclass();
1395 $plugin->type
= $type;
1396 $plugin->typerootdir
= $typerootdir;
1397 $plugin->name
= $name;
1398 $plugin->rootdir
= $namerootdir;
1400 $plugin->init_display_name();
1401 $plugin->load_disk_version();
1402 $plugin->load_db_version();
1403 $plugin->load_required_main_version();
1404 $plugin->init_is_standard();
1412 * Base class providing access to the information about a plugin
1414 * @property-read string component the component name, type_name
1416 abstract class plugininfo_base
{
1418 /** @var string the plugintype name, eg. mod, auth or workshopform */
1420 /** @var string full path to the location of all the plugins of this type */
1421 public $typerootdir;
1422 /** @var string the plugin name, eg. assignment, ldap */
1424 /** @var string the localized plugin name */
1425 public $displayname;
1426 /** @var string the plugin source, one of plugin_manager::PLUGIN_SOURCE_xxx constants */
1428 /** @var fullpath to the location of this plugin */
1430 /** @var int|string the version of the plugin's source code */
1431 public $versiondisk;
1432 /** @var int|string the version of the installed plugin */
1434 /** @var int|float|string required version of Moodle core */
1435 public $versionrequires;
1436 /** @var array other plugins that this one depends on, lazy-loaded by {@link get_other_required_plugins()} */
1437 public $dependencies;
1438 /** @var int number of instances of the plugin - not supported yet */
1440 /** @var int order of the plugin among other plugins of the same type - not supported yet */
1442 /** @var array|null array of {@link available_update_info} for this plugin */
1443 public $availableupdates;
1446 * Gathers and returns the information about all plugins of the given type
1448 * @param string $type the name of the plugintype, eg. mod, auth or workshopform
1449 * @param string $typerootdir full path to the location of the plugin dir
1450 * @param string $typeclass the name of the actually called class
1451 * @return array of plugintype classes, indexed by the plugin name
1453 public static function get_plugins($type, $typerootdir, $typeclass) {
1455 // get the information about plugins at the disk
1456 $plugins = get_plugin_list($type);
1458 foreach ($plugins as $pluginname => $pluginrootdir) {
1459 $ondisk[$pluginname] = plugininfo_default_factory
::make($type, $typerootdir,
1460 $pluginname, $pluginrootdir, $typeclass);
1466 * Sets {@link $displayname} property to a localized name of the plugin
1468 public function init_display_name() {
1469 if (!get_string_manager()->string_exists('pluginname', $this->component
)) {
1470 $this->displayname
= '[pluginname,' . $this->component
. ']';
1472 $this->displayname
= get_string('pluginname', $this->component
);
1477 * Magic method getter, redirects to read only values.
1479 * @param string $name
1482 public function __get($name) {
1484 case 'component': return $this->type
. '_' . $this->name
;
1487 debugging('Invalid plugin property accessed! '.$name);
1493 * Return the full path name of a file within the plugin.
1495 * No check is made to see if the file exists.
1497 * @param string $relativepath e.g. 'version.php'.
1498 * @return string e.g. $CFG->dirroot . '/mod/quiz/version.php'.
1500 public function full_path($relativepath) {
1501 if (empty($this->rootdir
)) {
1504 return $this->rootdir
. '/' . $relativepath;
1508 * Load the data from version.php.
1510 * @return stdClass the object called $plugin defined in version.php
1512 protected function load_version_php() {
1513 $versionfile = $this->full_path('version.php');
1515 $plugin = new stdClass();
1516 if (is_readable($versionfile)) {
1517 include($versionfile);
1523 * Sets {@link $versiondisk} property to a numerical value representing the
1524 * version of the plugin's source code.
1526 * If the value is null after calling this method, either the plugin
1527 * does not use versioning (typically does not have any database
1528 * data) or is missing from disk.
1530 public function load_disk_version() {
1531 $plugin = $this->load_version_php();
1532 if (isset($plugin->version
)) {
1533 $this->versiondisk
= $plugin->version
;
1538 * Sets {@link $versionrequires} property to a numerical value representing
1539 * the version of Moodle core that this plugin requires.
1541 public function load_required_main_version() {
1542 $plugin = $this->load_version_php();
1543 if (isset($plugin->requires
)) {
1544 $this->versionrequires
= $plugin->requires
;
1549 * Initialise {@link $dependencies} to the list of other plugins (in any)
1550 * that this one requires to be installed.
1552 protected function load_other_required_plugins() {
1553 $plugin = $this->load_version_php();
1554 if (!empty($plugin->dependencies
)) {
1555 $this->dependencies
= $plugin->dependencies
;
1557 $this->dependencies
= array(); // By default, no dependencies.
1562 * Get the list of other plugins that this plugin requires to be installed.
1564 * @return array with keys the frankenstyle plugin name, and values either
1565 * a version string (like '2011101700') or the constant ANY_VERSION.
1567 public function get_other_required_plugins() {
1568 if (is_null($this->dependencies
)) {
1569 $this->load_other_required_plugins();
1571 return $this->dependencies
;
1575 * Sets {@link $versiondb} property to a numerical value representing the
1576 * currently installed version of the plugin.
1578 * If the value is null after calling this method, either the plugin
1579 * does not use versioning (typically does not have any database
1580 * data) or has not been installed yet.
1582 public function load_db_version() {
1583 if ($ver = self
::get_version_from_config_plugins($this->component
)) {
1584 $this->versiondb
= $ver;
1589 * Sets {@link $source} property to one of plugin_manager::PLUGIN_SOURCE_xxx
1592 * If the property's value is null after calling this method, then
1593 * the type of the plugin has not been recognized and you should throw
1596 public function init_is_standard() {
1598 $standard = plugin_manager
::standard_plugins_list($this->type
);
1600 if ($standard !== false) {
1601 $standard = array_flip($standard);
1602 if (isset($standard[$this->name
])) {
1603 $this->source
= plugin_manager
::PLUGIN_SOURCE_STANDARD
;
1604 } else if (!is_null($this->versiondb
) and is_null($this->versiondisk
)
1605 and plugin_manager
::is_deleted_standard_plugin($this->type
, $this->name
)) {
1606 $this->source
= plugin_manager
::PLUGIN_SOURCE_STANDARD
; // to be deleted
1608 $this->source
= plugin_manager
::PLUGIN_SOURCE_EXTENSION
;
1614 * Returns true if the plugin is shipped with the official distribution
1615 * of the current Moodle version, false otherwise.
1619 public function is_standard() {
1620 return $this->source
=== plugin_manager
::PLUGIN_SOURCE_STANDARD
;
1624 * Returns true if the the given Moodle version is enough to run this plugin
1626 * @param string|int|double $moodleversion
1629 public function is_core_dependency_satisfied($moodleversion) {
1631 if (empty($this->versionrequires
)) {
1635 return (double)$this->versionrequires
<= (double)$moodleversion;
1640 * Returns the status of the plugin
1642 * @return string one of plugin_manager::PLUGIN_STATUS_xxx constants
1644 public function get_status() {
1646 if (is_null($this->versiondb
) and is_null($this->versiondisk
)) {
1647 return plugin_manager
::PLUGIN_STATUS_NODB
;
1649 } else if (is_null($this->versiondb
) and !is_null($this->versiondisk
)) {
1650 return plugin_manager
::PLUGIN_STATUS_NEW
;
1652 } else if (!is_null($this->versiondb
) and is_null($this->versiondisk
)) {
1653 if (plugin_manager
::is_deleted_standard_plugin($this->type
, $this->name
)) {
1654 return plugin_manager
::PLUGIN_STATUS_DELETE
;
1656 return plugin_manager
::PLUGIN_STATUS_MISSING
;
1659 } else if ((string)$this->versiondb
=== (string)$this->versiondisk
) {
1660 return plugin_manager
::PLUGIN_STATUS_UPTODATE
;
1662 } else if ($this->versiondb
< $this->versiondisk
) {
1663 return plugin_manager
::PLUGIN_STATUS_UPGRADE
;
1665 } else if ($this->versiondb
> $this->versiondisk
) {
1666 return plugin_manager
::PLUGIN_STATUS_DOWNGRADE
;
1669 // $version = pi(); and similar funny jokes - hopefully Donald E. Knuth will never contribute to Moodle ;-)
1670 throw new coding_exception('Unable to determine plugin state, check the plugin versions');
1675 * Returns the information about plugin availability
1677 * True means that the plugin is enabled. False means that the plugin is
1678 * disabled. Null means that the information is not available, or the
1679 * plugin does not support configurable availability or the availability
1680 * can not be changed.
1684 public function is_enabled() {
1689 * Populates the property {@link $availableupdates} with the information provided by
1690 * available update checker
1692 * @param available_update_checker $provider the class providing the available update info
1694 public function check_available_updates(available_update_checker
$provider) {
1697 if (isset($CFG->updateminmaturity
)) {
1698 $minmaturity = $CFG->updateminmaturity
;
1700 // this can happen during the very first upgrade to 2.3
1701 $minmaturity = MATURITY_STABLE
;
1704 $this->availableupdates
= $provider->get_update_info($this->component
,
1705 array('minmaturity' => $minmaturity));
1709 * If there are updates for this plugin available, returns them.
1711 * Returns array of {@link available_update_info} objects, if some update
1712 * is available. Returns null if there is no update available or if the update
1713 * availability is unknown.
1715 * @return array|null
1717 public function available_updates() {
1719 if (empty($this->availableupdates
) or !is_array($this->availableupdates
)) {
1725 foreach ($this->availableupdates
as $availableupdate) {
1726 if ($availableupdate->version
> $this->versiondisk
) {
1727 $updates[] = $availableupdate;
1731 if (empty($updates)) {
1739 * Returns the URL of the plugin settings screen
1741 * Null value means that the plugin either does not have the settings screen
1742 * or its location is not available via this library.
1744 * @return null|moodle_url
1746 public function get_settings_url() {
1751 * Returns the URL of the screen where this plugin can be uninstalled
1753 * Visiting that URL must be safe, that is a manual confirmation is needed
1754 * for actual uninstallation of the plugin. Null value means that the
1755 * plugin either does not support uninstallation, or does not require any
1756 * database cleanup or the location of the screen is not available via this
1759 * @return null|moodle_url
1761 public function get_uninstall_url() {
1766 * Returns relative directory of the plugin with heading '/'
1770 public function get_dir() {
1773 return substr($this->rootdir
, strlen($CFG->dirroot
));
1777 * Provides access to plugin versions from {config_plugins}
1779 * @param string $plugin plugin name
1780 * @param double $disablecache optional, defaults to false
1781 * @return int|false the stored value or false if not found
1783 protected function get_version_from_config_plugins($plugin, $disablecache=false) {
1785 static $pluginversions = null;
1787 if (is_null($pluginversions) or $disablecache) {
1789 $pluginversions = $DB->get_records_menu('config_plugins', array('name' => 'version'), 'plugin', 'plugin,value');
1790 } catch (dml_exception
$e) {
1792 $pluginversions = array();
1796 if (!array_key_exists($plugin, $pluginversions)) {
1800 return $pluginversions[$plugin];
1806 * General class for all plugin types that do not have their own class
1808 class plugininfo_general
extends plugininfo_base
{
1813 * Class for page side blocks
1815 class plugininfo_block
extends plugininfo_base
{
1817 public static function get_plugins($type, $typerootdir, $typeclass) {
1819 // get the information about blocks at the disk
1820 $blocks = parent
::get_plugins($type, $typerootdir, $typeclass);
1822 // add blocks missing from disk
1823 $blocksinfo = self
::get_blocks_info();
1824 foreach ($blocksinfo as $blockname => $blockinfo) {
1825 if (isset($blocks[$blockname])) {
1828 $plugin = new $typeclass();
1829 $plugin->type
= $type;
1830 $plugin->typerootdir
= $typerootdir;
1831 $plugin->name
= $blockname;
1832 $plugin->rootdir
= null;
1833 $plugin->displayname
= $blockname;
1834 $plugin->versiondb
= $blockinfo->version
;
1835 $plugin->init_is_standard();
1837 $blocks[$blockname] = $plugin;
1843 public function init_display_name() {
1845 if (get_string_manager()->string_exists('pluginname', 'block_' . $this->name
)) {
1846 $this->displayname
= get_string('pluginname', 'block_' . $this->name
);
1848 } else if (($block = block_instance($this->name
)) !== false) {
1849 $this->displayname
= $block->get_title();
1852 parent
::init_display_name();
1856 public function load_db_version() {
1859 $blocksinfo = self
::get_blocks_info();
1860 if (isset($blocksinfo[$this->name
]->version
)) {
1861 $this->versiondb
= $blocksinfo[$this->name
]->version
;
1865 public function is_enabled() {
1867 $blocksinfo = self
::get_blocks_info();
1868 if (isset($blocksinfo[$this->name
]->visible
)) {
1869 if ($blocksinfo[$this->name
]->visible
) {
1875 return parent
::is_enabled();
1879 public function get_settings_url() {
1881 if (($block = block_instance($this->name
)) === false) {
1882 return parent
::get_settings_url();
1884 } else if ($block->has_config()) {
1885 if (file_exists($this->full_path('settings.php'))) {
1886 return new moodle_url('/admin/settings.php', array('section' => 'blocksetting' . $this->name
));
1888 $blocksinfo = self
::get_blocks_info();
1889 return new moodle_url('/admin/block.php', array('block' => $blocksinfo[$this->name
]->id
));
1893 return parent
::get_settings_url();
1897 public function get_uninstall_url() {
1899 $blocksinfo = self
::get_blocks_info();
1900 return new moodle_url('/admin/blocks.php', array('delete' => $blocksinfo[$this->name
]->id
, 'sesskey' => sesskey()));
1904 * Provides access to the records in {block} table
1906 * @param bool $disablecache do not use internal static cache
1907 * @return array array of stdClasses
1909 protected static function get_blocks_info($disablecache=false) {
1911 static $blocksinfocache = null;
1913 if (is_null($blocksinfocache) or $disablecache) {
1915 $blocksinfocache = $DB->get_records('block', null, 'name', 'name,id,version,visible');
1916 } catch (dml_exception
$e) {
1918 $blocksinfocache = array();
1922 return $blocksinfocache;
1928 * Class for text filters
1930 class plugininfo_filter
extends plugininfo_base
{
1932 public static function get_plugins($type, $typerootdir, $typeclass) {
1937 // get the list of filters from both /filter and /mod location
1938 $installed = filter_get_all_installed();
1940 foreach ($installed as $filterlegacyname => $displayname) {
1941 $plugin = new $typeclass();
1942 $plugin->type
= $type;
1943 $plugin->typerootdir
= $typerootdir;
1944 $plugin->name
= self
::normalize_legacy_name($filterlegacyname);
1945 $plugin->rootdir
= $CFG->dirroot
. '/' . $filterlegacyname;
1946 $plugin->displayname
= $displayname;
1948 $plugin->load_disk_version();
1949 $plugin->load_db_version();
1950 $plugin->load_required_main_version();
1951 $plugin->init_is_standard();
1953 $filters[$plugin->name
] = $plugin;
1956 $globalstates = self
::get_global_states();
1958 if ($DB->get_manager()->table_exists('filter_active')) {
1959 // if we're upgrading from 1.9, the table does not exist yet
1960 // if it does, make sure that all installed filters are registered
1961 $needsreload = false;
1962 foreach (array_keys($installed) as $filterlegacyname) {
1963 if (!isset($globalstates[self
::normalize_legacy_name($filterlegacyname)])) {
1964 filter_set_global_state($filterlegacyname, TEXTFILTER_DISABLED
);
1965 $needsreload = true;
1969 $globalstates = self
::get_global_states(true);
1973 // make sure that all registered filters are installed, just in case
1974 foreach ($globalstates as $name => $info) {
1975 if (!isset($filters[$name])) {
1976 // oops, there is a record in filter_active but the filter is not installed
1977 $plugin = new $typeclass();
1978 $plugin->type
= $type;
1979 $plugin->typerootdir
= $typerootdir;
1980 $plugin->name
= $name;
1981 $plugin->rootdir
= $CFG->dirroot
. '/' . $info->legacyname
;
1982 $plugin->displayname
= $info->legacyname
;
1984 $plugin->load_db_version();
1986 if (is_null($plugin->versiondb
)) {
1987 // this is a hack to stimulate 'Missing from disk' error
1988 // because $plugin->versiondisk will be null !== false
1989 $plugin->versiondb
= false;
1992 $filters[$plugin->name
] = $plugin;
1999 public function init_display_name() {
2000 // do nothing, the name is set in self::get_plugins()
2004 * @see load_version_php()
2006 protected function load_version_php() {
2007 if (strpos($this->name
, 'mod_') === 0) {
2008 // filters bundled with modules do not have a version.php and so
2009 // do not provide their own versioning information.
2010 return new stdClass();
2012 return parent
::load_version_php();
2015 public function is_enabled() {
2017 $globalstates = self
::get_global_states();
2019 foreach ($globalstates as $filterlegacyname => $info) {
2020 $name = self
::normalize_legacy_name($filterlegacyname);
2021 if ($name === $this->name
) {
2022 if ($info->active
== TEXTFILTER_DISABLED
) {
2025 // it may be 'On' or 'Off, but available'
2034 public function get_settings_url() {
2036 $globalstates = self
::get_global_states();
2037 $legacyname = $globalstates[$this->name
]->legacyname
;
2038 if (filter_has_global_settings($legacyname)) {
2039 return new moodle_url('/admin/settings.php', array('section' => 'filtersetting' . str_replace('/', '', $legacyname)));
2045 public function get_uninstall_url() {
2047 if (strpos($this->name
, 'mod_') === 0) {
2050 $globalstates = self
::get_global_states();
2051 $legacyname = $globalstates[$this->name
]->legacyname
;
2052 return new moodle_url('/admin/filters.php', array('sesskey' => sesskey(), 'filterpath' => $legacyname, 'action' => 'delete'));
2057 * Convert legacy filter names like 'filter/foo' or 'mod/bar' into frankenstyle
2059 * @param string $legacyfiltername legacy filter name
2060 * @return string frankenstyle-like name
2062 protected static function normalize_legacy_name($legacyfiltername) {
2064 $name = str_replace('/', '_', $legacyfiltername);
2065 if (strpos($name, 'filter_') === 0) {
2066 $name = substr($name, 7);
2068 throw new coding_exception('Unable to determine filter name: ' . $legacyfiltername);
2076 * Provides access to the results of {@link filter_get_global_states()}
2077 * but indexed by the normalized filter name
2079 * The legacy filter name is available as ->legacyname property.
2081 * @param bool $disablecache
2084 protected static function get_global_states($disablecache=false) {
2086 static $globalstatescache = null;
2088 if ($disablecache or is_null($globalstatescache)) {
2090 if (!$DB->get_manager()->table_exists('filter_active')) {
2091 // we're upgrading from 1.9 and the table used by {@link filter_get_global_states()}
2092 // does not exist yet
2093 $globalstatescache = array();
2096 foreach (filter_get_global_states() as $legacyname => $info) {
2097 $name = self
::normalize_legacy_name($legacyname);
2098 $filterinfo = new stdClass();
2099 $filterinfo->legacyname
= $legacyname;
2100 $filterinfo->active
= $info->active
;
2101 $filterinfo->sortorder
= $info->sortorder
;
2102 $globalstatescache[$name] = $filterinfo;
2107 return $globalstatescache;
2113 * Class for activity modules
2115 class plugininfo_mod
extends plugininfo_base
{
2117 public static function get_plugins($type, $typerootdir, $typeclass) {
2119 // get the information about plugins at the disk
2120 $modules = parent
::get_plugins($type, $typerootdir, $typeclass);
2122 // add modules missing from disk
2123 $modulesinfo = self
::get_modules_info();
2124 foreach ($modulesinfo as $modulename => $moduleinfo) {
2125 if (isset($modules[$modulename])) {
2128 $plugin = new $typeclass();
2129 $plugin->type
= $type;
2130 $plugin->typerootdir
= $typerootdir;
2131 $plugin->name
= $modulename;
2132 $plugin->rootdir
= null;
2133 $plugin->displayname
= $modulename;
2134 $plugin->versiondb
= $moduleinfo->version
;
2135 $plugin->init_is_standard();
2137 $modules[$modulename] = $plugin;
2143 public function init_display_name() {
2144 if (get_string_manager()->string_exists('pluginname', $this->component
)) {
2145 $this->displayname
= get_string('pluginname', $this->component
);
2147 $this->displayname
= get_string('modulename', $this->component
);
2152 * Load the data from version.php.
2153 * @return object the data object defined in version.php.
2155 protected function load_version_php() {
2156 $versionfile = $this->full_path('version.php');
2158 $module = new stdClass();
2159 if (is_readable($versionfile)) {
2160 include($versionfile);
2165 public function load_db_version() {
2168 $modulesinfo = self
::get_modules_info();
2169 if (isset($modulesinfo[$this->name
]->version
)) {
2170 $this->versiondb
= $modulesinfo[$this->name
]->version
;
2174 public function is_enabled() {
2176 $modulesinfo = self
::get_modules_info();
2177 if (isset($modulesinfo[$this->name
]->visible
)) {
2178 if ($modulesinfo[$this->name
]->visible
) {
2184 return parent
::is_enabled();
2188 public function get_settings_url() {
2190 if (file_exists($this->full_path('settings.php')) or file_exists($this->full_path('settingstree.php'))) {
2191 return new moodle_url('/admin/settings.php', array('section' => 'modsetting' . $this->name
));
2193 return parent
::get_settings_url();
2197 public function get_uninstall_url() {
2199 if ($this->name
!== 'forum') {
2200 return new moodle_url('/admin/modules.php', array('delete' => $this->name
, 'sesskey' => sesskey()));
2207 * Provides access to the records in {modules} table
2209 * @param bool $disablecache do not use internal static cache
2210 * @return array array of stdClasses
2212 protected static function get_modules_info($disablecache=false) {
2214 static $modulesinfocache = null;
2216 if (is_null($modulesinfocache) or $disablecache) {
2218 $modulesinfocache = $DB->get_records('modules', null, 'name', 'name,id,version,visible');
2219 } catch (dml_exception
$e) {
2221 $modulesinfocache = array();
2225 return $modulesinfocache;
2231 * Class for question behaviours.
2233 class plugininfo_qbehaviour
extends plugininfo_base
{
2235 public function get_uninstall_url() {
2236 return new moodle_url('/admin/qbehaviours.php',
2237 array('delete' => $this->name
, 'sesskey' => sesskey()));
2243 * Class for question types
2245 class plugininfo_qtype
extends plugininfo_base
{
2247 public function get_uninstall_url() {
2248 return new moodle_url('/admin/qtypes.php',
2249 array('delete' => $this->name
, 'sesskey' => sesskey()));
2255 * Class for authentication plugins
2257 class plugininfo_auth
extends plugininfo_base
{
2259 public function is_enabled() {
2261 /** @var null|array list of enabled authentication plugins */
2262 static $enabled = null;
2264 if (in_array($this->name
, array('nologin', 'manual'))) {
2265 // these two are always enabled and can't be disabled
2269 if (is_null($enabled)) {
2270 $enabled = array_flip(explode(',', $CFG->auth
));
2273 return isset($enabled[$this->name
]);
2276 public function get_settings_url() {
2277 if (file_exists($this->full_path('settings.php'))) {
2278 return new moodle_url('/admin/settings.php', array('section' => 'authsetting' . $this->name
));
2280 return new moodle_url('/admin/auth_config.php', array('auth' => $this->name
));
2287 * Class for enrolment plugins
2289 class plugininfo_enrol
extends plugininfo_base
{
2291 public function is_enabled() {
2293 /** @var null|array list of enabled enrolment plugins */
2294 static $enabled = null;
2296 // We do not actually need whole enrolment classes here so we do not call
2297 // {@link enrol_get_plugins()}. Note that this may produce slightly different
2298 // results, for example if the enrolment plugin does not contain lib.php
2299 // but it is listed in $CFG->enrol_plugins_enabled
2301 if (is_null($enabled)) {
2302 $enabled = array_flip(explode(',', $CFG->enrol_plugins_enabled
));
2305 return isset($enabled[$this->name
]);
2308 public function get_settings_url() {
2310 if ($this->is_enabled() or file_exists($this->full_path('settings.php'))) {
2311 return new moodle_url('/admin/settings.php', array('section' => 'enrolsettings' . $this->name
));
2313 return parent
::get_settings_url();
2317 public function get_uninstall_url() {
2318 return new moodle_url('/admin/enrol.php', array('action' => 'uninstall', 'enrol' => $this->name
, 'sesskey' => sesskey()));
2324 * Class for messaging processors
2326 class plugininfo_message
extends plugininfo_base
{
2328 public function get_settings_url() {
2329 $processors = get_message_processors();
2330 if (isset($processors[$this->name
])) {
2331 $processor = $processors[$this->name
];
2332 if ($processor->available
&& $processor->hassettings
) {
2333 return new moodle_url('settings.php', array('section' => 'messagesetting'.$processor->name
));
2336 return parent
::get_settings_url();
2340 * @see plugintype_interface::is_enabled()
2342 public function is_enabled() {
2343 $processors = get_message_processors();
2344 if (isset($processors[$this->name
])) {
2345 return $processors[$this->name
]->configured
&& $processors[$this->name
]->enabled
;
2347 return parent
::is_enabled();
2352 * @see plugintype_interface::get_uninstall_url()
2354 public function get_uninstall_url() {
2355 $processors = get_message_processors();
2356 if (isset($processors[$this->name
])) {
2357 return new moodle_url('message.php', array('uninstall' => $processors[$this->name
]->id
, 'sesskey' => sesskey()));
2359 return parent
::get_uninstall_url();
2366 * Class for repositories
2368 class plugininfo_repository
extends plugininfo_base
{
2370 public function is_enabled() {
2372 $enabled = self
::get_enabled_repositories();
2374 return isset($enabled[$this->name
]);
2377 public function get_settings_url() {
2379 if ($this->is_enabled()) {
2380 return new moodle_url('/admin/repository.php', array('sesskey' => sesskey(), 'action' => 'edit', 'repos' => $this->name
));
2382 return parent
::get_settings_url();
2387 * Provides access to the records in {repository} table
2389 * @param bool $disablecache do not use internal static cache
2390 * @return array array of stdClasses
2392 protected static function get_enabled_repositories($disablecache=false) {
2394 static $repositories = null;
2396 if (is_null($repositories) or $disablecache) {
2397 $repositories = $DB->get_records('repository', null, 'type', 'type,visible,sortorder');
2400 return $repositories;
2406 * Class for portfolios
2408 class plugininfo_portfolio
extends plugininfo_base
{
2410 public function is_enabled() {
2412 $enabled = self
::get_enabled_portfolios();
2414 return isset($enabled[$this->name
]);
2418 * Provides access to the records in {portfolio_instance} table
2420 * @param bool $disablecache do not use internal static cache
2421 * @return array array of stdClasses
2423 protected static function get_enabled_portfolios($disablecache=false) {
2425 static $portfolios = null;
2427 if (is_null($portfolios) or $disablecache) {
2428 $portfolios = array();
2429 $instances = $DB->get_recordset('portfolio_instance', null, 'plugin');
2430 foreach ($instances as $instance) {
2431 if (isset($portfolios[$instance->plugin
])) {
2432 if ($instance->visible
) {
2433 $portfolios[$instance->plugin
]->visible
= $instance->visible
;
2436 $portfolios[$instance->plugin
] = $instance;
2449 class plugininfo_theme
extends plugininfo_base
{
2451 public function is_enabled() {
2454 if ((!empty($CFG->theme
) and $CFG->theme
=== $this->name
) or
2455 (!empty($CFG->themelegacy
) and $CFG->themelegacy
=== $this->name
)) {
2458 return parent
::is_enabled();
2465 * Class representing an MNet service
2467 class plugininfo_mnetservice
extends plugininfo_base
{
2469 public function is_enabled() {
2472 if (empty($CFG->mnet_dispatcher_mode
) ||
$CFG->mnet_dispatcher_mode
!== 'strict') {
2475 return parent
::is_enabled();
2482 * Class for admin tool plugins
2484 class plugininfo_tool
extends plugininfo_base
{
2486 public function get_uninstall_url() {
2487 return new moodle_url('/admin/tools.php', array('delete' => $this->name
, 'sesskey' => sesskey()));
2493 * Class for admin tool plugins
2495 class plugininfo_report
extends plugininfo_base
{
2497 public function get_uninstall_url() {
2498 return new moodle_url('/admin/reports.php', array('delete' => $this->name
, 'sesskey' => sesskey()));