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) {
101 if ($disablecache or is_null($this->pluginsinfo
)) {
102 $this->pluginsinfo
= array();
103 $plugintypes = get_plugin_types();
104 $plugintypes = $this->reorder_plugin_types($plugintypes);
105 foreach ($plugintypes as $plugintype => $plugintyperootdir) {
106 if (in_array($plugintype, array('base', 'general'))) {
107 throw new coding_exception('Illegal usage of reserved word for plugin type');
109 if (class_exists('plugininfo_' . $plugintype)) {
110 $plugintypeclass = 'plugininfo_' . $plugintype;
112 $plugintypeclass = 'plugininfo_general';
114 if (!in_array('plugininfo_base', class_parents($plugintypeclass))) {
115 throw new coding_exception('Class ' . $plugintypeclass . ' must extend plugininfo_base');
117 $plugins = call_user_func(array($plugintypeclass, 'get_plugins'), $plugintype, $plugintyperootdir, $plugintypeclass);
118 $this->pluginsinfo
[$plugintype] = $plugins;
121 // TODO: MDL-20438 verify this is the correct solution/replace
122 if (!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'
383 'coursereport' => array(
387 'datafield' => array(
388 'checkbox', 'date', 'file', 'latlong', 'menu', 'multimenu',
389 'number', 'picture', 'radiobutton', 'text', 'textarea', 'url'
392 'datapreset' => array(
397 'textarea', 'tinymce'
401 'authorize', 'category', 'cohort', 'database', 'flatfile',
402 'guest', 'imsenterprise', 'ldap', 'manual', 'meta', 'mnet',
407 'activitynames', 'algebra', 'censor', 'emailprotect',
408 'emoticon', 'mediaplugin', 'multilang', 'tex', 'tidy',
409 'urltolink', 'data', 'glossary'
413 'scorm', 'social', 'topics', 'weeks'
416 'gradeexport' => array(
417 'ods', 'txt', 'xls', 'xml'
420 'gradeimport' => array(
424 'gradereport' => array(
425 'grader', 'outcomes', 'overview', 'user'
428 'gradingform' => array(
436 'email', 'jabber', 'popup'
439 'mnetservice' => array(
444 'assign', 'assignment', 'chat', 'choice', 'data', 'feedback', 'folder',
445 'forum', 'glossary', 'imscp', 'label', 'lesson', 'lti', 'page',
446 'quiz', 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop'
449 'plagiarism' => array(
452 'portfolio' => array(
453 'boxnet', 'download', 'flickr', 'googledocs', 'mahara', 'picasa'
456 'profilefield' => array(
457 'checkbox', 'datetime', 'menu', 'text', 'textarea'
460 'qbehaviour' => array(
461 'adaptive', 'adaptivenopenalty', 'deferredcbm',
462 'deferredfeedback', 'immediatecbm', 'immediatefeedback',
463 'informationitem', 'interactive', 'interactivecountback',
464 'manualgraded', 'missing'
468 'aiken', 'blackboard', 'blackboard_six', 'examview', 'gift',
469 'learnwise', 'missingword', 'multianswer', 'webct',
474 'calculated', 'calculatedmulti', 'calculatedsimple',
475 'description', 'essay', 'match', 'missingtype', 'multianswer',
476 'multichoice', 'numerical', 'random', 'randomsamatch',
477 'shortanswer', 'truefalse'
481 'grading', 'overview', 'responses', 'statistics'
484 'quizaccess' => array(
485 'delaybetweenattempts', 'ipaddress', 'numattempts', 'openclosedate',
486 'password', 'safebrowser', 'securewindow', 'timelimit'
490 'backups', 'completion', 'configlog', 'courseoverview',
491 'log', 'loglive', 'outline', 'participation', 'progress', 'questioninstances', 'security', 'stats'
494 'repository' => array(
495 'alfresco', 'boxnet', 'coursefiles', 'dropbox', 'filesystem',
496 'flickr', 'flickr_public', 'googledocs', 'local', 'merlot',
497 'picasa', 'recent', 's3', 'upload', 'url', 'user', 'webdav',
498 'wikimedia', 'youtube'
501 'scormreport' => array(
508 'afterburner', 'anomaly', 'arialist', 'base', 'binarius',
509 'boxxie', 'brick', 'canvas', 'formal_white', 'formfactor',
510 'fusion', 'leatherbound', 'magazine', 'mymobile', 'nimble',
511 'nonzero', 'overlay', 'serenity', 'sky_high', 'splash',
512 'standard', 'standardold'
516 'assignmentupgrade', 'bloglevelupgrade', 'capability', 'customlang', 'dbtransfer', 'generator',
517 'health', 'innodb', 'langimport', 'multilangupgrade', 'phpunit', 'profiling',
518 'qeupgradehelper', 'replace', 'spamcleaner', 'timezoneimport', 'unittest',
519 'uploaduser', 'unsuproles', 'xmldb'
522 'webservice' => array(
523 'amf', 'rest', 'soap', 'xmlrpc'
526 'workshopallocation' => array(
527 'manual', 'random', 'scheduled'
530 'workshopeval' => array(
534 'workshopform' => array(
535 'accumulative', 'comments', 'numerrors', 'rubric'
539 if (isset($standard_plugins[$type])) {
540 return $standard_plugins[$type];
547 * Reorders plugin types into a sequence to be displayed
549 * For technical reasons, plugin types returned by {@link get_plugin_types()} are
550 * in a certain order that does not need to fit the expected order for the display.
551 * Particularly, activity modules should be displayed first as they represent the
552 * real heart of Moodle. They should be followed by other plugin types that are
553 * used to build the courses (as that is what one expects from LMS). After that,
554 * other supportive plugin types follow.
556 * @param array $types associative array
557 * @return array same array with altered order of items
559 protected function reorder_plugin_types(array $types) {
561 'mod' => $types['mod'],
562 'block' => $types['block'],
563 'qtype' => $types['qtype'],
564 'qbehaviour' => $types['qbehaviour'],
565 'qformat' => $types['qformat'],
566 'filter' => $types['filter'],
567 'enrol' => $types['enrol'],
569 foreach ($types as $type => $path) {
570 if (!isset($fix[$type])) {
580 * General exception thrown by the {@link available_update_checker} class
582 class available_update_checker_exception
extends moodle_exception
{
585 * @param string $errorcode exception description identifier
586 * @param mixed $debuginfo debugging data to display
588 public function __construct($errorcode, $debuginfo=null) {
589 parent
::__construct($errorcode, 'core_plugin', '', null, print_r($debuginfo, true));
595 * Singleton class that handles checking for available updates
597 class available_update_checker
{
599 /** @var available_update_checker holds the singleton instance */
600 protected static $singletoninstance;
601 /** @var null|int the timestamp of when the most recent response was fetched */
602 protected $recentfetch = null;
603 /** @var null|array the recent response from the update notification provider */
604 protected $recentresponse = null;
605 /** @var null|string the numerical version of the local Moodle code */
606 protected $currentversion = null;
607 /** @var null|string the release info of the local Moodle code */
608 protected $currentrelease = null;
609 /** @var null|string branch of the local Moodle code */
610 protected $currentbranch = null;
611 /** @var array of (string)frankestyle => (string)version list of additional plugins deployed at this site */
612 protected $currentplugins = array();
615 * Direct initiation not allowed, use the factory method {@link self::instance()}
617 protected function __construct() {
621 * Sorry, this is singleton
623 protected function __clone() {
627 * Factory method for this class
629 * @return available_update_checker the singleton instance
631 public static function instance() {
632 if (is_null(self
::$singletoninstance)) {
633 self
::$singletoninstance = new self();
635 return self
::$singletoninstance;
639 * Returns the timestamp of the last execution of {@link fetch()}
641 * @return int|null null if it has never been executed or we don't known
643 public function get_last_timefetched() {
645 $this->restore_response();
647 if (!empty($this->recentfetch
)) {
648 return $this->recentfetch
;
656 * Fetches the available update status from the remote site
658 * @throws available_update_checker_exception
660 public function fetch() {
661 $response = $this->get_response();
662 $this->validate_response($response);
663 $this->store_response($response);
667 * Returns the available update information for the given component
669 * This method returns null if the most recent response does not contain any information
670 * about it. The returned structure is an array of available updates for the given
671 * component. Each update info is an object with at least one property called
672 * 'version'. Other possible properties are 'release', 'maturity', 'url' and 'downloadurl'.
674 * For the 'core' component, the method returns real updates only (those with higher version).
675 * For all other components, the list of all known remote updates is returned and the caller
676 * (usually the {@link plugin_manager}) is supposed to make the actual comparison of versions.
678 * @param string $component frankenstyle
679 * @param array $options with supported keys 'minmaturity' and/or 'notifybuilds'
680 * @return null|array null or array of available_update_info objects
682 public function get_update_info($component, array $options = array()) {
684 if (!isset($options['minmaturity'])) {
685 $options['minmaturity'] = 0;
688 if (!isset($options['notifybuilds'])) {
689 $options['notifybuilds'] = false;
692 if ($component == 'core') {
693 $this->load_current_environment();
696 $this->restore_response();
698 if (empty($this->recentresponse
['updates'][$component])) {
703 foreach ($this->recentresponse
['updates'][$component] as $info) {
704 $update = new available_update_info($component, $info);
705 if (isset($update->maturity
) and ($update->maturity
< $options['minmaturity'])) {
708 if ($component == 'core') {
709 if ($update->version
<= $this->currentversion
) {
712 if (empty($options['notifybuilds']) and $this->is_same_release($update->release
)) {
716 $updates[] = $update;
719 if (empty($updates)) {
727 * The method being run via cron.php
729 public function cron() {
732 if (!$this->cron_autocheck_enabled()) {
733 $this->cron_mtrace('Automatic check for available updates not enabled, skipping.');
737 $now = $this->cron_current_timestamp();
739 if ($this->cron_has_fresh_fetch($now)) {
740 $this->cron_mtrace('Recently fetched info about available updates is still fresh enough, skipping.');
744 if ($this->cron_has_outdated_fetch($now)) {
745 $this->cron_mtrace('Outdated or missing info about available updates, forced fetching ... ', '');
746 $this->cron_execute();
750 $offset = $this->cron_execution_offset();
751 $start = mktime(1, 0, 0, date('n', $now), date('j', $now), date('Y', $now)); // 01:00 AM today local time
752 if ($now > $start +
$offset) {
753 $this->cron_mtrace('Regular daily check for available updates ... ', '');
754 $this->cron_execute();
759 /// end of public API //////////////////////////////////////////////////////
762 * Makes cURL request to get data from the remote site
764 * @return string raw request result
765 * @throws available_update_checker_exception
767 protected function get_response() {
768 $curl = new curl(array('proxy' => true));
769 $response = $curl->post($this->prepare_request_url(), $this->prepare_request_params());
770 $curlinfo = $curl->get_info();
771 if ($curlinfo['http_code'] != 200) {
772 throw new available_update_checker_exception('err_response_http_code', $curlinfo['http_code']);
778 * Makes sure the response is valid, has correct API format etc.
780 * @param string $response raw response as returned by the {@link self::get_response()}
781 * @throws available_update_checker_exception
783 protected function validate_response($response) {
785 $response = $this->decode_response($response);
787 if (empty($response)) {
788 throw new available_update_checker_exception('err_response_empty');
791 if (empty($response['status']) or $response['status'] !== 'OK') {
792 throw new available_update_checker_exception('err_response_status', $response['status']);
795 if (empty($response['apiver']) or $response['apiver'] !== '1.0') {
796 throw new available_update_checker_exception('err_response_format_version', $response['apiver']);
799 if (empty($response['forbranch']) or $response['forbranch'] !== moodle_major_version(true)) {
800 throw new available_update_checker_exception('err_response_target_version', $response['target']);
805 * Decodes the raw string response from the update notifications provider
807 * @param string $response as returned by {@link self::get_response()}
808 * @return array decoded response structure
810 protected function decode_response($response) {
811 return json_decode($response, true);
815 * Stores the valid fetched response for later usage
817 * This implementation uses the config_plugins table as the permanent storage.
819 * @param string $response raw valid data returned by {@link self::get_response()}
821 protected function store_response($response) {
823 set_config('recentfetch', time(), 'core_plugin');
824 set_config('recentresponse', $response, 'core_plugin');
826 $this->restore_response(true);
830 * Loads the most recent raw response record we have fetched
832 * This implementation uses the config_plugins table as the permanent storage.
834 * @param bool $forcereload reload even if it was already loaded
836 protected function restore_response($forcereload = false) {
838 if (!$forcereload and !is_null($this->recentresponse
)) {
839 // we already have it, nothing to do
843 $config = get_config('core_plugin');
845 if (!empty($config->recentresponse
) and !empty($config->recentfetch
)) {
847 $this->validate_response($config->recentresponse
);
848 $this->recentfetch
= $config->recentfetch
;
849 $this->recentresponse
= $this->decode_response($config->recentresponse
);
850 } catch (available_update_checker_exception
$e) {
851 // do not set recentresponse if the validation fails
855 $this->recentresponse
= array();
860 * Compares two raw {@link $recentresponse} records and returns the list of changed updates
862 * This method is used to populate potential update info to be sent to site admins.
866 * @throws available_update_checker_exception
867 * @return array parts of $new['updates'] that have changed
869 protected function compare_responses(array $old, array $new) {
875 if (!array_key_exists('updates', $new)) {
876 throw new available_update_checker_exception('err_response_format');
880 return $new['updates'];
883 if (!array_key_exists('updates', $old)) {
884 throw new available_update_checker_exception('err_response_format');
889 foreach ($new['updates'] as $newcomponent => $newcomponentupdates) {
890 if (empty($old['updates'][$newcomponent])) {
891 $changes[$newcomponent] = $newcomponentupdates;
894 foreach ($newcomponentupdates as $newcomponentupdate) {
896 foreach ($old['updates'][$newcomponent] as $oldcomponentupdate) {
897 if ($newcomponentupdate['version'] == $oldcomponentupdate['version']) {
902 if (!isset($changes[$newcomponent])) {
903 $changes[$newcomponent] = array();
905 $changes[$newcomponent][] = $newcomponentupdate;
914 * Returns the URL to send update requests to
916 * During the development or testing, you can set $CFG->alternativeupdateproviderurl
917 * to a custom URL that will be used. Otherwise the standard URL will be returned.
921 protected function prepare_request_url() {
924 if (!empty($CFG->alternativeupdateproviderurl
)) {
925 return $CFG->alternativeupdateproviderurl
;
927 return 'http://download.moodle.org/api/1.0/updates.php';
932 * Sets the properties currentversion, currentrelease, currentbranch and currentplugins
934 * @param bool $forcereload
936 protected function load_current_environment($forcereload=false) {
939 if (!is_null($this->currentversion
) and !$forcereload) {
944 require($CFG->dirroot
.'/version.php');
945 $this->currentversion
= $version;
946 $this->currentrelease
= $release;
947 $this->currentbranch
= moodle_major_version(true);
949 $pluginman = plugin_manager
::instance();
950 foreach ($pluginman->get_plugins() as $type => $plugins) {
951 foreach ($plugins as $plugin) {
952 if (!$plugin->is_standard()) {
953 $this->currentplugins
[$plugin->component
] = $plugin->versiondisk
;
960 * Returns the list of HTTP params to be sent to the updates provider URL
962 * @return array of (string)param => (string)value
964 protected function prepare_request_params() {
967 $this->load_current_environment();
968 $this->restore_response();
971 $params['format'] = 'json';
973 if (isset($this->recentresponse
['ticket'])) {
974 $params['ticket'] = $this->recentresponse
['ticket'];
977 if (isset($this->currentversion
)) {
978 $params['version'] = $this->currentversion
;
980 throw new coding_exception('Main Moodle version must be already known here');
983 if (isset($this->currentbranch
)) {
984 $params['branch'] = $this->currentbranch
;
986 throw new coding_exception('Moodle release must be already known here');
990 foreach ($this->currentplugins
as $plugin => $version) {
991 $plugins[] = $plugin.'@'.$version;
993 if (!empty($plugins)) {
994 $params['plugins'] = implode(',', $plugins);
1001 * Returns the current timestamp
1003 * @return int the timestamp
1005 protected function cron_current_timestamp() {
1010 * Output cron debugging info
1013 * @param string $msg output message
1014 * @param string $eol end of line
1016 protected function cron_mtrace($msg, $eol = PHP_EOL
) {
1021 * Decide if the autocheck feature is disabled in the server setting
1023 * @return bool true if autocheck enabled, false if disabled
1025 protected function cron_autocheck_enabled() {
1028 if (empty($CFG->updateautocheck
)) {
1036 * Decide if the recently fetched data are still fresh enough
1038 * @param int $now current timestamp
1039 * @return bool true if no need to re-fetch, false otherwise
1041 protected function cron_has_fresh_fetch($now) {
1042 $recent = $this->get_last_timefetched();
1044 if (empty($recent)) {
1048 if ($now < $recent) {
1049 $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1053 if ($now - $recent > HOURSECS
) {
1061 * Decide if the fetch is outadated or even missing
1063 * @param int $now current timestamp
1064 * @return bool false if no need to re-fetch, true otherwise
1066 protected function cron_has_outdated_fetch($now) {
1067 $recent = $this->get_last_timefetched();
1069 if (empty($recent)) {
1073 if ($now < $recent) {
1074 $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1078 if ($now - $recent > 48 * HOURSECS
) {
1086 * Returns the cron execution offset for this site
1088 * The main {@link self::cron()} is supposed to run every night in some random time
1089 * between 01:00 and 06:00 AM (local time). The exact moment is defined by so called
1090 * execution offset, that is the amount of time after 01:00 AM. The offset value is
1091 * initially generated randomly and then used consistently at the site. This way, the
1092 * regular checks against the download.moodle.org server are spread in time.
1094 * @return int the offset number of seconds from range 1 sec to 5 hours
1096 protected function cron_execution_offset() {
1099 if (empty($CFG->updatecronoffset
)) {
1100 set_config('updatecronoffset', rand(1, 5 * HOURSECS
));
1103 return $CFG->updatecronoffset
;
1107 * Fetch available updates info and eventually send notification to site admins
1109 protected function cron_execute() {
1111 $this->restore_response();
1112 $previous = $this->recentresponse
;
1114 $this->restore_response(true);
1115 $current = $this->recentresponse
;
1117 $changes = $this->compare_responses($previous, $current);
1118 $notifications = $this->cron_notifications($changes);
1119 $this->cron_notify($notifications);
1120 $this->cron_mtrace('done');
1121 } catch (available_update_checker_exception
$e) {
1122 $this->cron_mtrace('FAILED!');
1127 * Given the list of changes in available updates, pick those to send to site admins
1129 * @param array $changes as returned by {@link self::compare_responses()}
1130 * @return array of available_update_info objects to send to site admins
1132 protected function cron_notifications(array $changes) {
1135 $notifications = array();
1136 $pluginman = plugin_manager
::instance();
1137 $plugins = $pluginman->get_plugins(true);
1139 foreach ($changes as $component => $componentchanges) {
1140 if (empty($componentchanges)) {
1143 $componentupdates = $this->get_update_info($component,
1144 array('minmaturity' => $CFG->updateminmaturity
, 'notifybuilds' => $CFG->updatenotifybuilds
));
1145 if (empty($componentupdates)) {
1148 // notify only about those $componentchanges that are present in $componentupdates
1149 // to respect the preferences
1150 foreach ($componentchanges as $componentchange) {
1151 foreach ($componentupdates as $componentupdate) {
1152 if ($componentupdate->version
== $componentchange['version']) {
1153 if ($component == 'core') {
1154 // in case of 'core' this is enough, we already know that the
1155 // $componentupdate is a real update with higher version
1156 $notifications[] = $componentupdate;
1158 // use the plugin_manager to check if the reported $componentchange
1159 // is a real update with higher version. such a real update must be
1160 // present in the 'availableupdates' property of one of the component's
1161 // available_update_info object
1162 list($plugintype, $pluginname) = normalize_component($component);
1163 if (!empty($plugins[$plugintype][$pluginname]->availableupdates
)) {
1164 foreach ($plugins[$plugintype][$pluginname]->availableupdates
as $availableupdate) {
1165 if ($availableupdate->version
== $componentchange['version']) {
1166 $notifications[] = $componentupdate;
1176 return $notifications;
1180 * Sends the given notifications to site admins via messaging API
1182 * @param array $notifications array of available_update_info objects to send
1184 protected function cron_notify(array $notifications) {
1187 if (empty($notifications)) {
1191 $admins = get_admins();
1193 if (empty($admins)) {
1197 $this->cron_mtrace('sending notifications ... ', '');
1199 $text = get_string('updatenotifications', 'core_admin') . PHP_EOL
;
1200 $html = html_writer
::tag('h1', get_string('updatenotifications', 'core_admin')) . PHP_EOL
;
1202 $coreupdates = array();
1203 $pluginupdates = array();
1205 foreach ($notifications as $notification) {
1206 if ($notification->component
== 'core') {
1207 $coreupdates[] = $notification;
1209 $pluginupdates[] = $notification;
1213 if (!empty($coreupdates)) {
1214 $text .= PHP_EOL
. get_string('updateavailable', 'core_admin') . PHP_EOL
;
1215 $html .= html_writer
::tag('h2', get_string('updateavailable', 'core_admin')) . PHP_EOL
;
1216 $html .= html_writer
::start_tag('ul') . PHP_EOL
;
1217 foreach ($coreupdates as $coreupdate) {
1218 $html .= html_writer
::start_tag('li');
1219 if (isset($coreupdate->release
)) {
1220 $text .= get_string('updateavailable_release', 'core_admin', $coreupdate->release
);
1221 $html .= html_writer
::tag('strong', get_string('updateavailable_release', 'core_admin', $coreupdate->release
));
1223 if (isset($coreupdate->version
)) {
1224 $text .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version
);
1225 $html .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version
);
1227 if (isset($coreupdate->maturity
)) {
1228 $text .= ' ('.get_string('maturity'.$coreupdate->maturity
, 'core_admin').')';
1229 $html .= ' ('.get_string('maturity'.$coreupdate->maturity
, 'core_admin').')';
1232 $html .= html_writer
::end_tag('li') . PHP_EOL
;
1235 $html .= html_writer
::end_tag('ul') . PHP_EOL
;
1237 $a = array('url' => $CFG->wwwroot
.'/'.$CFG->admin
.'/index.php');
1238 $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL
;
1239 $a = array('url' => html_writer
::link($CFG->wwwroot
.'/'.$CFG->admin
.'/index.php', $CFG->wwwroot
.'/'.$CFG->admin
.'/index.php'));
1240 $html .= html_writer
::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL
;
1243 if (!empty($pluginupdates)) {
1244 $text .= PHP_EOL
. get_string('updateavailableforplugin', 'core_admin') . PHP_EOL
;
1245 $html .= html_writer
::tag('h2', get_string('updateavailableforplugin', 'core_admin')) . PHP_EOL
;
1247 $html .= html_writer
::start_tag('ul') . PHP_EOL
;
1248 foreach ($pluginupdates as $pluginupdate) {
1249 $html .= html_writer
::start_tag('li');
1250 $text .= get_string('pluginname', $pluginupdate->component
);
1251 $html .= html_writer
::tag('strong', get_string('pluginname', $pluginupdate->component
));
1253 $text .= ' ('.$pluginupdate->component
.')';
1254 $html .= ' ('.$pluginupdate->component
.')';
1256 $text .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version
);
1257 $html .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version
);
1260 $html .= html_writer
::end_tag('li') . PHP_EOL
;
1263 $html .= html_writer
::end_tag('ul') . PHP_EOL
;
1265 $a = array('url' => $CFG->wwwroot
.'/'.$CFG->admin
.'/plugins.php');
1266 $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL
;
1267 $a = array('url' => html_writer
::link($CFG->wwwroot
.'/'.$CFG->admin
.'/plugins.php', $CFG->wwwroot
.'/'.$CFG->admin
.'/plugins.php'));
1268 $html .= html_writer
::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL
;
1271 $a = array('siteurl' => $CFG->wwwroot
);
1272 $text .= get_string('updatenotificationfooter', 'core_admin', $a) . PHP_EOL
;
1273 $a = array('siteurl' => html_writer
::link($CFG->wwwroot
, $CFG->wwwroot
));
1274 $html .= html_writer
::tag('footer', html_writer
::tag('p', get_string('updatenotificationfooter', 'core_admin', $a),
1275 array('style' => 'font-size:smaller; color:#333;')));
1277 $mainadmin = reset($admins);
1279 foreach ($admins as $admin) {
1280 $message = new stdClass();
1281 $message->component
= 'moodle';
1282 $message->name
= 'availableupdate';
1283 $message->userfrom
= $mainadmin;
1284 $message->userto
= $admin;
1285 $message->subject
= get_string('updatenotifications', 'core_admin');
1286 $message->fullmessage
= $text;
1287 $message->fullmessageformat
= FORMAT_PLAIN
;
1288 $message->fullmessagehtml
= $html;
1289 $message->smallmessage
= get_string('updatenotifications', 'core_admin');
1290 $message->notification
= 1;
1291 message_send($message);
1296 * Compare two release labels and decide if they are the same
1298 * @param string $remote release info of the available update
1299 * @param null|string $local release info of the local code, defaults to $release defined in version.php
1300 * @return boolean true if the releases declare the same minor+major version
1302 protected function is_same_release($remote, $local=null) {
1304 if (is_null($local)) {
1305 $this->load_current_environment();
1306 $local = $this->currentrelease
;
1309 $pattern = '/^([0-9\.\+]+)([^(]*)/';
1311 preg_match($pattern, $remote, $remotematches);
1312 preg_match($pattern, $local, $localmatches);
1314 $remotematches[1] = str_replace('+', '', $remotematches[1]);
1315 $localmatches[1] = str_replace('+', '', $localmatches[1]);
1317 if ($remotematches[1] === $localmatches[1] and rtrim($remotematches[2]) === rtrim($localmatches[2])) {
1327 * Defines the structure of objects returned by {@link available_update_checker::get_update_info()}
1329 class available_update_info
{
1331 /** @var string frankenstyle component name */
1333 /** @var int the available version of the component */
1335 /** @var string|null optional release name */
1336 public $release = null;
1337 /** @var int|null optional maturity info, eg {@link MATURITY_STABLE} */
1338 public $maturity = null;
1339 /** @var string|null optional URL of a page with more info about the update */
1341 /** @var string|null optional URL of a ZIP package that can be downloaded and installed */
1342 public $download = null;
1345 * Creates new instance of the class
1347 * The $info array must provide at least the 'version' value and optionally all other
1348 * values to populate the object's properties.
1350 * @param string $name the frankenstyle component name
1351 * @param array $info associative array with other properties
1353 public function __construct($name, array $info) {
1354 $this->component
= $name;
1355 foreach ($info as $k => $v) {
1356 if (property_exists('available_update_info', $k) and $k != 'component') {
1365 * Factory class producing required subclasses of {@link plugininfo_base}
1367 class plugininfo_default_factory
{
1370 * Makes a new instance of the plugininfo class
1372 * @param string $type the plugin type, eg. 'mod'
1373 * @param string $typerootdir full path to the location of all the plugins of this type
1374 * @param string $name the plugin name, eg. 'workshop'
1375 * @param string $namerootdir full path to the location of the plugin
1376 * @param string $typeclass the name of class that holds the info about the plugin
1377 * @return plugininfo_base the instance of $typeclass
1379 public static function make($type, $typerootdir, $name, $namerootdir, $typeclass) {
1380 $plugin = new $typeclass();
1381 $plugin->type
= $type;
1382 $plugin->typerootdir
= $typerootdir;
1383 $plugin->name
= $name;
1384 $plugin->rootdir
= $namerootdir;
1386 $plugin->init_display_name();
1387 $plugin->load_disk_version();
1388 $plugin->load_db_version();
1389 $plugin->load_required_main_version();
1390 $plugin->init_is_standard();
1398 * Base class providing access to the information about a plugin
1400 * @property-read string component the component name, type_name
1402 abstract class plugininfo_base
{
1404 /** @var string the plugintype name, eg. mod, auth or workshopform */
1406 /** @var string full path to the location of all the plugins of this type */
1407 public $typerootdir;
1408 /** @var string the plugin name, eg. assignment, ldap */
1410 /** @var string the localized plugin name */
1411 public $displayname;
1412 /** @var string the plugin source, one of plugin_manager::PLUGIN_SOURCE_xxx constants */
1414 /** @var fullpath to the location of this plugin */
1416 /** @var int|string the version of the plugin's source code */
1417 public $versiondisk;
1418 /** @var int|string the version of the installed plugin */
1420 /** @var int|float|string required version of Moodle core */
1421 public $versionrequires;
1422 /** @var array other plugins that this one depends on, lazy-loaded by {@link get_other_required_plugins()} */
1423 public $dependencies;
1424 /** @var int number of instances of the plugin - not supported yet */
1426 /** @var int order of the plugin among other plugins of the same type - not supported yet */
1428 /** @var array|null array of {@link available_update_info} for this plugin */
1429 public $availableupdates;
1432 * Gathers and returns the information about all plugins of the given type
1434 * @param string $type the name of the plugintype, eg. mod, auth or workshopform
1435 * @param string $typerootdir full path to the location of the plugin dir
1436 * @param string $typeclass the name of the actually called class
1437 * @return array of plugintype classes, indexed by the plugin name
1439 public static function get_plugins($type, $typerootdir, $typeclass) {
1441 // get the information about plugins at the disk
1442 $plugins = get_plugin_list($type);
1444 foreach ($plugins as $pluginname => $pluginrootdir) {
1445 $ondisk[$pluginname] = plugininfo_default_factory
::make($type, $typerootdir,
1446 $pluginname, $pluginrootdir, $typeclass);
1452 * Sets {@link $displayname} property to a localized name of the plugin
1454 public function init_display_name() {
1455 if (!get_string_manager()->string_exists('pluginname', $this->component
)) {
1456 $this->displayname
= '[pluginname,' . $this->component
. ']';
1458 $this->displayname
= get_string('pluginname', $this->component
);
1463 * Magic method getter, redirects to read only values.
1465 * @param string $name
1468 public function __get($name) {
1470 case 'component': return $this->type
. '_' . $this->name
;
1473 debugging('Invalid plugin property accessed! '.$name);
1479 * Return the full path name of a file within the plugin.
1481 * No check is made to see if the file exists.
1483 * @param string $relativepath e.g. 'version.php'.
1484 * @return string e.g. $CFG->dirroot . '/mod/quiz/version.php'.
1486 public function full_path($relativepath) {
1487 if (empty($this->rootdir
)) {
1490 return $this->rootdir
. '/' . $relativepath;
1494 * Load the data from version.php.
1496 * @return stdClass the object called $plugin defined in version.php
1498 protected function load_version_php() {
1499 $versionfile = $this->full_path('version.php');
1501 $plugin = new stdClass();
1502 if (is_readable($versionfile)) {
1503 include($versionfile);
1509 * Sets {@link $versiondisk} property to a numerical value representing the
1510 * version of the plugin's source code.
1512 * If the value is null after calling this method, either the plugin
1513 * does not use versioning (typically does not have any database
1514 * data) or is missing from disk.
1516 public function load_disk_version() {
1517 $plugin = $this->load_version_php();
1518 if (isset($plugin->version
)) {
1519 $this->versiondisk
= $plugin->version
;
1524 * Sets {@link $versionrequires} property to a numerical value representing
1525 * the version of Moodle core that this plugin requires.
1527 public function load_required_main_version() {
1528 $plugin = $this->load_version_php();
1529 if (isset($plugin->requires
)) {
1530 $this->versionrequires
= $plugin->requires
;
1535 * Initialise {@link $dependencies} to the list of other plugins (in any)
1536 * that this one requires to be installed.
1538 protected function load_other_required_plugins() {
1539 $plugin = $this->load_version_php();
1540 if (!empty($plugin->dependencies
)) {
1541 $this->dependencies
= $plugin->dependencies
;
1543 $this->dependencies
= array(); // By default, no dependencies.
1548 * Get the list of other plugins that this plugin requires to be installed.
1550 * @return array with keys the frankenstyle plugin name, and values either
1551 * a version string (like '2011101700') or the constant ANY_VERSION.
1553 public function get_other_required_plugins() {
1554 if (is_null($this->dependencies
)) {
1555 $this->load_other_required_plugins();
1557 return $this->dependencies
;
1561 * Sets {@link $versiondb} property to a numerical value representing the
1562 * currently installed version of the plugin.
1564 * If the value is null after calling this method, either the plugin
1565 * does not use versioning (typically does not have any database
1566 * data) or has not been installed yet.
1568 public function load_db_version() {
1569 if ($ver = self
::get_version_from_config_plugins($this->component
)) {
1570 $this->versiondb
= $ver;
1575 * Sets {@link $source} property to one of plugin_manager::PLUGIN_SOURCE_xxx
1578 * If the property's value is null after calling this method, then
1579 * the type of the plugin has not been recognized and you should throw
1582 public function init_is_standard() {
1584 $standard = plugin_manager
::standard_plugins_list($this->type
);
1586 if ($standard !== false) {
1587 $standard = array_flip($standard);
1588 if (isset($standard[$this->name
])) {
1589 $this->source
= plugin_manager
::PLUGIN_SOURCE_STANDARD
;
1590 } else if (!is_null($this->versiondb
) and is_null($this->versiondisk
)
1591 and plugin_manager
::is_deleted_standard_plugin($this->type
, $this->name
)) {
1592 $this->source
= plugin_manager
::PLUGIN_SOURCE_STANDARD
; // to be deleted
1594 $this->source
= plugin_manager
::PLUGIN_SOURCE_EXTENSION
;
1600 * Returns true if the plugin is shipped with the official distribution
1601 * of the current Moodle version, false otherwise.
1605 public function is_standard() {
1606 return $this->source
=== plugin_manager
::PLUGIN_SOURCE_STANDARD
;
1610 * Returns the status of the plugin
1612 * @return string one of plugin_manager::PLUGIN_STATUS_xxx constants
1614 public function get_status() {
1616 if (is_null($this->versiondb
) and is_null($this->versiondisk
)) {
1617 return plugin_manager
::PLUGIN_STATUS_NODB
;
1619 } else if (is_null($this->versiondb
) and !is_null($this->versiondisk
)) {
1620 return plugin_manager
::PLUGIN_STATUS_NEW
;
1622 } else if (!is_null($this->versiondb
) and is_null($this->versiondisk
)) {
1623 if (plugin_manager
::is_deleted_standard_plugin($this->type
, $this->name
)) {
1624 return plugin_manager
::PLUGIN_STATUS_DELETE
;
1626 return plugin_manager
::PLUGIN_STATUS_MISSING
;
1629 } else if ((string)$this->versiondb
=== (string)$this->versiondisk
) {
1630 return plugin_manager
::PLUGIN_STATUS_UPTODATE
;
1632 } else if ($this->versiondb
< $this->versiondisk
) {
1633 return plugin_manager
::PLUGIN_STATUS_UPGRADE
;
1635 } else if ($this->versiondb
> $this->versiondisk
) {
1636 return plugin_manager
::PLUGIN_STATUS_DOWNGRADE
;
1639 // $version = pi(); and similar funny jokes - hopefully Donald E. Knuth will never contribute to Moodle ;-)
1640 throw new coding_exception('Unable to determine plugin state, check the plugin versions');
1645 * Returns the information about plugin availability
1647 * True means that the plugin is enabled. False means that the plugin is
1648 * disabled. Null means that the information is not available, or the
1649 * plugin does not support configurable availability or the availability
1650 * can not be changed.
1654 public function is_enabled() {
1659 * Populates the property {@link $availableupdates} with the information provided by
1660 * available update checker
1662 * @param available_update_checker $provider the class providing the available update info
1664 public function check_available_updates(available_update_checker
$provider) {
1667 if (isset($CFG->updateminmaturity
)) {
1668 $minmaturity = $CFG->updateminmaturity
;
1670 // this can happen during the very first upgrade to 2.3
1671 $minmaturity = MATURITY_STABLE
;
1674 $this->availableupdates
= $provider->get_update_info($this->component
,
1675 array('minmaturity' => $minmaturity));
1679 * If there are updates for this plugin available, returns them.
1681 * Returns array of {@link available_update_info} objects, if some update
1682 * is available. Returns null if there is no update available or if the update
1683 * availability is unknown.
1685 * @return array|null
1687 public function available_updates() {
1689 if (empty($this->availableupdates
) or !is_array($this->availableupdates
)) {
1695 foreach ($this->availableupdates
as $availableupdate) {
1696 if ($availableupdate->version
> $this->versiondisk
) {
1697 $updates[] = $availableupdate;
1701 if (empty($updates)) {
1709 * Returns the URL of the plugin settings screen
1711 * Null value means that the plugin either does not have the settings screen
1712 * or its location is not available via this library.
1714 * @return null|moodle_url
1716 public function get_settings_url() {
1721 * Returns the URL of the screen where this plugin can be uninstalled
1723 * Visiting that URL must be safe, that is a manual confirmation is needed
1724 * for actual uninstallation of the plugin. Null value means that the
1725 * plugin either does not support uninstallation, or does not require any
1726 * database cleanup or the location of the screen is not available via this
1729 * @return null|moodle_url
1731 public function get_uninstall_url() {
1736 * Returns relative directory of the plugin with heading '/'
1740 public function get_dir() {
1743 return substr($this->rootdir
, strlen($CFG->dirroot
));
1747 * Provides access to plugin versions from {config_plugins}
1749 * @param string $plugin plugin name
1750 * @param double $disablecache optional, defaults to false
1751 * @return int|false the stored value or false if not found
1753 protected function get_version_from_config_plugins($plugin, $disablecache=false) {
1755 static $pluginversions = null;
1757 if (is_null($pluginversions) or $disablecache) {
1759 $pluginversions = $DB->get_records_menu('config_plugins', array('name' => 'version'), 'plugin', 'plugin,value');
1760 } catch (dml_exception
$e) {
1762 $pluginversions = array();
1766 if (!array_key_exists($plugin, $pluginversions)) {
1770 return $pluginversions[$plugin];
1776 * General class for all plugin types that do not have their own class
1778 class plugininfo_general
extends plugininfo_base
{
1783 * Class for page side blocks
1785 class plugininfo_block
extends plugininfo_base
{
1787 public static function get_plugins($type, $typerootdir, $typeclass) {
1789 // get the information about blocks at the disk
1790 $blocks = parent
::get_plugins($type, $typerootdir, $typeclass);
1792 // add blocks missing from disk
1793 $blocksinfo = self
::get_blocks_info();
1794 foreach ($blocksinfo as $blockname => $blockinfo) {
1795 if (isset($blocks[$blockname])) {
1798 $plugin = new $typeclass();
1799 $plugin->type
= $type;
1800 $plugin->typerootdir
= $typerootdir;
1801 $plugin->name
= $blockname;
1802 $plugin->rootdir
= null;
1803 $plugin->displayname
= $blockname;
1804 $plugin->versiondb
= $blockinfo->version
;
1805 $plugin->init_is_standard();
1807 $blocks[$blockname] = $plugin;
1813 public function init_display_name() {
1815 if (get_string_manager()->string_exists('pluginname', 'block_' . $this->name
)) {
1816 $this->displayname
= get_string('pluginname', 'block_' . $this->name
);
1818 } else if (($block = block_instance($this->name
)) !== false) {
1819 $this->displayname
= $block->get_title();
1822 parent
::init_display_name();
1826 public function load_db_version() {
1829 $blocksinfo = self
::get_blocks_info();
1830 if (isset($blocksinfo[$this->name
]->version
)) {
1831 $this->versiondb
= $blocksinfo[$this->name
]->version
;
1835 public function is_enabled() {
1837 $blocksinfo = self
::get_blocks_info();
1838 if (isset($blocksinfo[$this->name
]->visible
)) {
1839 if ($blocksinfo[$this->name
]->visible
) {
1845 return parent
::is_enabled();
1849 public function get_settings_url() {
1851 if (($block = block_instance($this->name
)) === false) {
1852 return parent
::get_settings_url();
1854 } else if ($block->has_config()) {
1855 if (file_exists($this->full_path('settings.php'))) {
1856 return new moodle_url('/admin/settings.php', array('section' => 'blocksetting' . $this->name
));
1858 $blocksinfo = self
::get_blocks_info();
1859 return new moodle_url('/admin/block.php', array('block' => $blocksinfo[$this->name
]->id
));
1863 return parent
::get_settings_url();
1867 public function get_uninstall_url() {
1869 $blocksinfo = self
::get_blocks_info();
1870 return new moodle_url('/admin/blocks.php', array('delete' => $blocksinfo[$this->name
]->id
, 'sesskey' => sesskey()));
1874 * Provides access to the records in {block} table
1876 * @param bool $disablecache do not use internal static cache
1877 * @return array array of stdClasses
1879 protected static function get_blocks_info($disablecache=false) {
1881 static $blocksinfocache = null;
1883 if (is_null($blocksinfocache) or $disablecache) {
1885 $blocksinfocache = $DB->get_records('block', null, 'name', 'name,id,version,visible');
1886 } catch (dml_exception
$e) {
1888 $blocksinfocache = array();
1892 return $blocksinfocache;
1898 * Class for text filters
1900 class plugininfo_filter
extends plugininfo_base
{
1902 public static function get_plugins($type, $typerootdir, $typeclass) {
1907 // get the list of filters from both /filter and /mod location
1908 $installed = filter_get_all_installed();
1910 foreach ($installed as $filterlegacyname => $displayname) {
1911 $plugin = new $typeclass();
1912 $plugin->type
= $type;
1913 $plugin->typerootdir
= $typerootdir;
1914 $plugin->name
= self
::normalize_legacy_name($filterlegacyname);
1915 $plugin->rootdir
= $CFG->dirroot
. '/' . $filterlegacyname;
1916 $plugin->displayname
= $displayname;
1918 $plugin->load_disk_version();
1919 $plugin->load_db_version();
1920 $plugin->load_required_main_version();
1921 $plugin->init_is_standard();
1923 $filters[$plugin->name
] = $plugin;
1926 $globalstates = self
::get_global_states();
1928 if ($DB->get_manager()->table_exists('filter_active')) {
1929 // if we're upgrading from 1.9, the table does not exist yet
1930 // if it does, make sure that all installed filters are registered
1931 $needsreload = false;
1932 foreach (array_keys($installed) as $filterlegacyname) {
1933 if (!isset($globalstates[self
::normalize_legacy_name($filterlegacyname)])) {
1934 filter_set_global_state($filterlegacyname, TEXTFILTER_DISABLED
);
1935 $needsreload = true;
1939 $globalstates = self
::get_global_states(true);
1943 // make sure that all registered filters are installed, just in case
1944 foreach ($globalstates as $name => $info) {
1945 if (!isset($filters[$name])) {
1946 // oops, there is a record in filter_active but the filter is not installed
1947 $plugin = new $typeclass();
1948 $plugin->type
= $type;
1949 $plugin->typerootdir
= $typerootdir;
1950 $plugin->name
= $name;
1951 $plugin->rootdir
= $CFG->dirroot
. '/' . $info->legacyname
;
1952 $plugin->displayname
= $info->legacyname
;
1954 $plugin->load_db_version();
1956 if (is_null($plugin->versiondb
)) {
1957 // this is a hack to stimulate 'Missing from disk' error
1958 // because $plugin->versiondisk will be null !== false
1959 $plugin->versiondb
= false;
1962 $filters[$plugin->name
] = $plugin;
1969 public function init_display_name() {
1970 // do nothing, the name is set in self::get_plugins()
1974 * @see load_version_php()
1976 protected function load_version_php() {
1977 if (strpos($this->name
, 'mod_') === 0) {
1978 // filters bundled with modules do not have a version.php and so
1979 // do not provide their own versioning information.
1980 return new stdClass();
1982 return parent
::load_version_php();
1985 public function is_enabled() {
1987 $globalstates = self
::get_global_states();
1989 foreach ($globalstates as $filterlegacyname => $info) {
1990 $name = self
::normalize_legacy_name($filterlegacyname);
1991 if ($name === $this->name
) {
1992 if ($info->active
== TEXTFILTER_DISABLED
) {
1995 // it may be 'On' or 'Off, but available'
2004 public function get_settings_url() {
2006 $globalstates = self
::get_global_states();
2007 $legacyname = $globalstates[$this->name
]->legacyname
;
2008 if (filter_has_global_settings($legacyname)) {
2009 return new moodle_url('/admin/settings.php', array('section' => 'filtersetting' . str_replace('/', '', $legacyname)));
2015 public function get_uninstall_url() {
2017 if (strpos($this->name
, 'mod_') === 0) {
2020 $globalstates = self
::get_global_states();
2021 $legacyname = $globalstates[$this->name
]->legacyname
;
2022 return new moodle_url('/admin/filters.php', array('sesskey' => sesskey(), 'filterpath' => $legacyname, 'action' => 'delete'));
2027 * Convert legacy filter names like 'filter/foo' or 'mod/bar' into frankenstyle
2029 * @param string $legacyfiltername legacy filter name
2030 * @return string frankenstyle-like name
2032 protected static function normalize_legacy_name($legacyfiltername) {
2034 $name = str_replace('/', '_', $legacyfiltername);
2035 if (strpos($name, 'filter_') === 0) {
2036 $name = substr($name, 7);
2038 throw new coding_exception('Unable to determine filter name: ' . $legacyfiltername);
2046 * Provides access to the results of {@link filter_get_global_states()}
2047 * but indexed by the normalized filter name
2049 * The legacy filter name is available as ->legacyname property.
2051 * @param bool $disablecache
2054 protected static function get_global_states($disablecache=false) {
2056 static $globalstatescache = null;
2058 if ($disablecache or is_null($globalstatescache)) {
2060 if (!$DB->get_manager()->table_exists('filter_active')) {
2061 // we're upgrading from 1.9 and the table used by {@link filter_get_global_states()}
2062 // does not exist yet
2063 $globalstatescache = array();
2066 foreach (filter_get_global_states() as $legacyname => $info) {
2067 $name = self
::normalize_legacy_name($legacyname);
2068 $filterinfo = new stdClass();
2069 $filterinfo->legacyname
= $legacyname;
2070 $filterinfo->active
= $info->active
;
2071 $filterinfo->sortorder
= $info->sortorder
;
2072 $globalstatescache[$name] = $filterinfo;
2077 return $globalstatescache;
2083 * Class for activity modules
2085 class plugininfo_mod
extends plugininfo_base
{
2087 public static function get_plugins($type, $typerootdir, $typeclass) {
2089 // get the information about plugins at the disk
2090 $modules = parent
::get_plugins($type, $typerootdir, $typeclass);
2092 // add modules missing from disk
2093 $modulesinfo = self
::get_modules_info();
2094 foreach ($modulesinfo as $modulename => $moduleinfo) {
2095 if (isset($modules[$modulename])) {
2098 $plugin = new $typeclass();
2099 $plugin->type
= $type;
2100 $plugin->typerootdir
= $typerootdir;
2101 $plugin->name
= $modulename;
2102 $plugin->rootdir
= null;
2103 $plugin->displayname
= $modulename;
2104 $plugin->versiondb
= $moduleinfo->version
;
2105 $plugin->init_is_standard();
2107 $modules[$modulename] = $plugin;
2113 public function init_display_name() {
2114 if (get_string_manager()->string_exists('pluginname', $this->component
)) {
2115 $this->displayname
= get_string('pluginname', $this->component
);
2117 $this->displayname
= get_string('modulename', $this->component
);
2122 * Load the data from version.php.
2123 * @return object the data object defined in version.php.
2125 protected function load_version_php() {
2126 $versionfile = $this->full_path('version.php');
2128 $module = new stdClass();
2129 if (is_readable($versionfile)) {
2130 include($versionfile);
2135 public function load_db_version() {
2138 $modulesinfo = self
::get_modules_info();
2139 if (isset($modulesinfo[$this->name
]->version
)) {
2140 $this->versiondb
= $modulesinfo[$this->name
]->version
;
2144 public function is_enabled() {
2146 $modulesinfo = self
::get_modules_info();
2147 if (isset($modulesinfo[$this->name
]->visible
)) {
2148 if ($modulesinfo[$this->name
]->visible
) {
2154 return parent
::is_enabled();
2158 public function get_settings_url() {
2160 if (file_exists($this->full_path('settings.php')) or file_exists($this->full_path('settingstree.php'))) {
2161 return new moodle_url('/admin/settings.php', array('section' => 'modsetting' . $this->name
));
2163 return parent
::get_settings_url();
2167 public function get_uninstall_url() {
2169 if ($this->name
!== 'forum') {
2170 return new moodle_url('/admin/modules.php', array('delete' => $this->name
, 'sesskey' => sesskey()));
2177 * Provides access to the records in {modules} table
2179 * @param bool $disablecache do not use internal static cache
2180 * @return array array of stdClasses
2182 protected static function get_modules_info($disablecache=false) {
2184 static $modulesinfocache = null;
2186 if (is_null($modulesinfocache) or $disablecache) {
2188 $modulesinfocache = $DB->get_records('modules', null, 'name', 'name,id,version,visible');
2189 } catch (dml_exception
$e) {
2191 $modulesinfocache = array();
2195 return $modulesinfocache;
2201 * Class for question behaviours.
2203 class plugininfo_qbehaviour
extends plugininfo_base
{
2205 public function get_uninstall_url() {
2206 return new moodle_url('/admin/qbehaviours.php',
2207 array('delete' => $this->name
, 'sesskey' => sesskey()));
2213 * Class for question types
2215 class plugininfo_qtype
extends plugininfo_base
{
2217 public function get_uninstall_url() {
2218 return new moodle_url('/admin/qtypes.php',
2219 array('delete' => $this->name
, 'sesskey' => sesskey()));
2225 * Class for authentication plugins
2227 class plugininfo_auth
extends plugininfo_base
{
2229 public function is_enabled() {
2231 /** @var null|array list of enabled authentication plugins */
2232 static $enabled = null;
2234 if (in_array($this->name
, array('nologin', 'manual'))) {
2235 // these two are always enabled and can't be disabled
2239 if (is_null($enabled)) {
2240 $enabled = array_flip(explode(',', $CFG->auth
));
2243 return isset($enabled[$this->name
]);
2246 public function get_settings_url() {
2247 if (file_exists($this->full_path('settings.php'))) {
2248 return new moodle_url('/admin/settings.php', array('section' => 'authsetting' . $this->name
));
2250 return new moodle_url('/admin/auth_config.php', array('auth' => $this->name
));
2257 * Class for enrolment plugins
2259 class plugininfo_enrol
extends plugininfo_base
{
2261 public function is_enabled() {
2263 /** @var null|array list of enabled enrolment plugins */
2264 static $enabled = null;
2266 // We do not actually need whole enrolment classes here so we do not call
2267 // {@link enrol_get_plugins()}. Note that this may produce slightly different
2268 // results, for example if the enrolment plugin does not contain lib.php
2269 // but it is listed in $CFG->enrol_plugins_enabled
2271 if (is_null($enabled)) {
2272 $enabled = array_flip(explode(',', $CFG->enrol_plugins_enabled
));
2275 return isset($enabled[$this->name
]);
2278 public function get_settings_url() {
2280 if ($this->is_enabled() or file_exists($this->full_path('settings.php'))) {
2281 return new moodle_url('/admin/settings.php', array('section' => 'enrolsettings' . $this->name
));
2283 return parent
::get_settings_url();
2287 public function get_uninstall_url() {
2288 return new moodle_url('/admin/enrol.php', array('action' => 'uninstall', 'enrol' => $this->name
, 'sesskey' => sesskey()));
2294 * Class for messaging processors
2296 class plugininfo_message
extends plugininfo_base
{
2298 public function get_settings_url() {
2299 $processors = get_message_processors();
2300 if (isset($processors[$this->name
])) {
2301 $processor = $processors[$this->name
];
2302 if ($processor->available
&& $processor->hassettings
) {
2303 return new moodle_url('settings.php', array('section' => 'messagesetting'.$processor->name
));
2306 return parent
::get_settings_url();
2310 * @see plugintype_interface::is_enabled()
2312 public function is_enabled() {
2313 $processors = get_message_processors();
2314 if (isset($processors[$this->name
])) {
2315 return $processors[$this->name
]->configured
&& $processors[$this->name
]->enabled
;
2317 return parent
::is_enabled();
2322 * @see plugintype_interface::get_uninstall_url()
2324 public function get_uninstall_url() {
2325 $processors = get_message_processors();
2326 if (isset($processors[$this->name
])) {
2327 return new moodle_url('message.php', array('uninstall' => $processors[$this->name
]->id
, 'sesskey' => sesskey()));
2329 return parent
::get_uninstall_url();
2336 * Class for repositories
2338 class plugininfo_repository
extends plugininfo_base
{
2340 public function is_enabled() {
2342 $enabled = self
::get_enabled_repositories();
2344 return isset($enabled[$this->name
]);
2347 public function get_settings_url() {
2349 if ($this->is_enabled()) {
2350 return new moodle_url('/admin/repository.php', array('sesskey' => sesskey(), 'action' => 'edit', 'repos' => $this->name
));
2352 return parent
::get_settings_url();
2357 * Provides access to the records in {repository} table
2359 * @param bool $disablecache do not use internal static cache
2360 * @return array array of stdClasses
2362 protected static function get_enabled_repositories($disablecache=false) {
2364 static $repositories = null;
2366 if (is_null($repositories) or $disablecache) {
2367 $repositories = $DB->get_records('repository', null, 'type', 'type,visible,sortorder');
2370 return $repositories;
2376 * Class for portfolios
2378 class plugininfo_portfolio
extends plugininfo_base
{
2380 public function is_enabled() {
2382 $enabled = self
::get_enabled_portfolios();
2384 return isset($enabled[$this->name
]);
2388 * Provides access to the records in {portfolio_instance} table
2390 * @param bool $disablecache do not use internal static cache
2391 * @return array array of stdClasses
2393 protected static function get_enabled_portfolios($disablecache=false) {
2395 static $portfolios = null;
2397 if (is_null($portfolios) or $disablecache) {
2398 $portfolios = array();
2399 $instances = $DB->get_recordset('portfolio_instance', null, 'plugin');
2400 foreach ($instances as $instance) {
2401 if (isset($portfolios[$instance->plugin
])) {
2402 if ($instance->visible
) {
2403 $portfolios[$instance->plugin
]->visible
= $instance->visible
;
2406 $portfolios[$instance->plugin
] = $instance;
2419 class plugininfo_theme
extends plugininfo_base
{
2421 public function is_enabled() {
2424 if ((!empty($CFG->theme
) and $CFG->theme
=== $this->name
) or
2425 (!empty($CFG->themelegacy
) and $CFG->themelegacy
=== $this->name
)) {
2428 return parent
::is_enabled();
2435 * Class representing an MNet service
2437 class plugininfo_mnetservice
extends plugininfo_base
{
2439 public function is_enabled() {
2442 if (empty($CFG->mnet_dispatcher_mode
) ||
$CFG->mnet_dispatcher_mode
!== 'strict') {
2445 return parent
::is_enabled();
2452 * Class for admin tool plugins
2454 class plugininfo_tool
extends plugininfo_base
{
2456 public function get_uninstall_url() {
2457 return new moodle_url('/admin/tools.php', array('delete' => $this->name
, 'sesskey' => sesskey()));
2463 * Class for admin tool plugins
2465 class plugininfo_report
extends plugininfo_base
{
2467 public function get_uninstall_url() {
2468 return new moodle_url('/admin/reports.php', array('delete' => $this->name
, 'sesskey' => sesskey()));