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. Used by install and upgrade.
284 * @param int $moodleversion the version from version.php.
285 * @return bool true if all the dependencies are satisfied for all plugins.
287 public function all_plugins_ok($moodleversion) {
288 foreach ($this->get_plugins() as $type => $plugins) {
289 foreach ($plugins as $plugin) {
291 if (!empty($plugin->versionrequires
) && $plugin->versionrequires
> $moodleversion) {
295 if (!$this->are_dependencies_satisfied($plugin->get_other_required_plugins())) {
305 * Checks if there are some plugins with a known available update
307 * @return bool true if there is at least one available update
309 public function some_plugins_updatable() {
310 foreach ($this->get_plugins() as $type => $plugins) {
311 foreach ($plugins as $plugin) {
312 if ($plugin->available_updates()) {
322 * Defines a list of all plugins that were originally shipped in the standard Moodle distribution,
323 * but are not anymore and are deleted during upgrades.
325 * The main purpose of this list is to hide missing plugins during upgrade.
327 * @param string $type plugin type
328 * @param string $name plugin name
331 public static function is_deleted_standard_plugin($type, $name) {
332 static $plugins = array(
333 // do not add 1.9-2.2 plugin removals here
336 if (!isset($plugins[$type])) {
339 return in_array($name, $plugins[$type]);
343 * Defines a white list of all plugins shipped in the standard Moodle distribution
345 * @param string $type
346 * @return false|array array of standard plugins or false if the type is unknown
348 public static function standard_plugins_list($type) {
349 static $standard_plugins = array(
351 'assignment' => array(
352 'offline', 'online', 'upload', 'uploadsingle'
355 'assignsubmission' => array(
356 'comments', 'file', 'onlinetext'
359 'assignfeedback' => array(
364 'cas', 'db', 'email', 'fc', 'imap', 'ldap', 'manual', 'mnet',
365 'nntp', 'nologin', 'none', 'pam', 'pop3', 'radius',
366 'shibboleth', 'webservice'
370 'activity_modules', 'admin_bookmarks', 'blog_menu',
371 'blog_recent', 'blog_tags', 'calendar_month',
372 'calendar_upcoming', 'comments', 'community',
373 'completionstatus', 'course_list', 'course_overview',
374 'course_summary', 'feedback', 'glossary_random', 'html',
375 'login', 'mentees', 'messages', 'mnet_hosts', 'myprofile',
376 'navigation', 'news_items', 'online_users', 'participants',
377 'private_files', 'quiz_results', 'recent_activity',
378 'rss_client', 'search_forums', 'section_links',
379 'selfcompletion', 'settings', 'site_main_menu',
380 'social_activities', 'tag_flickr', 'tag_youtube', 'tags'
384 'exportimscp', 'importhtml', 'print'
387 'coursereport' => array(
391 'datafield' => array(
392 'checkbox', 'date', 'file', 'latlong', 'menu', 'multimenu',
393 'number', 'picture', 'radiobutton', 'text', 'textarea', 'url'
396 'datapreset' => array(
401 'textarea', 'tinymce'
405 'authorize', 'category', 'cohort', 'database', 'flatfile',
406 'guest', 'imsenterprise', 'ldap', 'manual', 'meta', 'mnet',
411 'activitynames', 'algebra', 'censor', 'emailprotect',
412 'emoticon', 'mediaplugin', 'multilang', 'tex', 'tidy',
413 'urltolink', 'data', 'glossary'
417 'scorm', 'social', 'topics', 'weeks'
420 'gradeexport' => array(
421 'ods', 'txt', 'xls', 'xml'
424 'gradeimport' => array(
428 'gradereport' => array(
429 'grader', 'outcomes', 'overview', 'user'
432 'gradingform' => array(
440 'email', 'jabber', 'popup'
443 'mnetservice' => array(
448 'assign', 'assignment', 'book', 'chat', 'choice', 'data', 'feedback', 'folder',
449 'forum', 'glossary', 'imscp', 'label', 'lesson', 'lti', 'page',
450 'quiz', 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop'
453 'plagiarism' => array(
456 'portfolio' => array(
457 'boxnet', 'download', 'flickr', 'googledocs', 'mahara', 'picasa'
460 'profilefield' => array(
461 'checkbox', 'datetime', 'menu', 'text', 'textarea'
464 'qbehaviour' => array(
465 'adaptive', 'adaptivenopenalty', 'deferredcbm',
466 'deferredfeedback', 'immediatecbm', 'immediatefeedback',
467 'informationitem', 'interactive', 'interactivecountback',
468 'manualgraded', 'missing'
472 'aiken', 'blackboard', 'blackboard_six', 'examview', 'gift',
473 'learnwise', 'missingword', 'multianswer', 'webct',
478 'calculated', 'calculatedmulti', 'calculatedsimple',
479 'description', 'essay', 'match', 'missingtype', 'multianswer',
480 'multichoice', 'numerical', 'random', 'randomsamatch',
481 'shortanswer', 'truefalse'
485 'grading', 'overview', 'responses', 'statistics'
488 'quizaccess' => array(
489 'delaybetweenattempts', 'ipaddress', 'numattempts', 'openclosedate',
490 'password', 'safebrowser', 'securewindow', 'timelimit'
494 'backups', 'completion', 'configlog', 'courseoverview',
495 'log', 'loglive', 'outline', 'participation', 'progress', 'questioninstances', 'security', 'stats'
498 'repository' => array(
499 'alfresco', 'boxnet', 'coursefiles', 'dropbox', 'filesystem',
500 'flickr', 'flickr_public', 'googledocs', 'local', 'merlot',
501 'picasa', 'recent', 's3', 'upload', 'url', 'user', 'webdav',
502 'wikimedia', 'youtube'
505 'scormreport' => array(
512 'afterburner', 'anomaly', 'arialist', 'base', 'binarius',
513 'boxxie', 'brick', 'canvas', 'formal_white', 'formfactor',
514 'fusion', 'leatherbound', 'magazine', 'mymobile', 'nimble',
515 'nonzero', 'overlay', 'serenity', 'sky_high', 'splash',
516 'standard', 'standardold'
520 'assignmentupgrade', 'bloglevelupgrade', 'capability', 'customlang', 'dbtransfer', 'generator',
521 'health', 'innodb', 'langimport', 'multilangupgrade', 'phpunit', 'profiling',
522 'qeupgradehelper', 'replace', 'spamcleaner', 'timezoneimport', 'unittest',
523 'uploaduser', 'unsuproles', 'xmldb'
526 'webservice' => array(
527 'amf', 'rest', 'soap', 'xmlrpc'
530 'workshopallocation' => array(
531 'manual', 'random', 'scheduled'
534 'workshopeval' => array(
538 'workshopform' => array(
539 'accumulative', 'comments', 'numerrors', 'rubric'
543 if (isset($standard_plugins[$type])) {
544 return $standard_plugins[$type];
551 * Reorders plugin types into a sequence to be displayed
553 * For technical reasons, plugin types returned by {@link get_plugin_types()} are
554 * in a certain order that does not need to fit the expected order for the display.
555 * Particularly, activity modules should be displayed first as they represent the
556 * real heart of Moodle. They should be followed by other plugin types that are
557 * used to build the courses (as that is what one expects from LMS). After that,
558 * other supportive plugin types follow.
560 * @param array $types associative array
561 * @return array same array with altered order of items
563 protected function reorder_plugin_types(array $types) {
565 'mod' => $types['mod'],
566 'block' => $types['block'],
567 'qtype' => $types['qtype'],
568 'qbehaviour' => $types['qbehaviour'],
569 'qformat' => $types['qformat'],
570 'filter' => $types['filter'],
571 'enrol' => $types['enrol'],
573 foreach ($types as $type => $path) {
574 if (!isset($fix[$type])) {
584 * General exception thrown by the {@link available_update_checker} class
586 class available_update_checker_exception
extends moodle_exception
{
589 * @param string $errorcode exception description identifier
590 * @param mixed $debuginfo debugging data to display
592 public function __construct($errorcode, $debuginfo=null) {
593 parent
::__construct($errorcode, 'core_plugin', '', null, print_r($debuginfo, true));
599 * Singleton class that handles checking for available updates
601 class available_update_checker
{
603 /** @var available_update_checker holds the singleton instance */
604 protected static $singletoninstance;
605 /** @var null|int the timestamp of when the most recent response was fetched */
606 protected $recentfetch = null;
607 /** @var null|array the recent response from the update notification provider */
608 protected $recentresponse = null;
609 /** @var null|string the numerical version of the local Moodle code */
610 protected $currentversion = null;
611 /** @var null|string the release info of the local Moodle code */
612 protected $currentrelease = null;
613 /** @var null|string branch of the local Moodle code */
614 protected $currentbranch = null;
615 /** @var array of (string)frankestyle => (string)version list of additional plugins deployed at this site */
616 protected $currentplugins = array();
619 * Direct initiation not allowed, use the factory method {@link self::instance()}
621 protected function __construct() {
625 * Sorry, this is singleton
627 protected function __clone() {
631 * Factory method for this class
633 * @return available_update_checker the singleton instance
635 public static function instance() {
636 if (is_null(self
::$singletoninstance)) {
637 self
::$singletoninstance = new self();
639 return self
::$singletoninstance;
643 * Returns the timestamp of the last execution of {@link fetch()}
645 * @return int|null null if it has never been executed or we don't known
647 public function get_last_timefetched() {
649 $this->restore_response();
651 if (!empty($this->recentfetch
)) {
652 return $this->recentfetch
;
660 * Fetches the available update status from the remote site
662 * @throws available_update_checker_exception
664 public function fetch() {
665 $response = $this->get_response();
666 $this->validate_response($response);
667 $this->store_response($response);
671 * Returns the available update information for the given component
673 * This method returns null if the most recent response does not contain any information
674 * about it. The returned structure is an array of available updates for the given
675 * component. Each update info is an object with at least one property called
676 * 'version'. Other possible properties are 'release', 'maturity', 'url' and 'downloadurl'.
678 * For the 'core' component, the method returns real updates only (those with higher version).
679 * For all other components, the list of all known remote updates is returned and the caller
680 * (usually the {@link plugin_manager}) is supposed to make the actual comparison of versions.
682 * @param string $component frankenstyle
683 * @param array $options with supported keys 'minmaturity' and/or 'notifybuilds'
684 * @return null|array null or array of available_update_info objects
686 public function get_update_info($component, array $options = array()) {
688 if (!isset($options['minmaturity'])) {
689 $options['minmaturity'] = 0;
692 if (!isset($options['notifybuilds'])) {
693 $options['notifybuilds'] = false;
696 if ($component == 'core') {
697 $this->load_current_environment();
700 $this->restore_response();
702 if (empty($this->recentresponse
['updates'][$component])) {
707 foreach ($this->recentresponse
['updates'][$component] as $info) {
708 $update = new available_update_info($component, $info);
709 if (isset($update->maturity
) and ($update->maturity
< $options['minmaturity'])) {
712 if ($component == 'core') {
713 if ($update->version
<= $this->currentversion
) {
716 if (empty($options['notifybuilds']) and $this->is_same_release($update->release
)) {
720 $updates[] = $update;
723 if (empty($updates)) {
731 * The method being run via cron.php
733 public function cron() {
736 if (!$this->cron_autocheck_enabled()) {
737 $this->cron_mtrace('Automatic check for available updates not enabled, skipping.');
741 $now = $this->cron_current_timestamp();
743 if ($this->cron_has_fresh_fetch($now)) {
744 $this->cron_mtrace('Recently fetched info about available updates is still fresh enough, skipping.');
748 if ($this->cron_has_outdated_fetch($now)) {
749 $this->cron_mtrace('Outdated or missing info about available updates, forced fetching ... ', '');
750 $this->cron_execute();
754 $offset = $this->cron_execution_offset();
755 $start = mktime(1, 0, 0, date('n', $now), date('j', $now), date('Y', $now)); // 01:00 AM today local time
756 if ($now > $start +
$offset) {
757 $this->cron_mtrace('Regular daily check for available updates ... ', '');
758 $this->cron_execute();
763 /// end of public API //////////////////////////////////////////////////////
766 * Makes cURL request to get data from the remote site
768 * @return string raw request result
769 * @throws available_update_checker_exception
771 protected function get_response() {
772 $curl = new curl(array('proxy' => true));
773 $response = $curl->post($this->prepare_request_url(), $this->prepare_request_params());
774 $curlinfo = $curl->get_info();
775 if ($curlinfo['http_code'] != 200) {
776 throw new available_update_checker_exception('err_response_http_code', $curlinfo['http_code']);
782 * Makes sure the response is valid, has correct API format etc.
784 * @param string $response raw response as returned by the {@link self::get_response()}
785 * @throws available_update_checker_exception
787 protected function validate_response($response) {
789 $response = $this->decode_response($response);
791 if (empty($response)) {
792 throw new available_update_checker_exception('err_response_empty');
795 if (empty($response['status']) or $response['status'] !== 'OK') {
796 throw new available_update_checker_exception('err_response_status', $response['status']);
799 if (empty($response['apiver']) or $response['apiver'] !== '1.0') {
800 throw new available_update_checker_exception('err_response_format_version', $response['apiver']);
803 if (empty($response['forbranch']) or $response['forbranch'] !== moodle_major_version(true)) {
804 throw new available_update_checker_exception('err_response_target_version', $response['target']);
809 * Decodes the raw string response from the update notifications provider
811 * @param string $response as returned by {@link self::get_response()}
812 * @return array decoded response structure
814 protected function decode_response($response) {
815 return json_decode($response, true);
819 * Stores the valid fetched response for later usage
821 * This implementation uses the config_plugins table as the permanent storage.
823 * @param string $response raw valid data returned by {@link self::get_response()}
825 protected function store_response($response) {
827 set_config('recentfetch', time(), 'core_plugin');
828 set_config('recentresponse', $response, 'core_plugin');
830 $this->restore_response(true);
834 * Loads the most recent raw response record we have fetched
836 * This implementation uses the config_plugins table as the permanent storage.
838 * @param bool $forcereload reload even if it was already loaded
840 protected function restore_response($forcereload = false) {
842 if (!$forcereload and !is_null($this->recentresponse
)) {
843 // we already have it, nothing to do
847 $config = get_config('core_plugin');
849 if (!empty($config->recentresponse
) and !empty($config->recentfetch
)) {
851 $this->validate_response($config->recentresponse
);
852 $this->recentfetch
= $config->recentfetch
;
853 $this->recentresponse
= $this->decode_response($config->recentresponse
);
854 } catch (available_update_checker_exception
$e) {
855 // do not set recentresponse if the validation fails
859 $this->recentresponse
= array();
864 * Compares two raw {@link $recentresponse} records and returns the list of changed updates
866 * This method is used to populate potential update info to be sent to site admins.
870 * @throws available_update_checker_exception
871 * @return array parts of $new['updates'] that have changed
873 protected function compare_responses(array $old, array $new) {
879 if (!array_key_exists('updates', $new)) {
880 throw new available_update_checker_exception('err_response_format');
884 return $new['updates'];
887 if (!array_key_exists('updates', $old)) {
888 throw new available_update_checker_exception('err_response_format');
893 foreach ($new['updates'] as $newcomponent => $newcomponentupdates) {
894 if (empty($old['updates'][$newcomponent])) {
895 $changes[$newcomponent] = $newcomponentupdates;
898 foreach ($newcomponentupdates as $newcomponentupdate) {
900 foreach ($old['updates'][$newcomponent] as $oldcomponentupdate) {
901 if ($newcomponentupdate['version'] == $oldcomponentupdate['version']) {
906 if (!isset($changes[$newcomponent])) {
907 $changes[$newcomponent] = array();
909 $changes[$newcomponent][] = $newcomponentupdate;
918 * Returns the URL to send update requests to
920 * During the development or testing, you can set $CFG->alternativeupdateproviderurl
921 * to a custom URL that will be used. Otherwise the standard URL will be returned.
925 protected function prepare_request_url() {
928 if (!empty($CFG->alternativeupdateproviderurl
)) {
929 return $CFG->alternativeupdateproviderurl
;
931 return 'http://download.moodle.org/api/1.0/updates.php';
936 * Sets the properties currentversion, currentrelease, currentbranch and currentplugins
938 * @param bool $forcereload
940 protected function load_current_environment($forcereload=false) {
943 if (!is_null($this->currentversion
) and !$forcereload) {
948 require($CFG->dirroot
.'/version.php');
949 $this->currentversion
= $version;
950 $this->currentrelease
= $release;
951 $this->currentbranch
= moodle_major_version(true);
953 $pluginman = plugin_manager
::instance();
954 foreach ($pluginman->get_plugins() as $type => $plugins) {
955 foreach ($plugins as $plugin) {
956 if (!$plugin->is_standard()) {
957 $this->currentplugins
[$plugin->component
] = $plugin->versiondisk
;
964 * Returns the list of HTTP params to be sent to the updates provider URL
966 * @return array of (string)param => (string)value
968 protected function prepare_request_params() {
971 $this->load_current_environment();
972 $this->restore_response();
975 $params['format'] = 'json';
977 if (isset($this->recentresponse
['ticket'])) {
978 $params['ticket'] = $this->recentresponse
['ticket'];
981 if (isset($this->currentversion
)) {
982 $params['version'] = $this->currentversion
;
984 throw new coding_exception('Main Moodle version must be already known here');
987 if (isset($this->currentbranch
)) {
988 $params['branch'] = $this->currentbranch
;
990 throw new coding_exception('Moodle release must be already known here');
994 foreach ($this->currentplugins
as $plugin => $version) {
995 $plugins[] = $plugin.'@'.$version;
997 if (!empty($plugins)) {
998 $params['plugins'] = implode(',', $plugins);
1005 * Returns the current timestamp
1007 * @return int the timestamp
1009 protected function cron_current_timestamp() {
1014 * Output cron debugging info
1017 * @param string $msg output message
1018 * @param string $eol end of line
1020 protected function cron_mtrace($msg, $eol = PHP_EOL
) {
1025 * Decide if the autocheck feature is disabled in the server setting
1027 * @return bool true if autocheck enabled, false if disabled
1029 protected function cron_autocheck_enabled() {
1032 if (empty($CFG->updateautocheck
)) {
1040 * Decide if the recently fetched data are still fresh enough
1042 * @param int $now current timestamp
1043 * @return bool true if no need to re-fetch, false otherwise
1045 protected function cron_has_fresh_fetch($now) {
1046 $recent = $this->get_last_timefetched();
1048 if (empty($recent)) {
1052 if ($now < $recent) {
1053 $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1057 if ($now - $recent > HOURSECS
) {
1065 * Decide if the fetch is outadated or even missing
1067 * @param int $now current timestamp
1068 * @return bool false if no need to re-fetch, true otherwise
1070 protected function cron_has_outdated_fetch($now) {
1071 $recent = $this->get_last_timefetched();
1073 if (empty($recent)) {
1077 if ($now < $recent) {
1078 $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1082 if ($now - $recent > 48 * HOURSECS
) {
1090 * Returns the cron execution offset for this site
1092 * The main {@link self::cron()} is supposed to run every night in some random time
1093 * between 01:00 and 06:00 AM (local time). The exact moment is defined by so called
1094 * execution offset, that is the amount of time after 01:00 AM. The offset value is
1095 * initially generated randomly and then used consistently at the site. This way, the
1096 * regular checks against the download.moodle.org server are spread in time.
1098 * @return int the offset number of seconds from range 1 sec to 5 hours
1100 protected function cron_execution_offset() {
1103 if (empty($CFG->updatecronoffset
)) {
1104 set_config('updatecronoffset', rand(1, 5 * HOURSECS
));
1107 return $CFG->updatecronoffset
;
1111 * Fetch available updates info and eventually send notification to site admins
1113 protected function cron_execute() {
1116 $this->restore_response();
1117 $previous = $this->recentresponse
;
1119 $this->restore_response(true);
1120 $current = $this->recentresponse
;
1121 $changes = $this->compare_responses($previous, $current);
1122 $notifications = $this->cron_notifications($changes);
1123 $this->cron_notify($notifications);
1124 $this->cron_mtrace('done');
1125 } catch (available_update_checker_exception
$e) {
1126 $this->cron_mtrace('FAILED!');
1131 * Given the list of changes in available updates, pick those to send to site admins
1133 * @param array $changes as returned by {@link self::compare_responses()}
1134 * @return array of available_update_info objects to send to site admins
1136 protected function cron_notifications(array $changes) {
1139 $notifications = array();
1140 $pluginman = plugin_manager
::instance();
1141 $plugins = $pluginman->get_plugins(true);
1143 foreach ($changes as $component => $componentchanges) {
1144 if (empty($componentchanges)) {
1147 $componentupdates = $this->get_update_info($component,
1148 array('minmaturity' => $CFG->updateminmaturity
, 'notifybuilds' => $CFG->updatenotifybuilds
));
1149 if (empty($componentupdates)) {
1152 // notify only about those $componentchanges that are present in $componentupdates
1153 // to respect the preferences
1154 foreach ($componentchanges as $componentchange) {
1155 foreach ($componentupdates as $componentupdate) {
1156 if ($componentupdate->version
== $componentchange['version']) {
1157 if ($component == 'core') {
1158 // in case of 'core' this is enough, we already know that the
1159 // $componentupdate is a real update with higher version
1160 $notifications[] = $componentupdate;
1162 // use the plugin_manager to check if the reported $componentchange
1163 // is a real update with higher version. such a real update must be
1164 // present in the 'availableupdates' property of one of the component's
1165 // available_update_info object
1166 list($plugintype, $pluginname) = normalize_component($component);
1167 if (!empty($plugins[$plugintype][$pluginname]->availableupdates
)) {
1168 foreach ($plugins[$plugintype][$pluginname]->availableupdates
as $availableupdate) {
1169 if ($availableupdate->version
== $componentchange['version']) {
1170 $notifications[] = $componentupdate;
1180 return $notifications;
1184 * Sends the given notifications to site admins via messaging API
1186 * @param array $notifications array of available_update_info objects to send
1188 protected function cron_notify(array $notifications) {
1191 if (empty($notifications)) {
1195 $admins = get_admins();
1197 if (empty($admins)) {
1201 $this->cron_mtrace('sending notifications ... ', '');
1203 $text = get_string('updatenotifications', 'core_admin') . PHP_EOL
;
1204 $html = html_writer
::tag('h1', get_string('updatenotifications', 'core_admin')) . PHP_EOL
;
1206 $coreupdates = array();
1207 $pluginupdates = array();
1209 foreach ($notifications as $notification) {
1210 if ($notification->component
== 'core') {
1211 $coreupdates[] = $notification;
1213 $pluginupdates[] = $notification;
1217 if (!empty($coreupdates)) {
1218 $text .= PHP_EOL
. get_string('updateavailable', 'core_admin') . PHP_EOL
;
1219 $html .= html_writer
::tag('h2', get_string('updateavailable', 'core_admin')) . PHP_EOL
;
1220 $html .= html_writer
::start_tag('ul') . PHP_EOL
;
1221 foreach ($coreupdates as $coreupdate) {
1222 $html .= html_writer
::start_tag('li');
1223 if (isset($coreupdate->release
)) {
1224 $text .= get_string('updateavailable_release', 'core_admin', $coreupdate->release
);
1225 $html .= html_writer
::tag('strong', get_string('updateavailable_release', 'core_admin', $coreupdate->release
));
1227 if (isset($coreupdate->version
)) {
1228 $text .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version
);
1229 $html .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version
);
1231 if (isset($coreupdate->maturity
)) {
1232 $text .= ' ('.get_string('maturity'.$coreupdate->maturity
, 'core_admin').')';
1233 $html .= ' ('.get_string('maturity'.$coreupdate->maturity
, 'core_admin').')';
1236 $html .= html_writer
::end_tag('li') . PHP_EOL
;
1239 $html .= html_writer
::end_tag('ul') . PHP_EOL
;
1241 $a = array('url' => $CFG->wwwroot
.'/'.$CFG->admin
.'/index.php');
1242 $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL
;
1243 $a = array('url' => html_writer
::link($CFG->wwwroot
.'/'.$CFG->admin
.'/index.php', $CFG->wwwroot
.'/'.$CFG->admin
.'/index.php'));
1244 $html .= html_writer
::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL
;
1247 if (!empty($pluginupdates)) {
1248 $text .= PHP_EOL
. get_string('updateavailableforplugin', 'core_admin') . PHP_EOL
;
1249 $html .= html_writer
::tag('h2', get_string('updateavailableforplugin', 'core_admin')) . PHP_EOL
;
1251 $html .= html_writer
::start_tag('ul') . PHP_EOL
;
1252 foreach ($pluginupdates as $pluginupdate) {
1253 $html .= html_writer
::start_tag('li');
1254 $text .= get_string('pluginname', $pluginupdate->component
);
1255 $html .= html_writer
::tag('strong', get_string('pluginname', $pluginupdate->component
));
1257 $text .= ' ('.$pluginupdate->component
.')';
1258 $html .= ' ('.$pluginupdate->component
.')';
1260 $text .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version
);
1261 $html .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version
);
1264 $html .= html_writer
::end_tag('li') . PHP_EOL
;
1267 $html .= html_writer
::end_tag('ul') . PHP_EOL
;
1269 $a = array('url' => $CFG->wwwroot
.'/'.$CFG->admin
.'/plugins.php');
1270 $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL
;
1271 $a = array('url' => html_writer
::link($CFG->wwwroot
.'/'.$CFG->admin
.'/plugins.php', $CFG->wwwroot
.'/'.$CFG->admin
.'/plugins.php'));
1272 $html .= html_writer
::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL
;
1275 $a = array('siteurl' => $CFG->wwwroot
);
1276 $text .= get_string('updatenotificationfooter', 'core_admin', $a) . PHP_EOL
;
1277 $a = array('siteurl' => html_writer
::link($CFG->wwwroot
, $CFG->wwwroot
));
1278 $html .= html_writer
::tag('footer', html_writer
::tag('p', get_string('updatenotificationfooter', 'core_admin', $a),
1279 array('style' => 'font-size:smaller; color:#333;')));
1281 $mainadmin = reset($admins);
1283 foreach ($admins as $admin) {
1284 $message = new stdClass();
1285 $message->component
= 'moodle';
1286 $message->name
= 'availableupdate';
1287 $message->userfrom
= $mainadmin;
1288 $message->userto
= $admin;
1289 $message->subject
= get_string('updatenotifications', 'core_admin');
1290 $message->fullmessage
= $text;
1291 $message->fullmessageformat
= FORMAT_PLAIN
;
1292 $message->fullmessagehtml
= $html;
1293 $message->smallmessage
= get_string('updatenotifications', 'core_admin');
1294 $message->notification
= 1;
1295 message_send($message);
1300 * Compare two release labels and decide if they are the same
1302 * @param string $remote release info of the available update
1303 * @param null|string $local release info of the local code, defaults to $release defined in version.php
1304 * @return boolean true if the releases declare the same minor+major version
1306 protected function is_same_release($remote, $local=null) {
1308 if (is_null($local)) {
1309 $this->load_current_environment();
1310 $local = $this->currentrelease
;
1313 $pattern = '/^([0-9\.\+]+)([^(]*)/';
1315 preg_match($pattern, $remote, $remotematches);
1316 preg_match($pattern, $local, $localmatches);
1318 $remotematches[1] = str_replace('+', '', $remotematches[1]);
1319 $localmatches[1] = str_replace('+', '', $localmatches[1]);
1321 if ($remotematches[1] === $localmatches[1] and rtrim($remotematches[2]) === rtrim($localmatches[2])) {
1331 * Defines the structure of objects returned by {@link available_update_checker::get_update_info()}
1333 class available_update_info
{
1335 /** @var string frankenstyle component name */
1337 /** @var int the available version of the component */
1339 /** @var string|null optional release name */
1340 public $release = null;
1341 /** @var int|null optional maturity info, eg {@link MATURITY_STABLE} */
1342 public $maturity = null;
1343 /** @var string|null optional URL of a page with more info about the update */
1345 /** @var string|null optional URL of a ZIP package that can be downloaded and installed */
1346 public $download = null;
1349 * Creates new instance of the class
1351 * The $info array must provide at least the 'version' value and optionally all other
1352 * values to populate the object's properties.
1354 * @param string $name the frankenstyle component name
1355 * @param array $info associative array with other properties
1357 public function __construct($name, array $info) {
1358 $this->component
= $name;
1359 foreach ($info as $k => $v) {
1360 if (property_exists('available_update_info', $k) and $k != 'component') {
1369 * Factory class producing required subclasses of {@link plugininfo_base}
1371 class plugininfo_default_factory
{
1374 * Makes a new instance of the plugininfo class
1376 * @param string $type the plugin type, eg. 'mod'
1377 * @param string $typerootdir full path to the location of all the plugins of this type
1378 * @param string $name the plugin name, eg. 'workshop'
1379 * @param string $namerootdir full path to the location of the plugin
1380 * @param string $typeclass the name of class that holds the info about the plugin
1381 * @return plugininfo_base the instance of $typeclass
1383 public static function make($type, $typerootdir, $name, $namerootdir, $typeclass) {
1384 $plugin = new $typeclass();
1385 $plugin->type
= $type;
1386 $plugin->typerootdir
= $typerootdir;
1387 $plugin->name
= $name;
1388 $plugin->rootdir
= $namerootdir;
1390 $plugin->init_display_name();
1391 $plugin->load_disk_version();
1392 $plugin->load_db_version();
1393 $plugin->load_required_main_version();
1394 $plugin->init_is_standard();
1402 * Base class providing access to the information about a plugin
1404 * @property-read string component the component name, type_name
1406 abstract class plugininfo_base
{
1408 /** @var string the plugintype name, eg. mod, auth or workshopform */
1410 /** @var string full path to the location of all the plugins of this type */
1411 public $typerootdir;
1412 /** @var string the plugin name, eg. assignment, ldap */
1414 /** @var string the localized plugin name */
1415 public $displayname;
1416 /** @var string the plugin source, one of plugin_manager::PLUGIN_SOURCE_xxx constants */
1418 /** @var fullpath to the location of this plugin */
1420 /** @var int|string the version of the plugin's source code */
1421 public $versiondisk;
1422 /** @var int|string the version of the installed plugin */
1424 /** @var int|float|string required version of Moodle core */
1425 public $versionrequires;
1426 /** @var array other plugins that this one depends on, lazy-loaded by {@link get_other_required_plugins()} */
1427 public $dependencies;
1428 /** @var int number of instances of the plugin - not supported yet */
1430 /** @var int order of the plugin among other plugins of the same type - not supported yet */
1432 /** @var array|null array of {@link available_update_info} for this plugin */
1433 public $availableupdates;
1436 * Gathers and returns the information about all plugins of the given type
1438 * @param string $type the name of the plugintype, eg. mod, auth or workshopform
1439 * @param string $typerootdir full path to the location of the plugin dir
1440 * @param string $typeclass the name of the actually called class
1441 * @return array of plugintype classes, indexed by the plugin name
1443 public static function get_plugins($type, $typerootdir, $typeclass) {
1445 // get the information about plugins at the disk
1446 $plugins = get_plugin_list($type);
1448 foreach ($plugins as $pluginname => $pluginrootdir) {
1449 $ondisk[$pluginname] = plugininfo_default_factory
::make($type, $typerootdir,
1450 $pluginname, $pluginrootdir, $typeclass);
1456 * Sets {@link $displayname} property to a localized name of the plugin
1458 public function init_display_name() {
1459 if (!get_string_manager()->string_exists('pluginname', $this->component
)) {
1460 $this->displayname
= '[pluginname,' . $this->component
. ']';
1462 $this->displayname
= get_string('pluginname', $this->component
);
1467 * Magic method getter, redirects to read only values.
1469 * @param string $name
1472 public function __get($name) {
1474 case 'component': return $this->type
. '_' . $this->name
;
1477 debugging('Invalid plugin property accessed! '.$name);
1483 * Return the full path name of a file within the plugin.
1485 * No check is made to see if the file exists.
1487 * @param string $relativepath e.g. 'version.php'.
1488 * @return string e.g. $CFG->dirroot . '/mod/quiz/version.php'.
1490 public function full_path($relativepath) {
1491 if (empty($this->rootdir
)) {
1494 return $this->rootdir
. '/' . $relativepath;
1498 * Load the data from version.php.
1500 * @return stdClass the object called $plugin defined in version.php
1502 protected function load_version_php() {
1503 $versionfile = $this->full_path('version.php');
1505 $plugin = new stdClass();
1506 if (is_readable($versionfile)) {
1507 include($versionfile);
1513 * Sets {@link $versiondisk} property to a numerical value representing the
1514 * version of the plugin's source code.
1516 * If the value is null after calling this method, either the plugin
1517 * does not use versioning (typically does not have any database
1518 * data) or is missing from disk.
1520 public function load_disk_version() {
1521 $plugin = $this->load_version_php();
1522 if (isset($plugin->version
)) {
1523 $this->versiondisk
= $plugin->version
;
1528 * Sets {@link $versionrequires} property to a numerical value representing
1529 * the version of Moodle core that this plugin requires.
1531 public function load_required_main_version() {
1532 $plugin = $this->load_version_php();
1533 if (isset($plugin->requires
)) {
1534 $this->versionrequires
= $plugin->requires
;
1539 * Initialise {@link $dependencies} to the list of other plugins (in any)
1540 * that this one requires to be installed.
1542 protected function load_other_required_plugins() {
1543 $plugin = $this->load_version_php();
1544 if (!empty($plugin->dependencies
)) {
1545 $this->dependencies
= $plugin->dependencies
;
1547 $this->dependencies
= array(); // By default, no dependencies.
1552 * Get the list of other plugins that this plugin requires to be installed.
1554 * @return array with keys the frankenstyle plugin name, and values either
1555 * a version string (like '2011101700') or the constant ANY_VERSION.
1557 public function get_other_required_plugins() {
1558 if (is_null($this->dependencies
)) {
1559 $this->load_other_required_plugins();
1561 return $this->dependencies
;
1565 * Sets {@link $versiondb} property to a numerical value representing the
1566 * currently installed version of the plugin.
1568 * If the value is null after calling this method, either the plugin
1569 * does not use versioning (typically does not have any database
1570 * data) or has not been installed yet.
1572 public function load_db_version() {
1573 if ($ver = self
::get_version_from_config_plugins($this->component
)) {
1574 $this->versiondb
= $ver;
1579 * Sets {@link $source} property to one of plugin_manager::PLUGIN_SOURCE_xxx
1582 * If the property's value is null after calling this method, then
1583 * the type of the plugin has not been recognized and you should throw
1586 public function init_is_standard() {
1588 $standard = plugin_manager
::standard_plugins_list($this->type
);
1590 if ($standard !== false) {
1591 $standard = array_flip($standard);
1592 if (isset($standard[$this->name
])) {
1593 $this->source
= plugin_manager
::PLUGIN_SOURCE_STANDARD
;
1594 } else if (!is_null($this->versiondb
) and is_null($this->versiondisk
)
1595 and plugin_manager
::is_deleted_standard_plugin($this->type
, $this->name
)) {
1596 $this->source
= plugin_manager
::PLUGIN_SOURCE_STANDARD
; // to be deleted
1598 $this->source
= plugin_manager
::PLUGIN_SOURCE_EXTENSION
;
1604 * Returns true if the plugin is shipped with the official distribution
1605 * of the current Moodle version, false otherwise.
1609 public function is_standard() {
1610 return $this->source
=== plugin_manager
::PLUGIN_SOURCE_STANDARD
;
1614 * Returns the status of the plugin
1616 * @return string one of plugin_manager::PLUGIN_STATUS_xxx constants
1618 public function get_status() {
1620 if (is_null($this->versiondb
) and is_null($this->versiondisk
)) {
1621 return plugin_manager
::PLUGIN_STATUS_NODB
;
1623 } else if (is_null($this->versiondb
) and !is_null($this->versiondisk
)) {
1624 return plugin_manager
::PLUGIN_STATUS_NEW
;
1626 } else if (!is_null($this->versiondb
) and is_null($this->versiondisk
)) {
1627 if (plugin_manager
::is_deleted_standard_plugin($this->type
, $this->name
)) {
1628 return plugin_manager
::PLUGIN_STATUS_DELETE
;
1630 return plugin_manager
::PLUGIN_STATUS_MISSING
;
1633 } else if ((string)$this->versiondb
=== (string)$this->versiondisk
) {
1634 return plugin_manager
::PLUGIN_STATUS_UPTODATE
;
1636 } else if ($this->versiondb
< $this->versiondisk
) {
1637 return plugin_manager
::PLUGIN_STATUS_UPGRADE
;
1639 } else if ($this->versiondb
> $this->versiondisk
) {
1640 return plugin_manager
::PLUGIN_STATUS_DOWNGRADE
;
1643 // $version = pi(); and similar funny jokes - hopefully Donald E. Knuth will never contribute to Moodle ;-)
1644 throw new coding_exception('Unable to determine plugin state, check the plugin versions');
1649 * Returns the information about plugin availability
1651 * True means that the plugin is enabled. False means that the plugin is
1652 * disabled. Null means that the information is not available, or the
1653 * plugin does not support configurable availability or the availability
1654 * can not be changed.
1658 public function is_enabled() {
1663 * Populates the property {@link $availableupdates} with the information provided by
1664 * available update checker
1666 * @param available_update_checker $provider the class providing the available update info
1668 public function check_available_updates(available_update_checker
$provider) {
1671 if (isset($CFG->updateminmaturity
)) {
1672 $minmaturity = $CFG->updateminmaturity
;
1674 // this can happen during the very first upgrade to 2.3
1675 $minmaturity = MATURITY_STABLE
;
1678 $this->availableupdates
= $provider->get_update_info($this->component
,
1679 array('minmaturity' => $minmaturity));
1683 * If there are updates for this plugin available, returns them.
1685 * Returns array of {@link available_update_info} objects, if some update
1686 * is available. Returns null if there is no update available or if the update
1687 * availability is unknown.
1689 * @return array|null
1691 public function available_updates() {
1693 if (empty($this->availableupdates
) or !is_array($this->availableupdates
)) {
1699 foreach ($this->availableupdates
as $availableupdate) {
1700 if ($availableupdate->version
> $this->versiondisk
) {
1701 $updates[] = $availableupdate;
1705 if (empty($updates)) {
1713 * Returns the URL of the plugin settings screen
1715 * Null value means that the plugin either does not have the settings screen
1716 * or its location is not available via this library.
1718 * @return null|moodle_url
1720 public function get_settings_url() {
1725 * Returns the URL of the screen where this plugin can be uninstalled
1727 * Visiting that URL must be safe, that is a manual confirmation is needed
1728 * for actual uninstallation of the plugin. Null value means that the
1729 * plugin either does not support uninstallation, or does not require any
1730 * database cleanup or the location of the screen is not available via this
1733 * @return null|moodle_url
1735 public function get_uninstall_url() {
1740 * Returns relative directory of the plugin with heading '/'
1744 public function get_dir() {
1747 return substr($this->rootdir
, strlen($CFG->dirroot
));
1751 * Provides access to plugin versions from {config_plugins}
1753 * @param string $plugin plugin name
1754 * @param double $disablecache optional, defaults to false
1755 * @return int|false the stored value or false if not found
1757 protected function get_version_from_config_plugins($plugin, $disablecache=false) {
1759 static $pluginversions = null;
1761 if (is_null($pluginversions) or $disablecache) {
1763 $pluginversions = $DB->get_records_menu('config_plugins', array('name' => 'version'), 'plugin', 'plugin,value');
1764 } catch (dml_exception
$e) {
1766 $pluginversions = array();
1770 if (!array_key_exists($plugin, $pluginversions)) {
1774 return $pluginversions[$plugin];
1780 * General class for all plugin types that do not have their own class
1782 class plugininfo_general
extends plugininfo_base
{
1787 * Class for page side blocks
1789 class plugininfo_block
extends plugininfo_base
{
1791 public static function get_plugins($type, $typerootdir, $typeclass) {
1793 // get the information about blocks at the disk
1794 $blocks = parent
::get_plugins($type, $typerootdir, $typeclass);
1796 // add blocks missing from disk
1797 $blocksinfo = self
::get_blocks_info();
1798 foreach ($blocksinfo as $blockname => $blockinfo) {
1799 if (isset($blocks[$blockname])) {
1802 $plugin = new $typeclass();
1803 $plugin->type
= $type;
1804 $plugin->typerootdir
= $typerootdir;
1805 $plugin->name
= $blockname;
1806 $plugin->rootdir
= null;
1807 $plugin->displayname
= $blockname;
1808 $plugin->versiondb
= $blockinfo->version
;
1809 $plugin->init_is_standard();
1811 $blocks[$blockname] = $plugin;
1817 public function init_display_name() {
1819 if (get_string_manager()->string_exists('pluginname', 'block_' . $this->name
)) {
1820 $this->displayname
= get_string('pluginname', 'block_' . $this->name
);
1822 } else if (($block = block_instance($this->name
)) !== false) {
1823 $this->displayname
= $block->get_title();
1826 parent
::init_display_name();
1830 public function load_db_version() {
1833 $blocksinfo = self
::get_blocks_info();
1834 if (isset($blocksinfo[$this->name
]->version
)) {
1835 $this->versiondb
= $blocksinfo[$this->name
]->version
;
1839 public function is_enabled() {
1841 $blocksinfo = self
::get_blocks_info();
1842 if (isset($blocksinfo[$this->name
]->visible
)) {
1843 if ($blocksinfo[$this->name
]->visible
) {
1849 return parent
::is_enabled();
1853 public function get_settings_url() {
1855 if (($block = block_instance($this->name
)) === false) {
1856 return parent
::get_settings_url();
1858 } else if ($block->has_config()) {
1859 if (file_exists($this->full_path('settings.php'))) {
1860 return new moodle_url('/admin/settings.php', array('section' => 'blocksetting' . $this->name
));
1862 $blocksinfo = self
::get_blocks_info();
1863 return new moodle_url('/admin/block.php', array('block' => $blocksinfo[$this->name
]->id
));
1867 return parent
::get_settings_url();
1871 public function get_uninstall_url() {
1873 $blocksinfo = self
::get_blocks_info();
1874 return new moodle_url('/admin/blocks.php', array('delete' => $blocksinfo[$this->name
]->id
, 'sesskey' => sesskey()));
1878 * Provides access to the records in {block} table
1880 * @param bool $disablecache do not use internal static cache
1881 * @return array array of stdClasses
1883 protected static function get_blocks_info($disablecache=false) {
1885 static $blocksinfocache = null;
1887 if (is_null($blocksinfocache) or $disablecache) {
1889 $blocksinfocache = $DB->get_records('block', null, 'name', 'name,id,version,visible');
1890 } catch (dml_exception
$e) {
1892 $blocksinfocache = array();
1896 return $blocksinfocache;
1902 * Class for text filters
1904 class plugininfo_filter
extends plugininfo_base
{
1906 public static function get_plugins($type, $typerootdir, $typeclass) {
1911 // get the list of filters from both /filter and /mod location
1912 $installed = filter_get_all_installed();
1914 foreach ($installed as $filterlegacyname => $displayname) {
1915 $plugin = new $typeclass();
1916 $plugin->type
= $type;
1917 $plugin->typerootdir
= $typerootdir;
1918 $plugin->name
= self
::normalize_legacy_name($filterlegacyname);
1919 $plugin->rootdir
= $CFG->dirroot
. '/' . $filterlegacyname;
1920 $plugin->displayname
= $displayname;
1922 $plugin->load_disk_version();
1923 $plugin->load_db_version();
1924 $plugin->load_required_main_version();
1925 $plugin->init_is_standard();
1927 $filters[$plugin->name
] = $plugin;
1930 $globalstates = self
::get_global_states();
1932 if ($DB->get_manager()->table_exists('filter_active')) {
1933 // if we're upgrading from 1.9, the table does not exist yet
1934 // if it does, make sure that all installed filters are registered
1935 $needsreload = false;
1936 foreach (array_keys($installed) as $filterlegacyname) {
1937 if (!isset($globalstates[self
::normalize_legacy_name($filterlegacyname)])) {
1938 filter_set_global_state($filterlegacyname, TEXTFILTER_DISABLED
);
1939 $needsreload = true;
1943 $globalstates = self
::get_global_states(true);
1947 // make sure that all registered filters are installed, just in case
1948 foreach ($globalstates as $name => $info) {
1949 if (!isset($filters[$name])) {
1950 // oops, there is a record in filter_active but the filter is not installed
1951 $plugin = new $typeclass();
1952 $plugin->type
= $type;
1953 $plugin->typerootdir
= $typerootdir;
1954 $plugin->name
= $name;
1955 $plugin->rootdir
= $CFG->dirroot
. '/' . $info->legacyname
;
1956 $plugin->displayname
= $info->legacyname
;
1958 $plugin->load_db_version();
1960 if (is_null($plugin->versiondb
)) {
1961 // this is a hack to stimulate 'Missing from disk' error
1962 // because $plugin->versiondisk will be null !== false
1963 $plugin->versiondb
= false;
1966 $filters[$plugin->name
] = $plugin;
1973 public function init_display_name() {
1974 // do nothing, the name is set in self::get_plugins()
1978 * @see load_version_php()
1980 protected function load_version_php() {
1981 if (strpos($this->name
, 'mod_') === 0) {
1982 // filters bundled with modules do not have a version.php and so
1983 // do not provide their own versioning information.
1984 return new stdClass();
1986 return parent
::load_version_php();
1989 public function is_enabled() {
1991 $globalstates = self
::get_global_states();
1993 foreach ($globalstates as $filterlegacyname => $info) {
1994 $name = self
::normalize_legacy_name($filterlegacyname);
1995 if ($name === $this->name
) {
1996 if ($info->active
== TEXTFILTER_DISABLED
) {
1999 // it may be 'On' or 'Off, but available'
2008 public function get_settings_url() {
2010 $globalstates = self
::get_global_states();
2011 $legacyname = $globalstates[$this->name
]->legacyname
;
2012 if (filter_has_global_settings($legacyname)) {
2013 return new moodle_url('/admin/settings.php', array('section' => 'filtersetting' . str_replace('/', '', $legacyname)));
2019 public function get_uninstall_url() {
2021 if (strpos($this->name
, 'mod_') === 0) {
2024 $globalstates = self
::get_global_states();
2025 $legacyname = $globalstates[$this->name
]->legacyname
;
2026 return new moodle_url('/admin/filters.php', array('sesskey' => sesskey(), 'filterpath' => $legacyname, 'action' => 'delete'));
2031 * Convert legacy filter names like 'filter/foo' or 'mod/bar' into frankenstyle
2033 * @param string $legacyfiltername legacy filter name
2034 * @return string frankenstyle-like name
2036 protected static function normalize_legacy_name($legacyfiltername) {
2038 $name = str_replace('/', '_', $legacyfiltername);
2039 if (strpos($name, 'filter_') === 0) {
2040 $name = substr($name, 7);
2042 throw new coding_exception('Unable to determine filter name: ' . $legacyfiltername);
2050 * Provides access to the results of {@link filter_get_global_states()}
2051 * but indexed by the normalized filter name
2053 * The legacy filter name is available as ->legacyname property.
2055 * @param bool $disablecache
2058 protected static function get_global_states($disablecache=false) {
2060 static $globalstatescache = null;
2062 if ($disablecache or is_null($globalstatescache)) {
2064 if (!$DB->get_manager()->table_exists('filter_active')) {
2065 // we're upgrading from 1.9 and the table used by {@link filter_get_global_states()}
2066 // does not exist yet
2067 $globalstatescache = array();
2070 foreach (filter_get_global_states() as $legacyname => $info) {
2071 $name = self
::normalize_legacy_name($legacyname);
2072 $filterinfo = new stdClass();
2073 $filterinfo->legacyname
= $legacyname;
2074 $filterinfo->active
= $info->active
;
2075 $filterinfo->sortorder
= $info->sortorder
;
2076 $globalstatescache[$name] = $filterinfo;
2081 return $globalstatescache;
2087 * Class for activity modules
2089 class plugininfo_mod
extends plugininfo_base
{
2091 public static function get_plugins($type, $typerootdir, $typeclass) {
2093 // get the information about plugins at the disk
2094 $modules = parent
::get_plugins($type, $typerootdir, $typeclass);
2096 // add modules missing from disk
2097 $modulesinfo = self
::get_modules_info();
2098 foreach ($modulesinfo as $modulename => $moduleinfo) {
2099 if (isset($modules[$modulename])) {
2102 $plugin = new $typeclass();
2103 $plugin->type
= $type;
2104 $plugin->typerootdir
= $typerootdir;
2105 $plugin->name
= $modulename;
2106 $plugin->rootdir
= null;
2107 $plugin->displayname
= $modulename;
2108 $plugin->versiondb
= $moduleinfo->version
;
2109 $plugin->init_is_standard();
2111 $modules[$modulename] = $plugin;
2117 public function init_display_name() {
2118 if (get_string_manager()->string_exists('pluginname', $this->component
)) {
2119 $this->displayname
= get_string('pluginname', $this->component
);
2121 $this->displayname
= get_string('modulename', $this->component
);
2126 * Load the data from version.php.
2127 * @return object the data object defined in version.php.
2129 protected function load_version_php() {
2130 $versionfile = $this->full_path('version.php');
2132 $module = new stdClass();
2133 if (is_readable($versionfile)) {
2134 include($versionfile);
2139 public function load_db_version() {
2142 $modulesinfo = self
::get_modules_info();
2143 if (isset($modulesinfo[$this->name
]->version
)) {
2144 $this->versiondb
= $modulesinfo[$this->name
]->version
;
2148 public function is_enabled() {
2150 $modulesinfo = self
::get_modules_info();
2151 if (isset($modulesinfo[$this->name
]->visible
)) {
2152 if ($modulesinfo[$this->name
]->visible
) {
2158 return parent
::is_enabled();
2162 public function get_settings_url() {
2164 if (file_exists($this->full_path('settings.php')) or file_exists($this->full_path('settingstree.php'))) {
2165 return new moodle_url('/admin/settings.php', array('section' => 'modsetting' . $this->name
));
2167 return parent
::get_settings_url();
2171 public function get_uninstall_url() {
2173 if ($this->name
!== 'forum') {
2174 return new moodle_url('/admin/modules.php', array('delete' => $this->name
, 'sesskey' => sesskey()));
2181 * Provides access to the records in {modules} table
2183 * @param bool $disablecache do not use internal static cache
2184 * @return array array of stdClasses
2186 protected static function get_modules_info($disablecache=false) {
2188 static $modulesinfocache = null;
2190 if (is_null($modulesinfocache) or $disablecache) {
2192 $modulesinfocache = $DB->get_records('modules', null, 'name', 'name,id,version,visible');
2193 } catch (dml_exception
$e) {
2195 $modulesinfocache = array();
2199 return $modulesinfocache;
2205 * Class for question behaviours.
2207 class plugininfo_qbehaviour
extends plugininfo_base
{
2209 public function get_uninstall_url() {
2210 return new moodle_url('/admin/qbehaviours.php',
2211 array('delete' => $this->name
, 'sesskey' => sesskey()));
2217 * Class for question types
2219 class plugininfo_qtype
extends plugininfo_base
{
2221 public function get_uninstall_url() {
2222 return new moodle_url('/admin/qtypes.php',
2223 array('delete' => $this->name
, 'sesskey' => sesskey()));
2229 * Class for authentication plugins
2231 class plugininfo_auth
extends plugininfo_base
{
2233 public function is_enabled() {
2235 /** @var null|array list of enabled authentication plugins */
2236 static $enabled = null;
2238 if (in_array($this->name
, array('nologin', 'manual'))) {
2239 // these two are always enabled and can't be disabled
2243 if (is_null($enabled)) {
2244 $enabled = array_flip(explode(',', $CFG->auth
));
2247 return isset($enabled[$this->name
]);
2250 public function get_settings_url() {
2251 if (file_exists($this->full_path('settings.php'))) {
2252 return new moodle_url('/admin/settings.php', array('section' => 'authsetting' . $this->name
));
2254 return new moodle_url('/admin/auth_config.php', array('auth' => $this->name
));
2261 * Class for enrolment plugins
2263 class plugininfo_enrol
extends plugininfo_base
{
2265 public function is_enabled() {
2267 /** @var null|array list of enabled enrolment plugins */
2268 static $enabled = null;
2270 // We do not actually need whole enrolment classes here so we do not call
2271 // {@link enrol_get_plugins()}. Note that this may produce slightly different
2272 // results, for example if the enrolment plugin does not contain lib.php
2273 // but it is listed in $CFG->enrol_plugins_enabled
2275 if (is_null($enabled)) {
2276 $enabled = array_flip(explode(',', $CFG->enrol_plugins_enabled
));
2279 return isset($enabled[$this->name
]);
2282 public function get_settings_url() {
2284 if ($this->is_enabled() or file_exists($this->full_path('settings.php'))) {
2285 return new moodle_url('/admin/settings.php', array('section' => 'enrolsettings' . $this->name
));
2287 return parent
::get_settings_url();
2291 public function get_uninstall_url() {
2292 return new moodle_url('/admin/enrol.php', array('action' => 'uninstall', 'enrol' => $this->name
, 'sesskey' => sesskey()));
2298 * Class for messaging processors
2300 class plugininfo_message
extends plugininfo_base
{
2302 public function get_settings_url() {
2303 $processors = get_message_processors();
2304 if (isset($processors[$this->name
])) {
2305 $processor = $processors[$this->name
];
2306 if ($processor->available
&& $processor->hassettings
) {
2307 return new moodle_url('settings.php', array('section' => 'messagesetting'.$processor->name
));
2310 return parent
::get_settings_url();
2314 * @see plugintype_interface::is_enabled()
2316 public function is_enabled() {
2317 $processors = get_message_processors();
2318 if (isset($processors[$this->name
])) {
2319 return $processors[$this->name
]->configured
&& $processors[$this->name
]->enabled
;
2321 return parent
::is_enabled();
2326 * @see plugintype_interface::get_uninstall_url()
2328 public function get_uninstall_url() {
2329 $processors = get_message_processors();
2330 if (isset($processors[$this->name
])) {
2331 return new moodle_url('message.php', array('uninstall' => $processors[$this->name
]->id
, 'sesskey' => sesskey()));
2333 return parent
::get_uninstall_url();
2340 * Class for repositories
2342 class plugininfo_repository
extends plugininfo_base
{
2344 public function is_enabled() {
2346 $enabled = self
::get_enabled_repositories();
2348 return isset($enabled[$this->name
]);
2351 public function get_settings_url() {
2353 if ($this->is_enabled()) {
2354 return new moodle_url('/admin/repository.php', array('sesskey' => sesskey(), 'action' => 'edit', 'repos' => $this->name
));
2356 return parent
::get_settings_url();
2361 * Provides access to the records in {repository} table
2363 * @param bool $disablecache do not use internal static cache
2364 * @return array array of stdClasses
2366 protected static function get_enabled_repositories($disablecache=false) {
2368 static $repositories = null;
2370 if (is_null($repositories) or $disablecache) {
2371 $repositories = $DB->get_records('repository', null, 'type', 'type,visible,sortorder');
2374 return $repositories;
2380 * Class for portfolios
2382 class plugininfo_portfolio
extends plugininfo_base
{
2384 public function is_enabled() {
2386 $enabled = self
::get_enabled_portfolios();
2388 return isset($enabled[$this->name
]);
2392 * Provides access to the records in {portfolio_instance} table
2394 * @param bool $disablecache do not use internal static cache
2395 * @return array array of stdClasses
2397 protected static function get_enabled_portfolios($disablecache=false) {
2399 static $portfolios = null;
2401 if (is_null($portfolios) or $disablecache) {
2402 $portfolios = array();
2403 $instances = $DB->get_recordset('portfolio_instance', null, 'plugin');
2404 foreach ($instances as $instance) {
2405 if (isset($portfolios[$instance->plugin
])) {
2406 if ($instance->visible
) {
2407 $portfolios[$instance->plugin
]->visible
= $instance->visible
;
2410 $portfolios[$instance->plugin
] = $instance;
2423 class plugininfo_theme
extends plugininfo_base
{
2425 public function is_enabled() {
2428 if ((!empty($CFG->theme
) and $CFG->theme
=== $this->name
) or
2429 (!empty($CFG->themelegacy
) and $CFG->themelegacy
=== $this->name
)) {
2432 return parent
::is_enabled();
2439 * Class representing an MNet service
2441 class plugininfo_mnetservice
extends plugininfo_base
{
2443 public function is_enabled() {
2446 if (empty($CFG->mnet_dispatcher_mode
) ||
$CFG->mnet_dispatcher_mode
!== 'strict') {
2449 return parent
::is_enabled();
2456 * Class for admin tool plugins
2458 class plugininfo_tool
extends plugininfo_base
{
2460 public function get_uninstall_url() {
2461 return new moodle_url('/admin/tools.php', array('delete' => $this->name
, 'sesskey' => sesskey()));
2467 * Class for admin tool plugins
2469 class plugininfo_report
extends plugininfo_base
{
2471 public function get_uninstall_url() {
2472 return new moodle_url('/admin/reports.php', array('delete' => $this->name
, 'sesskey' => sesskey()));