3 // This file is part of Moodle - http://moodle.org/
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
19 * Defines classes used for plugins management
21 * This library provides a unified interface to various plugin types in
22 * Moodle. It is mainly used by the plugins management admin page and the
23 * plugins check page during the upgrade.
27 * @copyright 2011 David Mudrak <david@moodle.com>
28 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
31 defined('MOODLE_INTERNAL') ||
die();
34 * Singleton class providing general plugins management functionality
36 class plugin_manager
{
38 /** the plugin is shipped with standard Moodle distribution */
39 const PLUGIN_SOURCE_STANDARD
= 'std';
40 /** the plugin is added extension */
41 const PLUGIN_SOURCE_EXTENSION
= 'ext';
43 /** the plugin uses neither database nor capabilities, no versions */
44 const PLUGIN_STATUS_NODB
= 'nodb';
45 /** the plugin is up-to-date */
46 const PLUGIN_STATUS_UPTODATE
= 'uptodate';
47 /** the plugin is about to be installed */
48 const PLUGIN_STATUS_NEW
= 'new';
49 /** the plugin is about to be upgraded */
50 const PLUGIN_STATUS_UPGRADE
= 'upgrade';
51 /** the standard plugin is about to be deleted */
52 const PLUGIN_STATUS_DELETE
= 'delete';
53 /** the version at the disk is lower than the one already installed */
54 const PLUGIN_STATUS_DOWNGRADE
= 'downgrade';
55 /** the plugin is installed but missing from disk */
56 const PLUGIN_STATUS_MISSING
= 'missing';
58 /** @var plugin_manager holds the singleton instance */
59 protected static $singletoninstance;
60 /** @var array of raw plugins information */
61 protected $pluginsinfo = null;
62 /** @var array of raw subplugins information */
63 protected $subpluginsinfo = null;
66 * Direct initiation not allowed, use the factory method {@link self::instance()}
68 protected function __construct() {
72 * Sorry, this is singleton
74 protected function __clone() {
78 * Factory method for this class
80 * @return plugin_manager the singleton instance
82 public static function instance() {
83 if (is_null(self
::$singletoninstance)) {
84 self
::$singletoninstance = new self();
86 return self
::$singletoninstance;
91 * @param bool $phpunitreset
93 public static function reset_caches($phpunitreset = false) {
95 self
::$singletoninstance = null;
100 * Returns a tree of known plugins and information about them
102 * @param bool $disablecache force reload, cache can be used otherwise
103 * @return array 2D array. The first keys are plugin type names (e.g. qtype);
104 * the second keys are the plugin local name (e.g. multichoice); and
105 * the values are the corresponding objects extending {@link plugininfo_base}
107 public function get_plugins($disablecache=false) {
110 if ($disablecache or is_null($this->pluginsinfo
)) {
111 // Hack: include mod and editor subplugin management classes first,
112 // the adminlib.php is supposed to contain extra admin settings too.
113 require_once($CFG->libdir
.'/adminlib.php');
114 foreach(array('mod', 'editor') as $type) {
115 foreach (get_plugin_list($type) as $dir) {
116 if (file_exists("$dir/adminlib.php")) {
117 include_once("$dir/adminlib.php");
121 $this->pluginsinfo
= array();
122 $plugintypes = get_plugin_types();
123 $plugintypes = $this->reorder_plugin_types($plugintypes);
124 foreach ($plugintypes as $plugintype => $plugintyperootdir) {
125 if (in_array($plugintype, array('base', 'general'))) {
126 throw new coding_exception('Illegal usage of reserved word for plugin type');
128 if (class_exists('plugininfo_' . $plugintype)) {
129 $plugintypeclass = 'plugininfo_' . $plugintype;
131 $plugintypeclass = 'plugininfo_general';
133 if (!in_array('plugininfo_base', class_parents($plugintypeclass))) {
134 throw new coding_exception('Class ' . $plugintypeclass . ' must extend plugininfo_base');
136 $plugins = call_user_func(array($plugintypeclass, 'get_plugins'), $plugintype, $plugintyperootdir, $plugintypeclass);
137 $this->pluginsinfo
[$plugintype] = $plugins;
140 if (empty($CFG->disableupdatenotifications
) and !during_initial_install()) {
141 // append the information about available updates provided by {@link available_update_checker()}
142 $provider = available_update_checker
::instance();
143 foreach ($this->pluginsinfo
as $plugintype => $plugins) {
144 foreach ($plugins as $plugininfoholder) {
145 $plugininfoholder->check_available_updates($provider);
151 return $this->pluginsinfo
;
155 * Returns list of plugins that define their subplugins and the information
156 * about them from the db/subplugins.php file.
158 * At the moment, only activity modules and editors can define subplugins.
160 * @param bool $disablecache force reload, cache can be used otherwise
161 * @return array with keys like 'mod_quiz', and values the data from the
162 * corresponding db/subplugins.php file.
164 public function get_subplugins($disablecache=false) {
166 if ($disablecache or is_null($this->subpluginsinfo
)) {
167 $this->subpluginsinfo
= array();
168 foreach (array('mod', 'editor') as $type) {
169 $owners = get_plugin_list($type);
170 foreach ($owners as $component => $ownerdir) {
171 $componentsubplugins = array();
172 if (file_exists($ownerdir . '/db/subplugins.php')) {
173 $subplugins = array();
174 include($ownerdir . '/db/subplugins.php');
175 foreach ($subplugins as $subplugintype => $subplugintyperootdir) {
176 $subplugin = new stdClass();
177 $subplugin->type
= $subplugintype;
178 $subplugin->typerootdir
= $subplugintyperootdir;
179 $componentsubplugins[$subplugintype] = $subplugin;
181 $this->subpluginsinfo
[$type . '_' . $component] = $componentsubplugins;
187 return $this->subpluginsinfo
;
191 * Returns the name of the plugin that defines the given subplugin type
193 * If the given subplugin type is not actually a subplugin, returns false.
195 * @param string $subplugintype the name of subplugin type, eg. workshopform or quiz
196 * @return false|string the name of the parent plugin, eg. mod_workshop
198 public function get_parent_of_subplugin($subplugintype) {
201 foreach ($this->get_subplugins() as $pluginname => $subplugintypes) {
202 if (isset($subplugintypes[$subplugintype])) {
203 $parent = $pluginname;
212 * Returns a localized name of a given plugin
214 * @param string $plugin name of the plugin, eg mod_workshop or auth_ldap
217 public function plugin_name($plugin) {
218 list($type, $name) = normalize_component($plugin);
219 return $this->pluginsinfo
[$type][$name]->displayname
;
223 * Returns a localized name of a plugin type in plural form
225 * Most plugin types define their names in core_plugin lang file. In case of subplugins,
226 * we try to ask the parent plugin for the name. In the worst case, we will return
227 * the value of the passed $type parameter.
229 * @param string $type the type of the plugin, e.g. mod or workshopform
232 public function plugintype_name_plural($type) {
234 if (get_string_manager()->string_exists('type_' . $type . '_plural', 'core_plugin')) {
235 // for most plugin types, their names are defined in core_plugin lang file
236 return get_string('type_' . $type . '_plural', 'core_plugin');
238 } else if ($parent = $this->get_parent_of_subplugin($type)) {
239 // if this is a subplugin, try to ask the parent plugin for the name
240 if (get_string_manager()->string_exists('subplugintype_' . $type . '_plural', $parent)) {
241 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type . '_plural', $parent);
243 return $this->plugin_name($parent) . ' / ' . $type;
252 * @param string $component frankenstyle component name.
253 * @return plugininfo_base|null the corresponding plugin information.
255 public function get_plugin_info($component) {
256 list($type, $name) = normalize_component($component);
257 $plugins = $this->get_plugins();
258 if (isset($plugins[$type][$name])) {
259 return $plugins[$type][$name];
266 * Get a list of any other plugins that require this one.
267 * @param string $component frankenstyle component name.
268 * @return array of frankensyle component names that require this one.
270 public function other_plugins_that_require($component) {
272 foreach ($this->get_plugins() as $type => $plugins) {
273 foreach ($plugins as $plugin) {
274 $required = $plugin->get_other_required_plugins();
275 if (isset($required[$component])) {
276 $others[] = $plugin->component
;
284 * Check a dependencies list against the list of installed plugins.
285 * @param array $dependencies compenent name to required version or ANY_VERSION.
286 * @return bool true if all the dependencies are satisfied.
288 public function are_dependencies_satisfied($dependencies) {
289 foreach ($dependencies as $component => $requiredversion) {
290 $otherplugin = $this->get_plugin_info($component);
291 if (is_null($otherplugin)) {
295 if ($requiredversion != ANY_VERSION
and $otherplugin->versiondisk
< $requiredversion) {
304 * Checks all dependencies for all installed plugins
306 * This is used by install and upgrade. The array passed by reference as the second
307 * argument is populated with the list of plugins that have failed dependencies (note that
308 * a single plugin can appear multiple times in the $failedplugins).
310 * @param int $moodleversion the version from version.php.
311 * @param array $failedplugins to return the list of plugins with non-satisfied dependencies
312 * @return bool true if all the dependencies are satisfied for all plugins.
314 public function all_plugins_ok($moodleversion, &$failedplugins = array()) {
317 foreach ($this->get_plugins() as $type => $plugins) {
318 foreach ($plugins as $plugin) {
320 if (!$plugin->is_core_dependency_satisfied($moodleversion)) {
322 $failedplugins[] = $plugin->component
;
325 if (!$this->are_dependencies_satisfied($plugin->get_other_required_plugins())) {
327 $failedplugins[] = $plugin->component
;
336 * Checks if there are some plugins with a known available update
338 * @return bool true if there is at least one available update
340 public function some_plugins_updatable() {
341 foreach ($this->get_plugins() as $type => $plugins) {
342 foreach ($plugins as $plugin) {
343 if ($plugin->available_updates()) {
353 * Defines a list of all plugins that were originally shipped in the standard Moodle distribution,
354 * but are not anymore and are deleted during upgrades.
356 * The main purpose of this list is to hide missing plugins during upgrade.
358 * @param string $type plugin type
359 * @param string $name plugin name
362 public static function is_deleted_standard_plugin($type, $name) {
363 static $plugins = array(
364 // do not add 1.9-2.2 plugin removals here
367 if (!isset($plugins[$type])) {
370 return in_array($name, $plugins[$type]);
374 * Defines a white list of all plugins shipped in the standard Moodle distribution
376 * @param string $type
377 * @return false|array array of standard plugins or false if the type is unknown
379 public static function standard_plugins_list($type) {
380 static $standard_plugins = array(
382 'assignment' => array(
383 'offline', 'online', 'upload', 'uploadsingle'
386 'assignsubmission' => array(
387 'comments', 'file', 'onlinetext'
390 'assignfeedback' => array(
391 'comments', 'file', 'offline'
395 'cas', 'db', 'email', 'fc', 'imap', 'ldap', 'manual', 'mnet',
396 'nntp', 'nologin', 'none', 'pam', 'pop3', 'radius',
397 'shibboleth', 'webservice'
401 'activity_modules', 'admin_bookmarks', 'blog_menu',
402 'blog_recent', 'blog_tags', 'calendar_month',
403 'calendar_upcoming', 'comments', 'community',
404 'completionstatus', 'course_list', 'course_overview',
405 'course_summary', 'feedback', 'glossary_random', 'html',
406 'login', 'mentees', 'messages', 'mnet_hosts', 'myprofile',
407 'navigation', 'news_items', 'online_users', 'participants',
408 'private_files', 'quiz_results', 'recent_activity',
409 'rss_client', 'search_forums', 'section_links',
410 'selfcompletion', 'settings', 'site_main_menu',
411 'social_activities', 'tag_flickr', 'tag_youtube', 'tags'
415 'exportimscp', 'importhtml', 'print'
418 'cachelock' => array(
422 'cachestore' => array(
423 'file', 'memcache', 'memcached', 'mongodb', 'session', 'static'
426 'coursereport' => array(
430 'datafield' => array(
431 'checkbox', 'date', 'file', 'latlong', 'menu', 'multimenu',
432 'number', 'picture', 'radiobutton', 'text', 'textarea', 'url'
435 'datapreset' => array(
440 'textarea', 'tinymce'
444 'authorize', 'category', 'cohort', 'database', 'flatfile',
445 'guest', 'imsenterprise', 'ldap', 'manual', 'meta', 'mnet',
450 'activitynames', 'algebra', 'censor', 'emailprotect',
451 'emoticon', 'mediaplugin', 'multilang', 'tex', 'tidy',
452 'urltolink', 'data', 'glossary'
456 'scorm', 'social', 'topics', 'weeks'
459 'gradeexport' => array(
460 'ods', 'txt', 'xls', 'xml'
463 'gradeimport' => array(
467 'gradereport' => array(
468 'grader', 'outcomes', 'overview', 'user'
471 'gradingform' => array(
479 'email', 'jabber', 'popup'
482 'mnetservice' => array(
487 'assign', 'assignment', 'book', 'chat', 'choice', 'data', 'feedback', 'folder',
488 'forum', 'glossary', 'imscp', 'label', 'lesson', 'lti', 'page',
489 'quiz', 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop'
492 'plagiarism' => array(
495 'portfolio' => array(
496 'boxnet', 'download', 'flickr', 'googledocs', 'mahara', 'picasa'
499 'profilefield' => array(
500 'checkbox', 'datetime', 'menu', 'text', 'textarea'
503 'qbehaviour' => array(
504 'adaptive', 'adaptivenopenalty', 'deferredcbm',
505 'deferredfeedback', 'immediatecbm', 'immediatefeedback',
506 'informationitem', 'interactive', 'interactivecountback',
507 'manualgraded', 'missing'
511 'aiken', 'blackboard', 'blackboard_six', 'examview', 'gift',
512 'learnwise', 'missingword', 'multianswer', 'webct',
517 'calculated', 'calculatedmulti', 'calculatedsimple',
518 'description', 'essay', 'match', 'missingtype', 'multianswer',
519 'multichoice', 'numerical', 'random', 'randomsamatch',
520 'shortanswer', 'truefalse'
524 'grading', 'overview', 'responses', 'statistics'
527 'quizaccess' => array(
528 'delaybetweenattempts', 'ipaddress', 'numattempts', 'openclosedate',
529 'password', 'safebrowser', 'securewindow', 'timelimit'
533 'backups', 'completion', 'configlog', 'courseoverview',
534 'log', 'loglive', 'outline', 'participation', 'progress', 'questioninstances', 'security', 'stats'
537 'repository' => array(
538 'alfresco', 'boxnet', 'coursefiles', 'dropbox', 'equella', 'filesystem',
539 'flickr', 'flickr_public', 'googledocs', 'local', 'merlot',
540 'picasa', 'recent', 's3', 'upload', 'url', 'user', 'webdav',
541 'wikimedia', 'youtube'
544 'scormreport' => array(
551 'dragmath', 'moodleemoticon', 'moodleimage', 'moodlemedia', 'moodlenolink', 'spellchecker',
555 'afterburner', 'anomaly', 'arialist', 'base', 'binarius',
556 'boxxie', 'brick', 'canvas', 'formal_white', 'formfactor',
557 'fusion', 'leatherbound', 'magazine', 'mymobile', 'nimble',
558 'nonzero', 'overlay', 'serenity', 'sky_high', 'splash',
559 'standard', 'standardold'
563 'assignmentupgrade', 'capability', 'customlang', 'dbtransfer', 'generator',
564 'health', 'innodb', 'langimport', 'multilangupgrade', 'phpunit', 'profiling',
565 'qeupgradehelper', 'replace', 'spamcleaner', 'timezoneimport', 'unittest',
566 'uploaduser', 'unsuproles', 'xmldb'
569 'webservice' => array(
570 'amf', 'rest', 'soap', 'xmlrpc'
573 'workshopallocation' => array(
574 'manual', 'random', 'scheduled'
577 'workshopeval' => array(
581 'workshopform' => array(
582 'accumulative', 'comments', 'numerrors', 'rubric'
586 if (isset($standard_plugins[$type])) {
587 return $standard_plugins[$type];
594 * Reorders plugin types into a sequence to be displayed
596 * For technical reasons, plugin types returned by {@link get_plugin_types()} are
597 * in a certain order that does not need to fit the expected order for the display.
598 * Particularly, activity modules should be displayed first as they represent the
599 * real heart of Moodle. They should be followed by other plugin types that are
600 * used to build the courses (as that is what one expects from LMS). After that,
601 * other supportive plugin types follow.
603 * @param array $types associative array
604 * @return array same array with altered order of items
606 protected function reorder_plugin_types(array $types) {
608 'mod' => $types['mod'],
609 'block' => $types['block'],
610 'qtype' => $types['qtype'],
611 'qbehaviour' => $types['qbehaviour'],
612 'qformat' => $types['qformat'],
613 'filter' => $types['filter'],
614 'enrol' => $types['enrol'],
616 foreach ($types as $type => $path) {
617 if (!isset($fix[$type])) {
627 * General exception thrown by the {@link available_update_checker} class
629 class available_update_checker_exception
extends moodle_exception
{
632 * @param string $errorcode exception description identifier
633 * @param mixed $debuginfo debugging data to display
635 public function __construct($errorcode, $debuginfo=null) {
636 parent
::__construct($errorcode, 'core_plugin', '', null, print_r($debuginfo, true));
642 * Singleton class that handles checking for available updates
644 class available_update_checker
{
646 /** @var available_update_checker holds the singleton instance */
647 protected static $singletoninstance;
648 /** @var null|int the timestamp of when the most recent response was fetched */
649 protected $recentfetch = null;
650 /** @var null|array the recent response from the update notification provider */
651 protected $recentresponse = null;
652 /** @var null|string the numerical version of the local Moodle code */
653 protected $currentversion = null;
654 /** @var null|string the release info of the local Moodle code */
655 protected $currentrelease = null;
656 /** @var null|string branch of the local Moodle code */
657 protected $currentbranch = null;
658 /** @var array of (string)frankestyle => (string)version list of additional plugins deployed at this site */
659 protected $currentplugins = array();
662 * Direct initiation not allowed, use the factory method {@link self::instance()}
664 protected function __construct() {
668 * Sorry, this is singleton
670 protected function __clone() {
674 * Factory method for this class
676 * @return available_update_checker the singleton instance
678 public static function instance() {
679 if (is_null(self
::$singletoninstance)) {
680 self
::$singletoninstance = new self();
682 return self
::$singletoninstance;
687 * @param bool $phpunitreset
689 public static function reset_caches($phpunitreset = false) {
691 self
::$singletoninstance = null;
696 * Returns the timestamp of the last execution of {@link fetch()}
698 * @return int|null null if it has never been executed or we don't known
700 public function get_last_timefetched() {
702 $this->restore_response();
704 if (!empty($this->recentfetch
)) {
705 return $this->recentfetch
;
713 * Fetches the available update status from the remote site
715 * @throws available_update_checker_exception
717 public function fetch() {
718 $response = $this->get_response();
719 $this->validate_response($response);
720 $this->store_response($response);
724 * Returns the available update information for the given component
726 * This method returns null if the most recent response does not contain any information
727 * about it. The returned structure is an array of available updates for the given
728 * component. Each update info is an object with at least one property called
729 * 'version'. Other possible properties are 'release', 'maturity', 'url' and 'downloadurl'.
731 * For the 'core' component, the method returns real updates only (those with higher version).
732 * For all other components, the list of all known remote updates is returned and the caller
733 * (usually the {@link plugin_manager}) is supposed to make the actual comparison of versions.
735 * @param string $component frankenstyle
736 * @param array $options with supported keys 'minmaturity' and/or 'notifybuilds'
737 * @return null|array null or array of available_update_info objects
739 public function get_update_info($component, array $options = array()) {
741 if (!isset($options['minmaturity'])) {
742 $options['minmaturity'] = 0;
745 if (!isset($options['notifybuilds'])) {
746 $options['notifybuilds'] = false;
749 if ($component == 'core') {
750 $this->load_current_environment();
753 $this->restore_response();
755 if (empty($this->recentresponse
['updates'][$component])) {
760 foreach ($this->recentresponse
['updates'][$component] as $info) {
761 $update = new available_update_info($component, $info);
762 if (isset($update->maturity
) and ($update->maturity
< $options['minmaturity'])) {
765 if ($component == 'core') {
766 if ($update->version
<= $this->currentversion
) {
769 if (empty($options['notifybuilds']) and $this->is_same_release($update->release
)) {
773 $updates[] = $update;
776 if (empty($updates)) {
784 * The method being run via cron.php
786 public function cron() {
789 if (!$this->cron_autocheck_enabled()) {
790 $this->cron_mtrace('Automatic check for available updates not enabled, skipping.');
794 $now = $this->cron_current_timestamp();
796 if ($this->cron_has_fresh_fetch($now)) {
797 $this->cron_mtrace('Recently fetched info about available updates is still fresh enough, skipping.');
801 if ($this->cron_has_outdated_fetch($now)) {
802 $this->cron_mtrace('Outdated or missing info about available updates, forced fetching ... ', '');
803 $this->cron_execute();
807 $offset = $this->cron_execution_offset();
808 $start = mktime(1, 0, 0, date('n', $now), date('j', $now), date('Y', $now)); // 01:00 AM today local time
809 if ($now > $start +
$offset) {
810 $this->cron_mtrace('Regular daily check for available updates ... ', '');
811 $this->cron_execute();
816 /// end of public API //////////////////////////////////////////////////////
819 * Makes cURL request to get data from the remote site
821 * @return string raw request result
822 * @throws available_update_checker_exception
824 protected function get_response() {
826 require_once($CFG->libdir
.'/filelib.php');
828 $curl = new curl(array('proxy' => true));
829 $response = $curl->post($this->prepare_request_url(), $this->prepare_request_params(), $this->prepare_request_options());
830 $curlerrno = $curl->get_errno();
831 if (!empty($curlerrno)) {
832 throw new available_update_checker_exception('err_response_curl', 'cURL error '.$curlerrno.': '.$curl->error
);
834 $curlinfo = $curl->get_info();
835 if ($curlinfo['http_code'] != 200) {
836 throw new available_update_checker_exception('err_response_http_code', $curlinfo['http_code']);
842 * Makes sure the response is valid, has correct API format etc.
844 * @param string $response raw response as returned by the {@link self::get_response()}
845 * @throws available_update_checker_exception
847 protected function validate_response($response) {
849 $response = $this->decode_response($response);
851 if (empty($response)) {
852 throw new available_update_checker_exception('err_response_empty');
855 if (empty($response['status']) or $response['status'] !== 'OK') {
856 throw new available_update_checker_exception('err_response_status', $response['status']);
859 if (empty($response['apiver']) or $response['apiver'] !== '1.1') {
860 throw new available_update_checker_exception('err_response_format_version', $response['apiver']);
863 if (empty($response['forbranch']) or $response['forbranch'] !== moodle_major_version(true)) {
864 throw new available_update_checker_exception('err_response_target_version', $response['forbranch']);
869 * Decodes the raw string response from the update notifications provider
871 * @param string $response as returned by {@link self::get_response()}
872 * @return array decoded response structure
874 protected function decode_response($response) {
875 return json_decode($response, true);
879 * Stores the valid fetched response for later usage
881 * This implementation uses the config_plugins table as the permanent storage.
883 * @param string $response raw valid data returned by {@link self::get_response()}
885 protected function store_response($response) {
887 set_config('recentfetch', time(), 'core_plugin');
888 set_config('recentresponse', $response, 'core_plugin');
890 $this->restore_response(true);
894 * Loads the most recent raw response record we have fetched
896 * After this method is called, $this->recentresponse is set to an array. If the
897 * array is empty, then either no data have been fetched yet or the fetched data
898 * do not have expected format (and thence they are ignored and a debugging
899 * message is displayed).
901 * This implementation uses the config_plugins table as the permanent storage.
903 * @param bool $forcereload reload even if it was already loaded
905 protected function restore_response($forcereload = false) {
907 if (!$forcereload and !is_null($this->recentresponse
)) {
908 // we already have it, nothing to do
912 $config = get_config('core_plugin');
914 if (!empty($config->recentresponse
) and !empty($config->recentfetch
)) {
916 $this->validate_response($config->recentresponse
);
917 $this->recentfetch
= $config->recentfetch
;
918 $this->recentresponse
= $this->decode_response($config->recentresponse
);
919 } catch (available_update_checker_exception
$e) {
920 // The server response is not valid. Behave as if no data were fetched yet.
921 // This may happen when the most recent update info (cached locally) has been
922 // fetched with the previous branch of Moodle (like during an upgrade from 2.x
923 // to 2.y) or when the API of the response has changed.
924 $this->recentresponse
= array();
928 $this->recentresponse
= array();
933 * Compares two raw {@link $recentresponse} records and returns the list of changed updates
935 * This method is used to populate potential update info to be sent to site admins.
939 * @throws available_update_checker_exception
940 * @return array parts of $new['updates'] that have changed
942 protected function compare_responses(array $old, array $new) {
948 if (!array_key_exists('updates', $new)) {
949 throw new available_update_checker_exception('err_response_format');
953 return $new['updates'];
956 if (!array_key_exists('updates', $old)) {
957 throw new available_update_checker_exception('err_response_format');
962 foreach ($new['updates'] as $newcomponent => $newcomponentupdates) {
963 if (empty($old['updates'][$newcomponent])) {
964 $changes[$newcomponent] = $newcomponentupdates;
967 foreach ($newcomponentupdates as $newcomponentupdate) {
969 foreach ($old['updates'][$newcomponent] as $oldcomponentupdate) {
970 if ($newcomponentupdate['version'] == $oldcomponentupdate['version']) {
975 if (!isset($changes[$newcomponent])) {
976 $changes[$newcomponent] = array();
978 $changes[$newcomponent][] = $newcomponentupdate;
987 * Returns the URL to send update requests to
989 * During the development or testing, you can set $CFG->alternativeupdateproviderurl
990 * to a custom URL that will be used. Otherwise the standard URL will be returned.
994 protected function prepare_request_url() {
997 if (!empty($CFG->config_php_settings
['alternativeupdateproviderurl'])) {
998 return $CFG->config_php_settings
['alternativeupdateproviderurl'];
1000 return 'https://download.moodle.org/api/1.1/updates.php';
1005 * Sets the properties currentversion, currentrelease, currentbranch and currentplugins
1007 * @param bool $forcereload
1009 protected function load_current_environment($forcereload=false) {
1012 if (!is_null($this->currentversion
) and !$forcereload) {
1020 require($CFG->dirroot
.'/version.php');
1021 $this->currentversion
= $version;
1022 $this->currentrelease
= $release;
1023 $this->currentbranch
= moodle_major_version(true);
1025 $pluginman = plugin_manager
::instance();
1026 foreach ($pluginman->get_plugins() as $type => $plugins) {
1027 foreach ($plugins as $plugin) {
1028 if (!$plugin->is_standard()) {
1029 $this->currentplugins
[$plugin->component
] = $plugin->versiondisk
;
1036 * Returns the list of HTTP params to be sent to the updates provider URL
1038 * @return array of (string)param => (string)value
1040 protected function prepare_request_params() {
1043 $this->load_current_environment();
1044 $this->restore_response();
1047 $params['format'] = 'json';
1049 if (isset($this->recentresponse
['ticket'])) {
1050 $params['ticket'] = $this->recentresponse
['ticket'];
1053 if (isset($this->currentversion
)) {
1054 $params['version'] = $this->currentversion
;
1056 throw new coding_exception('Main Moodle version must be already known here');
1059 if (isset($this->currentbranch
)) {
1060 $params['branch'] = $this->currentbranch
;
1062 throw new coding_exception('Moodle release must be already known here');
1066 foreach ($this->currentplugins
as $plugin => $version) {
1067 $plugins[] = $plugin.'@'.$version;
1069 if (!empty($plugins)) {
1070 $params['plugins'] = implode(',', $plugins);
1077 * Returns the list of cURL options to use when fetching available updates data
1079 * @return array of (string)param => (string)value
1081 protected function prepare_request_options() {
1085 'CURLOPT_SSL_VERIFYHOST' => 2, // this is the default in {@link curl} class but just in case
1086 'CURLOPT_SSL_VERIFYPEER' => true,
1089 $cacertfile = $CFG->dataroot
.'/moodleorgca.crt';
1090 if (is_readable($cacertfile)) {
1091 // Do not use CA certs provided by the operating system. Instead,
1092 // use this CA cert to verify the updates provider.
1093 $options['CURLOPT_CAINFO'] = $cacertfile;
1100 * Returns the current timestamp
1102 * @return int the timestamp
1104 protected function cron_current_timestamp() {
1109 * Output cron debugging info
1112 * @param string $msg output message
1113 * @param string $eol end of line
1115 protected function cron_mtrace($msg, $eol = PHP_EOL
) {
1120 * Decide if the autocheck feature is disabled in the server setting
1122 * @return bool true if autocheck enabled, false if disabled
1124 protected function cron_autocheck_enabled() {
1127 if (empty($CFG->updateautocheck
)) {
1135 * Decide if the recently fetched data are still fresh enough
1137 * @param int $now current timestamp
1138 * @return bool true if no need to re-fetch, false otherwise
1140 protected function cron_has_fresh_fetch($now) {
1141 $recent = $this->get_last_timefetched();
1143 if (empty($recent)) {
1147 if ($now < $recent) {
1148 $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1152 if ($now - $recent > 24 * HOURSECS
) {
1160 * Decide if the fetch is outadated or even missing
1162 * @param int $now current timestamp
1163 * @return bool false if no need to re-fetch, true otherwise
1165 protected function cron_has_outdated_fetch($now) {
1166 $recent = $this->get_last_timefetched();
1168 if (empty($recent)) {
1172 if ($now < $recent) {
1173 $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1177 if ($now - $recent > 48 * HOURSECS
) {
1185 * Returns the cron execution offset for this site
1187 * The main {@link self::cron()} is supposed to run every night in some random time
1188 * between 01:00 and 06:00 AM (local time). The exact moment is defined by so called
1189 * execution offset, that is the amount of time after 01:00 AM. The offset value is
1190 * initially generated randomly and then used consistently at the site. This way, the
1191 * regular checks against the download.moodle.org server are spread in time.
1193 * @return int the offset number of seconds from range 1 sec to 5 hours
1195 protected function cron_execution_offset() {
1198 if (empty($CFG->updatecronoffset
)) {
1199 set_config('updatecronoffset', rand(1, 5 * HOURSECS
));
1202 return $CFG->updatecronoffset
;
1206 * Fetch available updates info and eventually send notification to site admins
1208 protected function cron_execute() {
1211 $this->restore_response();
1212 $previous = $this->recentresponse
;
1214 $this->restore_response(true);
1215 $current = $this->recentresponse
;
1216 $changes = $this->compare_responses($previous, $current);
1217 $notifications = $this->cron_notifications($changes);
1218 $this->cron_notify($notifications);
1219 $this->cron_mtrace('done');
1220 } catch (available_update_checker_exception
$e) {
1221 $this->cron_mtrace('FAILED!');
1226 * Given the list of changes in available updates, pick those to send to site admins
1228 * @param array $changes as returned by {@link self::compare_responses()}
1229 * @return array of available_update_info objects to send to site admins
1231 protected function cron_notifications(array $changes) {
1234 $notifications = array();
1235 $pluginman = plugin_manager
::instance();
1236 $plugins = $pluginman->get_plugins(true);
1238 foreach ($changes as $component => $componentchanges) {
1239 if (empty($componentchanges)) {
1242 $componentupdates = $this->get_update_info($component,
1243 array('minmaturity' => $CFG->updateminmaturity
, 'notifybuilds' => $CFG->updatenotifybuilds
));
1244 if (empty($componentupdates)) {
1247 // notify only about those $componentchanges that are present in $componentupdates
1248 // to respect the preferences
1249 foreach ($componentchanges as $componentchange) {
1250 foreach ($componentupdates as $componentupdate) {
1251 if ($componentupdate->version
== $componentchange['version']) {
1252 if ($component == 'core') {
1253 // In case of 'core', we already know that the $componentupdate
1254 // is a real update with higher version ({@see self::get_update_info()}).
1255 // We just perform additional check for the release property as there
1256 // can be two Moodle releases having the same version (e.g. 2.4.0 and 2.5dev shortly
1257 // after the release). We can do that because we have the release info
1258 // always available for the core.
1259 if ((string)$componentupdate->release
=== (string)$componentchange['release']) {
1260 $notifications[] = $componentupdate;
1263 // Use the plugin_manager to check if the detected $componentchange
1264 // is a real update with higher version. That is, the $componentchange
1265 // is present in the array of {@link available_update_info} objects
1266 // returned by the plugin's available_updates() method.
1267 list($plugintype, $pluginname) = normalize_component($component);
1268 if (!empty($plugins[$plugintype][$pluginname])) {
1269 $availableupdates = $plugins[$plugintype][$pluginname]->available_updates();
1270 if (!empty($availableupdates)) {
1271 foreach ($availableupdates as $availableupdate) {
1272 if ($availableupdate->version
== $componentchange['version']) {
1273 $notifications[] = $componentupdate;
1284 return $notifications;
1288 * Sends the given notifications to site admins via messaging API
1290 * @param array $notifications array of available_update_info objects to send
1292 protected function cron_notify(array $notifications) {
1295 if (empty($notifications)) {
1299 $admins = get_admins();
1301 if (empty($admins)) {
1305 $this->cron_mtrace('sending notifications ... ', '');
1307 $text = get_string('updatenotifications', 'core_admin') . PHP_EOL
;
1308 $html = html_writer
::tag('h1', get_string('updatenotifications', 'core_admin')) . PHP_EOL
;
1310 $coreupdates = array();
1311 $pluginupdates = array();
1313 foreach ($notifications as $notification) {
1314 if ($notification->component
== 'core') {
1315 $coreupdates[] = $notification;
1317 $pluginupdates[] = $notification;
1321 if (!empty($coreupdates)) {
1322 $text .= PHP_EOL
. get_string('updateavailable', 'core_admin') . PHP_EOL
;
1323 $html .= html_writer
::tag('h2', get_string('updateavailable', 'core_admin')) . PHP_EOL
;
1324 $html .= html_writer
::start_tag('ul') . PHP_EOL
;
1325 foreach ($coreupdates as $coreupdate) {
1326 $html .= html_writer
::start_tag('li');
1327 if (isset($coreupdate->release
)) {
1328 $text .= get_string('updateavailable_release', 'core_admin', $coreupdate->release
);
1329 $html .= html_writer
::tag('strong', get_string('updateavailable_release', 'core_admin', $coreupdate->release
));
1331 if (isset($coreupdate->version
)) {
1332 $text .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version
);
1333 $html .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version
);
1335 if (isset($coreupdate->maturity
)) {
1336 $text .= ' ('.get_string('maturity'.$coreupdate->maturity
, 'core_admin').')';
1337 $html .= ' ('.get_string('maturity'.$coreupdate->maturity
, 'core_admin').')';
1340 $html .= html_writer
::end_tag('li') . PHP_EOL
;
1343 $html .= html_writer
::end_tag('ul') . PHP_EOL
;
1345 $a = array('url' => $CFG->wwwroot
.'/'.$CFG->admin
.'/index.php');
1346 $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL
;
1347 $a = array('url' => html_writer
::link($CFG->wwwroot
.'/'.$CFG->admin
.'/index.php', $CFG->wwwroot
.'/'.$CFG->admin
.'/index.php'));
1348 $html .= html_writer
::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL
;
1351 if (!empty($pluginupdates)) {
1352 $text .= PHP_EOL
. get_string('updateavailableforplugin', 'core_admin') . PHP_EOL
;
1353 $html .= html_writer
::tag('h2', get_string('updateavailableforplugin', 'core_admin')) . PHP_EOL
;
1355 $html .= html_writer
::start_tag('ul') . PHP_EOL
;
1356 foreach ($pluginupdates as $pluginupdate) {
1357 $html .= html_writer
::start_tag('li');
1358 $text .= get_string('pluginname', $pluginupdate->component
);
1359 $html .= html_writer
::tag('strong', get_string('pluginname', $pluginupdate->component
));
1361 $text .= ' ('.$pluginupdate->component
.')';
1362 $html .= ' ('.$pluginupdate->component
.')';
1364 $text .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version
);
1365 $html .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version
);
1368 $html .= html_writer
::end_tag('li') . PHP_EOL
;
1371 $html .= html_writer
::end_tag('ul') . PHP_EOL
;
1373 $a = array('url' => $CFG->wwwroot
.'/'.$CFG->admin
.'/plugins.php');
1374 $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL
;
1375 $a = array('url' => html_writer
::link($CFG->wwwroot
.'/'.$CFG->admin
.'/plugins.php', $CFG->wwwroot
.'/'.$CFG->admin
.'/plugins.php'));
1376 $html .= html_writer
::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL
;
1379 $a = array('siteurl' => $CFG->wwwroot
);
1380 $text .= get_string('updatenotificationfooter', 'core_admin', $a) . PHP_EOL
;
1381 $a = array('siteurl' => html_writer
::link($CFG->wwwroot
, $CFG->wwwroot
));
1382 $html .= html_writer
::tag('footer', html_writer
::tag('p', get_string('updatenotificationfooter', 'core_admin', $a),
1383 array('style' => 'font-size:smaller; color:#333;')));
1385 foreach ($admins as $admin) {
1386 $message = new stdClass();
1387 $message->component
= 'moodle';
1388 $message->name
= 'availableupdate';
1389 $message->userfrom
= get_admin();
1390 $message->userto
= $admin;
1391 $message->subject
= get_string('updatenotificationsubject', 'core_admin', array('siteurl' => $CFG->wwwroot
));
1392 $message->fullmessage
= $text;
1393 $message->fullmessageformat
= FORMAT_PLAIN
;
1394 $message->fullmessagehtml
= $html;
1395 $message->smallmessage
= get_string('updatenotifications', 'core_admin');
1396 $message->notification
= 1;
1397 message_send($message);
1402 * Compare two release labels and decide if they are the same
1404 * @param string $remote release info of the available update
1405 * @param null|string $local release info of the local code, defaults to $release defined in version.php
1406 * @return boolean true if the releases declare the same minor+major version
1408 protected function is_same_release($remote, $local=null) {
1410 if (is_null($local)) {
1411 $this->load_current_environment();
1412 $local = $this->currentrelease
;
1415 $pattern = '/^([0-9\.\+]+)([^(]*)/';
1417 preg_match($pattern, $remote, $remotematches);
1418 preg_match($pattern, $local, $localmatches);
1420 $remotematches[1] = str_replace('+', '', $remotematches[1]);
1421 $localmatches[1] = str_replace('+', '', $localmatches[1]);
1423 if ($remotematches[1] === $localmatches[1] and rtrim($remotematches[2]) === rtrim($localmatches[2])) {
1433 * Defines the structure of objects returned by {@link available_update_checker::get_update_info()}
1435 class available_update_info
{
1437 /** @var string frankenstyle component name */
1439 /** @var int the available version of the component */
1441 /** @var string|null optional release name */
1442 public $release = null;
1443 /** @var int|null optional maturity info, eg {@link MATURITY_STABLE} */
1444 public $maturity = null;
1445 /** @var string|null optional URL of a page with more info about the update */
1447 /** @var string|null optional URL of a ZIP package that can be downloaded and installed */
1448 public $download = null;
1449 /** @var string|null of self::download is set, then this must be the MD5 hash of the ZIP */
1450 public $downloadmd5 = null;
1453 * Creates new instance of the class
1455 * The $info array must provide at least the 'version' value and optionally all other
1456 * values to populate the object's properties.
1458 * @param string $name the frankenstyle component name
1459 * @param array $info associative array with other properties
1461 public function __construct($name, array $info) {
1462 $this->component
= $name;
1463 foreach ($info as $k => $v) {
1464 if (property_exists('available_update_info', $k) and $k != 'component') {
1473 * Implements a communication bridge to the mdeploy.php utility
1475 class available_update_deployer
{
1477 const HTTP_PARAM_PREFIX
= 'updteautodpldata_'; // Hey, even Google has not heard of such a prefix! So it MUST be safe :-p
1478 const HTTP_PARAM_CHECKER
= 'datapackagesize'; // Name of the parameter that holds the number of items in the received data items
1480 /** @var available_update_deployer holds the singleton instance */
1481 protected static $singletoninstance;
1482 /** @var moodle_url URL of a page that includes the deployer UI */
1483 protected $callerurl;
1484 /** @var moodle_url URL to return after the deployment */
1485 protected $returnurl;
1488 * Direct instantiation not allowed, use the factory method {@link self::instance()}
1490 protected function __construct() {
1494 * Sorry, this is singleton
1496 protected function __clone() {
1500 * Factory method for this class
1502 * @return available_update_deployer the singleton instance
1504 public static function instance() {
1505 if (is_null(self
::$singletoninstance)) {
1506 self
::$singletoninstance = new self();
1508 return self
::$singletoninstance;
1512 * Reset caches used by this script
1514 * @param bool $phpunitreset is this called as a part of PHPUnit reset?
1516 public static function reset_caches($phpunitreset = false) {
1517 if ($phpunitreset) {
1518 self
::$singletoninstance = null;
1523 * Is automatic deployment enabled?
1527 public function enabled() {
1530 if (!empty($CFG->disableupdateautodeploy
)) {
1531 // The feature is prohibited via config.php
1535 return get_config('updateautodeploy');
1539 * Sets some base properties of the class to make it usable.
1541 * @param moodle_url $callerurl the base URL of a script that will handle the class'es form data
1542 * @param moodle_url $returnurl the final URL to return to when the deployment is finished
1544 public function initialize(moodle_url
$callerurl, moodle_url
$returnurl) {
1546 if (!$this->enabled()) {
1547 throw new coding_exception('Unable to initialize the deployer, the feature is not enabled.');
1550 $this->callerurl
= $callerurl;
1551 $this->returnurl
= $returnurl;
1555 * Has the deployer been initialized?
1557 * Initialized deployer means that the following properties were set:
1558 * callerurl, returnurl
1562 public function initialized() {
1564 if (!$this->enabled()) {
1568 if (empty($this->callerurl
)) {
1572 if (empty($this->returnurl
)) {
1580 * Returns a list of reasons why the deployment can not happen
1582 * If the returned array is empty, the deployment seems to be possible. The returned
1583 * structure is an associative array with keys representing individual impediments.
1584 * Possible keys are: missingdownloadurl, missingdownloadmd5, notwritable.
1586 * @param available_update_info $info
1589 public function deployment_impediments(available_update_info
$info) {
1591 $impediments = array();
1593 if (empty($info->download
)) {
1594 $impediments['missingdownloadurl'] = true;
1597 if (empty($info->downloadmd5
)) {
1598 $impediments['missingdownloadmd5'] = true;
1601 if (!empty($info->download
) and !$this->update_downloadable($info->download
)) {
1602 $impediments['notdownloadable'] = true;
1605 if (!$this->component_writable($info->component
)) {
1606 $impediments['notwritable'] = true;
1609 return $impediments;
1613 * Check to see if the current version of the plugin seems to be a checkout of an external repository.
1615 * @param available_update_info $info
1616 * @return false|string
1618 public function plugin_external_source(available_update_info
$info) {
1620 $paths = get_plugin_types(true);
1621 list($plugintype, $pluginname) = normalize_component($info->component
);
1622 $pluginroot = $paths[$plugintype].'/'.$pluginname;
1624 if (is_dir($pluginroot.'/.git')) {
1628 if (is_dir($pluginroot.'/CVS')) {
1632 if (is_dir($pluginroot.'/.svn')) {
1640 * Prepares a renderable widget to confirm installation of an available update.
1642 * @param available_update_info $info component version to deploy
1643 * @return renderable
1645 public function make_confirm_widget(available_update_info
$info) {
1647 if (!$this->initialized()) {
1648 throw new coding_exception('Illegal method call - deployer not initialized.');
1651 $params = $this->data_to_params(array(
1652 'updateinfo' => (array)$info, // see http://www.php.net/manual/en/language.types.array.php#language.types.array.casting
1655 $widget = new single_button(
1656 new moodle_url($this->callerurl
, $params),
1657 get_string('updateavailableinstall', 'core_admin'),
1665 * Prepares a renderable widget to execute installation of an available update.
1667 * @param available_update_info $info component version to deploy
1668 * @return renderable
1670 public function make_execution_widget(available_update_info
$info) {
1673 if (!$this->initialized()) {
1674 throw new coding_exception('Illegal method call - deployer not initialized.');
1677 $pluginrootpaths = get_plugin_types(true);
1679 list($plugintype, $pluginname) = normalize_component($info->component
);
1681 if (empty($pluginrootpaths[$plugintype])) {
1682 throw new coding_exception('Unknown plugin type root location', $plugintype);
1685 list($passfile, $password) = $this->prepare_authorization();
1687 $upgradeurl = new moodle_url('/admin');
1691 'type' => $plugintype,
1692 'name' => $pluginname,
1693 'typeroot' => $pluginrootpaths[$plugintype],
1694 'package' => $info->download
,
1695 'md5' => $info->downloadmd5
,
1696 'dataroot' => $CFG->dataroot
,
1697 'dirroot' => $CFG->dirroot
,
1698 'passfile' => $passfile,
1699 'password' => $password,
1700 'returnurl' => $upgradeurl->out(true),
1703 if (!empty($CFG->proxyhost
)) {
1704 // MDL-36973 - Beware - we should call just !is_proxybypass() here. But currently, our
1705 // cURL wrapper class does not do it. So, to have consistent behaviour, we pass proxy
1706 // setting regardless the $CFG->proxybypass setting. Once the {@link curl} class is
1707 // fixed, the condition should be amended.
1708 if (true or !is_proxybypass($info->download
)) {
1709 if (empty($CFG->proxyport
)) {
1710 $params['proxy'] = $CFG->proxyhost
;
1712 $params['proxy'] = $CFG->proxyhost
.':'.$CFG->proxyport
;
1715 if (!empty($CFG->proxyuser
) and !empty($CFG->proxypassword
)) {
1716 $params['proxyuserpwd'] = $CFG->proxyuser
.':'.$CFG->proxypassword
;
1719 if (!empty($CFG->proxytype
)) {
1720 $params['proxytype'] = $CFG->proxytype
;
1725 $widget = new single_button(
1726 new moodle_url('/mdeploy.php', $params),
1727 get_string('updateavailableinstall', 'core_admin'),
1735 * Returns array of data objects passed to this tool.
1739 public function submitted_data() {
1741 $data = $this->params_to_data($_POST);
1743 if (empty($data) or empty($data[self
::HTTP_PARAM_CHECKER
])) {
1747 if (!empty($data['updateinfo']) and is_object($data['updateinfo'])) {
1748 $updateinfo = $data['updateinfo'];
1749 if (!empty($updateinfo->component
) and !empty($updateinfo->version
)) {
1750 $data['updateinfo'] = new available_update_info($updateinfo->component
, (array)$updateinfo);
1754 if (!empty($data['callerurl'])) {
1755 $data['callerurl'] = new moodle_url($data['callerurl']);
1758 if (!empty($data['returnurl'])) {
1759 $data['returnurl'] = new moodle_url($data['returnurl']);
1766 * Handles magic getters and setters for protected properties.
1768 * @param string $name method name, e.g. set_returnurl()
1769 * @param array $arguments arguments to be passed to the array
1771 public function __call($name, array $arguments = array()) {
1773 if (substr($name, 0, 4) === 'set_') {
1774 $property = substr($name, 4);
1775 if (empty($property)) {
1776 throw new coding_exception('Invalid property name (empty)');
1778 if (empty($arguments)) {
1779 $arguments = array(true); // Default value for flag-like properties.
1781 // Make sure it is a protected property.
1782 $isprotected = false;
1783 $reflection = new ReflectionObject($this);
1784 foreach ($reflection->getProperties(ReflectionProperty
::IS_PROTECTED
) as $reflectionproperty) {
1785 if ($reflectionproperty->getName() === $property) {
1786 $isprotected = true;
1790 if (!$isprotected) {
1791 throw new coding_exception('Unable to set property - it does not exist or it is not protected');
1793 $value = reset($arguments);
1794 $this->$property = $value;
1798 if (substr($name, 0, 4) === 'get_') {
1799 $property = substr($name, 4);
1800 if (empty($property)) {
1801 throw new coding_exception('Invalid property name (empty)');
1803 if (!empty($arguments)) {
1804 throw new coding_exception('No parameter expected');
1806 // Make sure it is a protected property.
1807 $isprotected = false;
1808 $reflection = new ReflectionObject($this);
1809 foreach ($reflection->getProperties(ReflectionProperty
::IS_PROTECTED
) as $reflectionproperty) {
1810 if ($reflectionproperty->getName() === $property) {
1811 $isprotected = true;
1815 if (!$isprotected) {
1816 throw new coding_exception('Unable to get property - it does not exist or it is not protected');
1818 return $this->$property;
1823 * Generates a random token and stores it in a file in moodledata directory.
1825 * @return array of the (string)filename and (string)password in this order
1827 public function prepare_authorization() {
1830 make_upload_directory('mdeploy/auth/');
1835 while (!$success and $attempts < 5) {
1838 $passfile = $this->generate_passfile();
1839 $password = $this->generate_password();
1842 $filepath = $CFG->dataroot
.'/mdeploy/auth/'.$passfile;
1844 if (!file_exists($filepath)) {
1845 $success = file_put_contents($filepath, $password . PHP_EOL
. $now . PHP_EOL
, LOCK_EX
);
1850 return array($passfile, $password);
1853 throw new moodle_exception('unable_prepare_authorization', 'core_plugin');
1857 // End of external API
1860 * Prepares an array of HTTP parameters that can be passed to another page.
1862 * @param array|object $data associative array or an object holding the data, data JSON-able
1863 * @return array suitable as a param for moodle_url
1865 protected function data_to_params($data) {
1867 // Append some our own data
1868 if (!empty($this->callerurl
)) {
1869 $data['callerurl'] = $this->callerurl
->out(false);
1871 if (!empty($this->callerurl
)) {
1872 $data['returnurl'] = $this->returnurl
->out(false);
1875 // Finally append the count of items in the package.
1876 $data[self
::HTTP_PARAM_CHECKER
] = count($data);
1880 foreach ($data as $name => $value) {
1881 $transname = self
::HTTP_PARAM_PREFIX
.$name;
1882 $transvalue = json_encode($value);
1883 $params[$transname] = $transvalue;
1890 * Converts HTTP parameters passed to the script into native PHP data
1892 * @param array $params such as $_REQUEST or $_POST
1893 * @return array data passed for this class
1895 protected function params_to_data(array $params) {
1897 if (empty($params)) {
1902 foreach ($params as $name => $value) {
1903 if (strpos($name, self
::HTTP_PARAM_PREFIX
) === 0) {
1904 $realname = substr($name, strlen(self
::HTTP_PARAM_PREFIX
));
1905 $realvalue = json_decode($value);
1906 $data[$realname] = $realvalue;
1914 * Returns a random string to be used as a filename of the password storage.
1918 protected function generate_passfile() {
1919 return clean_param(uniqid('mdeploy_', true), PARAM_FILE
);
1923 * Returns a random string to be used as the authorization token
1927 protected function generate_password() {
1928 return complex_random_string();
1932 * Checks if the given component's directory is writable
1934 * For the purpose of the deployment, the web server process has to have
1935 * write access to all files in the component's directory (recursively) and for the
1938 * @see worker::move_directory_source_precheck()
1939 * @param string $component normalized component name
1942 protected function component_writable($component) {
1944 list($plugintype, $pluginname) = normalize_component($component);
1946 $directory = get_plugin_directory($plugintype, $pluginname);
1948 if (is_null($directory)) {
1949 throw new coding_exception('Unknown component location', $component);
1952 return $this->directory_writable($directory);
1956 * Checks if the mdeploy.php will be able to fetch the ZIP from the given URL
1958 * This is mainly supposed to check if the transmission over HTTPS would
1959 * work. That is, if the CA certificates are present at the server.
1961 * @param string $downloadurl the URL of the ZIP package to download
1964 protected function update_downloadable($downloadurl) {
1967 $curloptions = array(
1968 'CURLOPT_SSL_VERIFYHOST' => 2, // this is the default in {@link curl} class but just in case
1969 'CURLOPT_SSL_VERIFYPEER' => true,
1972 $cacertfile = $CFG->dataroot
.'/moodleorgca.crt';
1973 if (is_readable($cacertfile)) {
1974 // Do not use CA certs provided by the operating system. Instead,
1975 // use this CA cert to verify the updates provider.
1976 $curloptions['CURLOPT_CAINFO'] = $cacertfile;
1979 $curl = new curl(array('proxy' => true));
1980 $result = $curl->head($downloadurl, $curloptions);
1981 $errno = $curl->get_errno();
1982 if (empty($errno)) {
1990 * Checks if the directory and all its contents (recursively) is writable
1992 * @param string $path full path to a directory
1995 private function directory_writable($path) {
1997 if (!is_writable($path)) {
2001 if (is_dir($path)) {
2002 $handle = opendir($path);
2009 while ($filename = readdir($handle)) {
2010 $filepath = $path.'/'.$filename;
2012 if ($filename === '.' or $filename === '..') {
2016 if (is_dir($filepath)) {
2017 $result = $result && $this->directory_writable($filepath);
2020 $result = $result && is_writable($filepath);
2032 * Factory class producing required subclasses of {@link plugininfo_base}
2034 class plugininfo_default_factory
{
2037 * Makes a new instance of the plugininfo class
2039 * @param string $type the plugin type, eg. 'mod'
2040 * @param string $typerootdir full path to the location of all the plugins of this type
2041 * @param string $name the plugin name, eg. 'workshop'
2042 * @param string $namerootdir full path to the location of the plugin
2043 * @param string $typeclass the name of class that holds the info about the plugin
2044 * @return plugininfo_base the instance of $typeclass
2046 public static function make($type, $typerootdir, $name, $namerootdir, $typeclass) {
2047 $plugin = new $typeclass();
2048 $plugin->type
= $type;
2049 $plugin->typerootdir
= $typerootdir;
2050 $plugin->name
= $name;
2051 $plugin->rootdir
= $namerootdir;
2053 $plugin->init_display_name();
2054 $plugin->load_disk_version();
2055 $plugin->load_db_version();
2056 $plugin->load_required_main_version();
2057 $plugin->init_is_standard();
2065 * Base class providing access to the information about a plugin
2067 * @property-read string component the component name, type_name
2069 abstract class plugininfo_base
{
2071 /** @var string the plugintype name, eg. mod, auth or workshopform */
2073 /** @var string full path to the location of all the plugins of this type */
2074 public $typerootdir;
2075 /** @var string the plugin name, eg. assignment, ldap */
2077 /** @var string the localized plugin name */
2078 public $displayname;
2079 /** @var string the plugin source, one of plugin_manager::PLUGIN_SOURCE_xxx constants */
2081 /** @var fullpath to the location of this plugin */
2083 /** @var int|string the version of the plugin's source code */
2084 public $versiondisk;
2085 /** @var int|string the version of the installed plugin */
2087 /** @var int|float|string required version of Moodle core */
2088 public $versionrequires;
2089 /** @var array other plugins that this one depends on, lazy-loaded by {@link get_other_required_plugins()} */
2090 public $dependencies;
2091 /** @var int number of instances of the plugin - not supported yet */
2093 /** @var int order of the plugin among other plugins of the same type - not supported yet */
2095 /** @var array|null array of {@link available_update_info} for this plugin */
2096 public $availableupdates;
2099 * Gathers and returns the information about all plugins of the given type
2101 * @param string $type the name of the plugintype, eg. mod, auth or workshopform
2102 * @param string $typerootdir full path to the location of the plugin dir
2103 * @param string $typeclass the name of the actually called class
2104 * @return array of plugintype classes, indexed by the plugin name
2106 public static function get_plugins($type, $typerootdir, $typeclass) {
2108 // get the information about plugins at the disk
2109 $plugins = get_plugin_list($type);
2111 foreach ($plugins as $pluginname => $pluginrootdir) {
2112 $ondisk[$pluginname] = plugininfo_default_factory
::make($type, $typerootdir,
2113 $pluginname, $pluginrootdir, $typeclass);
2119 * Sets {@link $displayname} property to a localized name of the plugin
2121 public function init_display_name() {
2122 if (!get_string_manager()->string_exists('pluginname', $this->component
)) {
2123 $this->displayname
= '[pluginname,' . $this->component
. ']';
2125 $this->displayname
= get_string('pluginname', $this->component
);
2130 * Magic method getter, redirects to read only values.
2132 * @param string $name
2135 public function __get($name) {
2137 case 'component': return $this->type
. '_' . $this->name
;
2140 debugging('Invalid plugin property accessed! '.$name);
2146 * Return the full path name of a file within the plugin.
2148 * No check is made to see if the file exists.
2150 * @param string $relativepath e.g. 'version.php'.
2151 * @return string e.g. $CFG->dirroot . '/mod/quiz/version.php'.
2153 public function full_path($relativepath) {
2154 if (empty($this->rootdir
)) {
2157 return $this->rootdir
. '/' . $relativepath;
2161 * Load the data from version.php.
2163 * @return stdClass the object called $plugin defined in version.php
2165 protected function load_version_php() {
2166 $versionfile = $this->full_path('version.php');
2168 $plugin = new stdClass();
2169 if (is_readable($versionfile)) {
2170 include($versionfile);
2176 * Sets {@link $versiondisk} property to a numerical value representing the
2177 * version of the plugin's source code.
2179 * If the value is null after calling this method, either the plugin
2180 * does not use versioning (typically does not have any database
2181 * data) or is missing from disk.
2183 public function load_disk_version() {
2184 $plugin = $this->load_version_php();
2185 if (isset($plugin->version
)) {
2186 $this->versiondisk
= $plugin->version
;
2191 * Sets {@link $versionrequires} property to a numerical value representing
2192 * the version of Moodle core that this plugin requires.
2194 public function load_required_main_version() {
2195 $plugin = $this->load_version_php();
2196 if (isset($plugin->requires
)) {
2197 $this->versionrequires
= $plugin->requires
;
2202 * Initialise {@link $dependencies} to the list of other plugins (in any)
2203 * that this one requires to be installed.
2205 protected function load_other_required_plugins() {
2206 $plugin = $this->load_version_php();
2207 if (!empty($plugin->dependencies
)) {
2208 $this->dependencies
= $plugin->dependencies
;
2210 $this->dependencies
= array(); // By default, no dependencies.
2215 * Get the list of other plugins that this plugin requires to be installed.
2217 * @return array with keys the frankenstyle plugin name, and values either
2218 * a version string (like '2011101700') or the constant ANY_VERSION.
2220 public function get_other_required_plugins() {
2221 if (is_null($this->dependencies
)) {
2222 $this->load_other_required_plugins();
2224 return $this->dependencies
;
2228 * Sets {@link $versiondb} property to a numerical value representing the
2229 * currently installed version of the plugin.
2231 * If the value is null after calling this method, either the plugin
2232 * does not use versioning (typically does not have any database
2233 * data) or has not been installed yet.
2235 public function load_db_version() {
2236 if ($ver = self
::get_version_from_config_plugins($this->component
)) {
2237 $this->versiondb
= $ver;
2242 * Sets {@link $source} property to one of plugin_manager::PLUGIN_SOURCE_xxx
2245 * If the property's value is null after calling this method, then
2246 * the type of the plugin has not been recognized and you should throw
2249 public function init_is_standard() {
2251 $standard = plugin_manager
::standard_plugins_list($this->type
);
2253 if ($standard !== false) {
2254 $standard = array_flip($standard);
2255 if (isset($standard[$this->name
])) {
2256 $this->source
= plugin_manager
::PLUGIN_SOURCE_STANDARD
;
2257 } else if (!is_null($this->versiondb
) and is_null($this->versiondisk
)
2258 and plugin_manager
::is_deleted_standard_plugin($this->type
, $this->name
)) {
2259 $this->source
= plugin_manager
::PLUGIN_SOURCE_STANDARD
; // to be deleted
2261 $this->source
= plugin_manager
::PLUGIN_SOURCE_EXTENSION
;
2267 * Returns true if the plugin is shipped with the official distribution
2268 * of the current Moodle version, false otherwise.
2272 public function is_standard() {
2273 return $this->source
=== plugin_manager
::PLUGIN_SOURCE_STANDARD
;
2277 * Returns true if the the given Moodle version is enough to run this plugin
2279 * @param string|int|double $moodleversion
2282 public function is_core_dependency_satisfied($moodleversion) {
2284 if (empty($this->versionrequires
)) {
2288 return (double)$this->versionrequires
<= (double)$moodleversion;
2293 * Returns the status of the plugin
2295 * @return string one of plugin_manager::PLUGIN_STATUS_xxx constants
2297 public function get_status() {
2299 if (is_null($this->versiondb
) and is_null($this->versiondisk
)) {
2300 return plugin_manager
::PLUGIN_STATUS_NODB
;
2302 } else if (is_null($this->versiondb
) and !is_null($this->versiondisk
)) {
2303 return plugin_manager
::PLUGIN_STATUS_NEW
;
2305 } else if (!is_null($this->versiondb
) and is_null($this->versiondisk
)) {
2306 if (plugin_manager
::is_deleted_standard_plugin($this->type
, $this->name
)) {
2307 return plugin_manager
::PLUGIN_STATUS_DELETE
;
2309 return plugin_manager
::PLUGIN_STATUS_MISSING
;
2312 } else if ((string)$this->versiondb
=== (string)$this->versiondisk
) {
2313 return plugin_manager
::PLUGIN_STATUS_UPTODATE
;
2315 } else if ($this->versiondb
< $this->versiondisk
) {
2316 return plugin_manager
::PLUGIN_STATUS_UPGRADE
;
2318 } else if ($this->versiondb
> $this->versiondisk
) {
2319 return plugin_manager
::PLUGIN_STATUS_DOWNGRADE
;
2322 // $version = pi(); and similar funny jokes - hopefully Donald E. Knuth will never contribute to Moodle ;-)
2323 throw new coding_exception('Unable to determine plugin state, check the plugin versions');
2328 * Returns the information about plugin availability
2330 * True means that the plugin is enabled. False means that the plugin is
2331 * disabled. Null means that the information is not available, or the
2332 * plugin does not support configurable availability or the availability
2333 * can not be changed.
2337 public function is_enabled() {
2342 * Populates the property {@link $availableupdates} with the information provided by
2343 * available update checker
2345 * @param available_update_checker $provider the class providing the available update info
2347 public function check_available_updates(available_update_checker
$provider) {
2350 if (isset($CFG->updateminmaturity
)) {
2351 $minmaturity = $CFG->updateminmaturity
;
2353 // this can happen during the very first upgrade to 2.3
2354 $minmaturity = MATURITY_STABLE
;
2357 $this->availableupdates
= $provider->get_update_info($this->component
,
2358 array('minmaturity' => $minmaturity));
2362 * If there are updates for this plugin available, returns them.
2364 * Returns array of {@link available_update_info} objects, if some update
2365 * is available. Returns null if there is no update available or if the update
2366 * availability is unknown.
2368 * @return array|null
2370 public function available_updates() {
2372 if (empty($this->availableupdates
) or !is_array($this->availableupdates
)) {
2378 foreach ($this->availableupdates
as $availableupdate) {
2379 if ($availableupdate->version
> $this->versiondisk
) {
2380 $updates[] = $availableupdate;
2384 if (empty($updates)) {
2392 * Returns the node name used in admin settings menu for this plugin settings (if applicable)
2394 * @return null|string node name or null if plugin does not create settings node (default)
2396 public function get_settings_section_name() {
2401 * Returns the URL of the plugin settings screen
2403 * Null value means that the plugin either does not have the settings screen
2404 * or its location is not available via this library.
2406 * @return null|moodle_url
2408 public function get_settings_url() {
2409 $section = $this->get_settings_section_name();
2410 if ($section === null) {
2413 $settings = admin_get_root()->locate($section);
2414 if ($settings && $settings instanceof admin_settingpage
) {
2415 return new moodle_url('/admin/settings.php', array('section' => $section));
2416 } else if ($settings && $settings instanceof admin_externalpage
) {
2417 return new moodle_url($settings->url
);
2424 * Loads plugin settings to the settings tree
2426 * This function usually includes settings.php file in plugins folder.
2427 * Alternatively it can create a link to some settings page (instance of admin_externalpage)
2429 * @param part_of_admin_tree $adminroot
2430 * @param string $parentnodename
2431 * @param bool $hassiteconfig whether the current user has moodle/site:config capability
2433 public function load_settings(part_of_admin_tree
$adminroot, $parentnodename, $hassiteconfig) {
2437 * Returns the URL of the screen where this plugin can be uninstalled
2439 * Visiting that URL must be safe, that is a manual confirmation is needed
2440 * for actual uninstallation of the plugin. Null value means that the
2441 * plugin either does not support uninstallation, or does not require any
2442 * database cleanup or the location of the screen is not available via this
2445 * @return null|moodle_url
2447 public function get_uninstall_url() {
2452 * Returns relative directory of the plugin with heading '/'
2456 public function get_dir() {
2459 return substr($this->rootdir
, strlen($CFG->dirroot
));
2463 * Provides access to plugin versions from {config_plugins}
2465 * @param string $plugin plugin name
2466 * @param double $disablecache optional, defaults to false
2467 * @return int|false the stored value or false if not found
2469 protected function get_version_from_config_plugins($plugin, $disablecache=false) {
2471 static $pluginversions = null;
2473 if (is_null($pluginversions) or $disablecache) {
2475 $pluginversions = $DB->get_records_menu('config_plugins', array('name' => 'version'), 'plugin', 'plugin,value');
2476 } catch (dml_exception
$e) {
2478 $pluginversions = array();
2482 if (!array_key_exists($plugin, $pluginversions)) {
2486 return $pluginversions[$plugin];
2492 * General class for all plugin types that do not have their own class
2494 class plugininfo_general
extends plugininfo_base
{
2499 * Class for page side blocks
2501 class plugininfo_block
extends plugininfo_base
{
2503 public static function get_plugins($type, $typerootdir, $typeclass) {
2505 // get the information about blocks at the disk
2506 $blocks = parent
::get_plugins($type, $typerootdir, $typeclass);
2508 // add blocks missing from disk
2509 $blocksinfo = self
::get_blocks_info();
2510 foreach ($blocksinfo as $blockname => $blockinfo) {
2511 if (isset($blocks[$blockname])) {
2514 $plugin = new $typeclass();
2515 $plugin->type
= $type;
2516 $plugin->typerootdir
= $typerootdir;
2517 $plugin->name
= $blockname;
2518 $plugin->rootdir
= null;
2519 $plugin->displayname
= $blockname;
2520 $plugin->versiondb
= $blockinfo->version
;
2521 $plugin->init_is_standard();
2523 $blocks[$blockname] = $plugin;
2530 * Magic method getter, redirects to read only values.
2532 * For block plugins pretends the object has 'visible' property for compatibility
2533 * with plugins developed for Moodle version below 2.4
2535 * @param string $name
2538 public function __get($name) {
2539 if ($name === 'visible') {
2540 debugging('This is now an instance of plugininfo_block, please use $block->is_enabled() instead of $block->visible', DEBUG_DEVELOPER
);
2541 return ($this->is_enabled() !== false);
2543 return parent
::__get($name);
2546 public function init_display_name() {
2548 if (get_string_manager()->string_exists('pluginname', 'block_' . $this->name
)) {
2549 $this->displayname
= get_string('pluginname', 'block_' . $this->name
);
2551 } else if (($block = block_instance($this->name
)) !== false) {
2552 $this->displayname
= $block->get_title();
2555 parent
::init_display_name();
2559 public function load_db_version() {
2562 $blocksinfo = self
::get_blocks_info();
2563 if (isset($blocksinfo[$this->name
]->version
)) {
2564 $this->versiondb
= $blocksinfo[$this->name
]->version
;
2568 public function is_enabled() {
2570 $blocksinfo = self
::get_blocks_info();
2571 if (isset($blocksinfo[$this->name
]->visible
)) {
2572 if ($blocksinfo[$this->name
]->visible
) {
2578 return parent
::is_enabled();
2582 public function get_settings_section_name() {
2583 return 'blocksetting' . $this->name
;
2586 public function load_settings(part_of_admin_tree
$adminroot, $parentnodename, $hassiteconfig) {
2587 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
2588 $ADMIN = $adminroot; // may be used in settings.php
2589 $block = $this; // also can be used inside settings.php
2590 $section = $this->get_settings_section_name();
2592 if (!$hassiteconfig ||
(($blockinstance = block_instance($this->name
)) === false)) {
2597 if ($blockinstance->has_config()) {
2598 if (file_exists($this->full_path('settings.php'))) {
2599 $settings = new admin_settingpage($section, $this->displayname
,
2600 'moodle/site:config', $this->is_enabled() === false);
2601 include($this->full_path('settings.php')); // this may also set $settings to null
2603 $blocksinfo = self
::get_blocks_info();
2604 $settingsurl = new moodle_url('/admin/block.php', array('block' => $blocksinfo[$this->name
]->id
));
2605 $settings = new admin_externalpage($section, $this->displayname
,
2606 $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
2610 $ADMIN->add($parentnodename, $settings);
2614 public function get_uninstall_url() {
2616 $blocksinfo = self
::get_blocks_info();
2617 return new moodle_url('/admin/blocks.php', array('delete' => $blocksinfo[$this->name
]->id
, 'sesskey' => sesskey()));
2621 * Provides access to the records in {block} table
2623 * @param bool $disablecache do not use internal static cache
2624 * @return array array of stdClasses
2626 protected static function get_blocks_info($disablecache=false) {
2628 static $blocksinfocache = null;
2630 if (is_null($blocksinfocache) or $disablecache) {
2632 $blocksinfocache = $DB->get_records('block', null, 'name', 'name,id,version,visible');
2633 } catch (dml_exception
$e) {
2635 $blocksinfocache = array();
2639 return $blocksinfocache;
2645 * Class for text filters
2647 class plugininfo_filter
extends plugininfo_base
{
2649 public static function get_plugins($type, $typerootdir, $typeclass) {
2654 // get the list of filters from both /filter and /mod location
2655 $installed = filter_get_all_installed();
2657 foreach ($installed as $filterlegacyname => $displayname) {
2658 $plugin = new $typeclass();
2659 $plugin->type
= $type;
2660 $plugin->typerootdir
= $typerootdir;
2661 $plugin->name
= self
::normalize_legacy_name($filterlegacyname);
2662 $plugin->rootdir
= $CFG->dirroot
. '/' . $filterlegacyname;
2663 $plugin->displayname
= $displayname;
2665 $plugin->load_disk_version();
2666 $plugin->load_db_version();
2667 $plugin->load_required_main_version();
2668 $plugin->init_is_standard();
2670 $filters[$plugin->name
] = $plugin;
2673 $globalstates = self
::get_global_states();
2675 if ($DB->get_manager()->table_exists('filter_active')) {
2676 // if we're upgrading from 1.9, the table does not exist yet
2677 // if it does, make sure that all installed filters are registered
2678 $needsreload = false;
2679 foreach (array_keys($installed) as $filterlegacyname) {
2680 if (!isset($globalstates[self
::normalize_legacy_name($filterlegacyname)])) {
2681 filter_set_global_state($filterlegacyname, TEXTFILTER_DISABLED
);
2682 $needsreload = true;
2686 $globalstates = self
::get_global_states(true);
2690 // make sure that all registered filters are installed, just in case
2691 foreach ($globalstates as $name => $info) {
2692 if (!isset($filters[$name])) {
2693 // oops, there is a record in filter_active but the filter is not installed
2694 $plugin = new $typeclass();
2695 $plugin->type
= $type;
2696 $plugin->typerootdir
= $typerootdir;
2697 $plugin->name
= $name;
2698 $plugin->rootdir
= $CFG->dirroot
. '/' . $info->legacyname
;
2699 $plugin->displayname
= $info->legacyname
;
2701 $plugin->load_db_version();
2703 if (is_null($plugin->versiondb
)) {
2704 // this is a hack to stimulate 'Missing from disk' error
2705 // because $plugin->versiondisk will be null !== false
2706 $plugin->versiondb
= false;
2709 $filters[$plugin->name
] = $plugin;
2716 public function init_display_name() {
2717 // do nothing, the name is set in self::get_plugins()
2721 * @see load_version_php()
2723 protected function load_version_php() {
2724 if (strpos($this->name
, 'mod_') === 0) {
2725 // filters bundled with modules do not have a version.php and so
2726 // do not provide their own versioning information.
2727 return new stdClass();
2729 return parent
::load_version_php();
2732 public function is_enabled() {
2734 $globalstates = self
::get_global_states();
2736 foreach ($globalstates as $filterlegacyname => $info) {
2737 $name = self
::normalize_legacy_name($filterlegacyname);
2738 if ($name === $this->name
) {
2739 if ($info->active
== TEXTFILTER_DISABLED
) {
2742 // it may be 'On' or 'Off, but available'
2751 public function get_settings_section_name() {
2752 $globalstates = self
::get_global_states();
2753 if (!isset($globalstates[$this->name
])) {
2754 return parent
::get_settings_section_name();
2756 $legacyname = $globalstates[$this->name
]->legacyname
;
2757 return 'filtersetting' . str_replace('/', '', $legacyname);
2760 public function load_settings(part_of_admin_tree
$adminroot, $parentnodename, $hassiteconfig) {
2761 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
2762 $ADMIN = $adminroot; // may be used in settings.php
2763 $filter = $this; // also can be used inside settings.php
2765 $globalstates = self
::get_global_states();
2767 if ($hassiteconfig && isset($globalstates[$this->name
]) && file_exists($this->full_path('filtersettings.php'))) {
2768 $section = $this->get_settings_section_name();
2769 $settings = new admin_settingpage($section, $this->displayname
,
2770 'moodle/site:config', $this->is_enabled() === false);
2771 include($this->full_path('filtersettings.php')); // this may also set $settings to null
2774 $ADMIN->add($parentnodename, $settings);
2778 public function get_uninstall_url() {
2780 if (strpos($this->name
, 'mod_') === 0) {
2783 $globalstates = self
::get_global_states();
2784 $legacyname = $globalstates[$this->name
]->legacyname
;
2785 return new moodle_url('/admin/filters.php', array('sesskey' => sesskey(), 'filterpath' => $legacyname, 'action' => 'delete'));
2790 * Convert legacy filter names like 'filter/foo' or 'mod/bar' into frankenstyle
2792 * @param string $legacyfiltername legacy filter name
2793 * @return string frankenstyle-like name
2795 protected static function normalize_legacy_name($legacyfiltername) {
2797 $name = str_replace('/', '_', $legacyfiltername);
2798 if (strpos($name, 'filter_') === 0) {
2799 $name = substr($name, 7);
2801 throw new coding_exception('Unable to determine filter name: ' . $legacyfiltername);
2809 * Provides access to the results of {@link filter_get_global_states()}
2810 * but indexed by the normalized filter name
2812 * The legacy filter name is available as ->legacyname property.
2814 * @param bool $disablecache
2817 protected static function get_global_states($disablecache=false) {
2819 static $globalstatescache = null;
2821 if ($disablecache or is_null($globalstatescache)) {
2823 if (!$DB->get_manager()->table_exists('filter_active')) {
2824 // we're upgrading from 1.9 and the table used by {@link filter_get_global_states()}
2825 // does not exist yet
2826 $globalstatescache = array();
2829 foreach (filter_get_global_states() as $legacyname => $info) {
2830 $name = self
::normalize_legacy_name($legacyname);
2831 $filterinfo = new stdClass();
2832 $filterinfo->legacyname
= $legacyname;
2833 $filterinfo->active
= $info->active
;
2834 $filterinfo->sortorder
= $info->sortorder
;
2835 $globalstatescache[$name] = $filterinfo;
2840 return $globalstatescache;
2846 * Class for activity modules
2848 class plugininfo_mod
extends plugininfo_base
{
2850 public static function get_plugins($type, $typerootdir, $typeclass) {
2852 // get the information about plugins at the disk
2853 $modules = parent
::get_plugins($type, $typerootdir, $typeclass);
2855 // add modules missing from disk
2856 $modulesinfo = self
::get_modules_info();
2857 foreach ($modulesinfo as $modulename => $moduleinfo) {
2858 if (isset($modules[$modulename])) {
2861 $plugin = new $typeclass();
2862 $plugin->type
= $type;
2863 $plugin->typerootdir
= $typerootdir;
2864 $plugin->name
= $modulename;
2865 $plugin->rootdir
= null;
2866 $plugin->displayname
= $modulename;
2867 $plugin->versiondb
= $moduleinfo->version
;
2868 $plugin->init_is_standard();
2870 $modules[$modulename] = $plugin;
2877 * Magic method getter, redirects to read only values.
2879 * For module plugins we pretend the object has 'visible' property for compatibility
2880 * with plugins developed for Moodle version below 2.4
2882 * @param string $name
2885 public function __get($name) {
2886 if ($name === 'visible') {
2887 debugging('This is now an instance of plugininfo_mod, please use $module->is_enabled() instead of $module->visible', DEBUG_DEVELOPER
);
2888 return ($this->is_enabled() !== false);
2890 return parent
::__get($name);
2893 public function init_display_name() {
2894 if (get_string_manager()->string_exists('pluginname', $this->component
)) {
2895 $this->displayname
= get_string('pluginname', $this->component
);
2897 $this->displayname
= get_string('modulename', $this->component
);
2902 * Load the data from version.php.
2903 * @return object the data object defined in version.php.
2905 protected function load_version_php() {
2906 $versionfile = $this->full_path('version.php');
2908 $module = new stdClass();
2909 if (is_readable($versionfile)) {
2910 include($versionfile);
2915 public function load_db_version() {
2918 $modulesinfo = self
::get_modules_info();
2919 if (isset($modulesinfo[$this->name
]->version
)) {
2920 $this->versiondb
= $modulesinfo[$this->name
]->version
;
2924 public function is_enabled() {
2926 $modulesinfo = self
::get_modules_info();
2927 if (isset($modulesinfo[$this->name
]->visible
)) {
2928 if ($modulesinfo[$this->name
]->visible
) {
2934 return parent
::is_enabled();
2938 public function get_settings_section_name() {
2939 return 'modsetting' . $this->name
;
2942 public function load_settings(part_of_admin_tree
$adminroot, $parentnodename, $hassiteconfig) {
2943 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
2944 $ADMIN = $adminroot; // may be used in settings.php
2945 $module = $this; // also can be used inside settings.php
2946 $section = $this->get_settings_section_name();
2948 $modulesinfo = self
::get_modules_info();
2950 if ($hassiteconfig && isset($modulesinfo[$this->name
]) && file_exists($this->full_path('settings.php'))) {
2951 $settings = new admin_settingpage($section, $this->displayname
,
2952 'moodle/site:config', $this->is_enabled() === false);
2953 include($this->full_path('settings.php')); // this may also set $settings to null
2956 $ADMIN->add($parentnodename, $settings);
2960 public function get_uninstall_url() {
2962 if ($this->name
!== 'forum') {
2963 return new moodle_url('/admin/modules.php', array('delete' => $this->name
, 'sesskey' => sesskey()));
2970 * Provides access to the records in {modules} table
2972 * @param bool $disablecache do not use internal static cache
2973 * @return array array of stdClasses
2975 protected static function get_modules_info($disablecache=false) {
2977 static $modulesinfocache = null;
2979 if (is_null($modulesinfocache) or $disablecache) {
2981 $modulesinfocache = $DB->get_records('modules', null, 'name', 'name,id,version,visible');
2982 } catch (dml_exception
$e) {
2984 $modulesinfocache = array();
2988 return $modulesinfocache;
2994 * Class for question behaviours.
2996 class plugininfo_qbehaviour
extends plugininfo_base
{
2998 public function get_uninstall_url() {
2999 return new moodle_url('/admin/qbehaviours.php',
3000 array('delete' => $this->name
, 'sesskey' => sesskey()));
3006 * Class for question types
3008 class plugininfo_qtype
extends plugininfo_base
{
3010 public function get_uninstall_url() {
3011 return new moodle_url('/admin/qtypes.php',
3012 array('delete' => $this->name
, 'sesskey' => sesskey()));
3015 public function get_settings_section_name() {
3016 return 'qtypesetting' . $this->name
;
3019 public function load_settings(part_of_admin_tree
$adminroot, $parentnodename, $hassiteconfig) {
3020 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3021 $ADMIN = $adminroot; // may be used in settings.php
3022 $qtype = $this; // also can be used inside settings.php
3023 $section = $this->get_settings_section_name();
3026 $systemcontext = context_system
::instance();
3027 if (($hassiteconfig ||
has_capability('moodle/question:config', $systemcontext)) &&
3028 file_exists($this->full_path('settings.php'))) {
3029 $settings = new admin_settingpage($section, $this->displayname
,
3030 'moodle/question:config', $this->is_enabled() === false);
3031 include($this->full_path('settings.php')); // this may also set $settings to null
3034 $ADMIN->add($parentnodename, $settings);
3041 * Class for authentication plugins
3043 class plugininfo_auth
extends plugininfo_base
{
3045 public function is_enabled() {
3047 /** @var null|array list of enabled authentication plugins */
3048 static $enabled = null;
3050 if (in_array($this->name
, array('nologin', 'manual'))) {
3051 // these two are always enabled and can't be disabled
3055 if (is_null($enabled)) {
3056 $enabled = array_flip(explode(',', $CFG->auth
));
3059 return isset($enabled[$this->name
]);
3062 public function get_settings_section_name() {
3063 return 'authsetting' . $this->name
;
3066 public function load_settings(part_of_admin_tree
$adminroot, $parentnodename, $hassiteconfig) {
3067 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3068 $ADMIN = $adminroot; // may be used in settings.php
3069 $auth = $this; // also to be used inside settings.php
3070 $section = $this->get_settings_section_name();
3073 if ($hassiteconfig) {
3074 if (file_exists($this->full_path('settings.php'))) {
3075 // TODO: finish implementation of common settings - locking, etc.
3076 $settings = new admin_settingpage($section, $this->displayname
,
3077 'moodle/site:config', $this->is_enabled() === false);
3078 include($this->full_path('settings.php')); // this may also set $settings to null
3080 $settingsurl = new moodle_url('/admin/auth_config.php', array('auth' => $this->name
));
3081 $settings = new admin_externalpage($section, $this->displayname
,
3082 $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
3086 $ADMIN->add($parentnodename, $settings);
3093 * Class for enrolment plugins
3095 class plugininfo_enrol
extends plugininfo_base
{
3097 public function is_enabled() {
3099 /** @var null|array list of enabled enrolment plugins */
3100 static $enabled = null;
3102 // We do not actually need whole enrolment classes here so we do not call
3103 // {@link enrol_get_plugins()}. Note that this may produce slightly different
3104 // results, for example if the enrolment plugin does not contain lib.php
3105 // but it is listed in $CFG->enrol_plugins_enabled
3107 if (is_null($enabled)) {
3108 $enabled = array_flip(explode(',', $CFG->enrol_plugins_enabled
));
3111 return isset($enabled[$this->name
]);
3114 public function get_settings_section_name() {
3115 return 'enrolsettings' . $this->name
;
3118 public function load_settings(part_of_admin_tree
$adminroot, $parentnodename, $hassiteconfig) {
3119 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3120 $ADMIN = $adminroot; // may be used in settings.php
3121 $enrol = $this; // also can be used inside settings.php
3122 $section = $this->get_settings_section_name();
3125 if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3126 $settings = new admin_settingpage($section, $this->displayname
,
3127 'moodle/site:config', $this->is_enabled() === false);
3128 include($this->full_path('settings.php')); // this may also set $settings to null
3131 $ADMIN->add($parentnodename, $settings);
3135 public function get_uninstall_url() {
3136 return new moodle_url('/admin/enrol.php', array('action' => 'uninstall', 'enrol' => $this->name
, 'sesskey' => sesskey()));
3142 * Class for messaging processors
3144 class plugininfo_message
extends plugininfo_base
{
3146 public function get_settings_section_name() {
3147 return 'messagesetting' . $this->name
;
3150 public function load_settings(part_of_admin_tree
$adminroot, $parentnodename, $hassiteconfig) {
3151 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3152 $ADMIN = $adminroot; // may be used in settings.php
3153 if (!$hassiteconfig) {
3156 $section = $this->get_settings_section_name();
3159 $processors = get_message_processors();
3160 if (isset($processors[$this->name
])) {
3161 $processor = $processors[$this->name
];
3162 if ($processor->available
&& $processor->hassettings
) {
3163 $settings = new admin_settingpage($section, $this->displayname
,
3164 'moodle/site:config', $this->is_enabled() === false);
3165 include($this->full_path('settings.php')); // this may also set $settings to null
3169 $ADMIN->add($parentnodename, $settings);
3174 * @see plugintype_interface::is_enabled()
3176 public function is_enabled() {
3177 $processors = get_message_processors();
3178 if (isset($processors[$this->name
])) {
3179 return $processors[$this->name
]->configured
&& $processors[$this->name
]->enabled
;
3181 return parent
::is_enabled();
3186 * @see plugintype_interface::get_uninstall_url()
3188 public function get_uninstall_url() {
3189 $processors = get_message_processors();
3190 if (isset($processors[$this->name
])) {
3191 return new moodle_url('/admin/message.php', array('uninstall' => $processors[$this->name
]->id
, 'sesskey' => sesskey()));
3193 return parent
::get_uninstall_url();
3200 * Class for repositories
3202 class plugininfo_repository
extends plugininfo_base
{
3204 public function is_enabled() {
3206 $enabled = self
::get_enabled_repositories();
3208 return isset($enabled[$this->name
]);
3211 public function get_settings_section_name() {
3212 return 'repositorysettings'.$this->name
;
3215 public function load_settings(part_of_admin_tree
$adminroot, $parentnodename, $hassiteconfig) {
3216 if ($hassiteconfig && $this->is_enabled()) {
3217 // completely no access to repository setting when it is not enabled
3218 $sectionname = $this->get_settings_section_name();
3219 $settingsurl = new moodle_url('/admin/repository.php',
3220 array('sesskey' => sesskey(), 'action' => 'edit', 'repos' => $this->name
));
3221 $settings = new admin_externalpage($sectionname, $this->displayname
,
3222 $settingsurl, 'moodle/site:config', false);
3223 $adminroot->add($parentnodename, $settings);
3228 * Provides access to the records in {repository} table
3230 * @param bool $disablecache do not use internal static cache
3231 * @return array array of stdClasses
3233 protected static function get_enabled_repositories($disablecache=false) {
3235 static $repositories = null;
3237 if (is_null($repositories) or $disablecache) {
3238 $repositories = $DB->get_records('repository', null, 'type', 'type,visible,sortorder');
3241 return $repositories;
3247 * Class for portfolios
3249 class plugininfo_portfolio
extends plugininfo_base
{
3251 public function is_enabled() {
3253 $enabled = self
::get_enabled_portfolios();
3255 return isset($enabled[$this->name
]);
3259 * Provides access to the records in {portfolio_instance} table
3261 * @param bool $disablecache do not use internal static cache
3262 * @return array array of stdClasses
3264 protected static function get_enabled_portfolios($disablecache=false) {
3266 static $portfolios = null;
3268 if (is_null($portfolios) or $disablecache) {
3269 $portfolios = array();
3270 $instances = $DB->get_recordset('portfolio_instance', null, 'plugin');
3271 foreach ($instances as $instance) {
3272 if (isset($portfolios[$instance->plugin
])) {
3273 if ($instance->visible
) {
3274 $portfolios[$instance->plugin
]->visible
= $instance->visible
;
3277 $portfolios[$instance->plugin
] = $instance;
3290 class plugininfo_theme
extends plugininfo_base
{
3292 public function is_enabled() {
3295 if ((!empty($CFG->theme
) and $CFG->theme
=== $this->name
) or
3296 (!empty($CFG->themelegacy
) and $CFG->themelegacy
=== $this->name
)) {
3299 return parent
::is_enabled();
3306 * Class representing an MNet service
3308 class plugininfo_mnetservice
extends plugininfo_base
{
3310 public function is_enabled() {
3313 if (empty($CFG->mnet_dispatcher_mode
) ||
$CFG->mnet_dispatcher_mode
!== 'strict') {
3316 return parent
::is_enabled();
3323 * Class for admin tool plugins
3325 class plugininfo_tool
extends plugininfo_base
{
3327 public function get_uninstall_url() {
3328 return new moodle_url('/admin/tools.php', array('delete' => $this->name
, 'sesskey' => sesskey()));
3334 * Class for admin tool plugins
3336 class plugininfo_report
extends plugininfo_base
{
3338 public function get_uninstall_url() {
3339 return new moodle_url('/admin/reports.php', array('delete' => $this->name
, 'sesskey' => sesskey()));
3345 * Class for local plugins
3347 class plugininfo_local
extends plugininfo_base
{
3349 public function get_uninstall_url() {
3350 return new moodle_url('/admin/localplugins.php', array('delete' => $this->name
, 'sesskey' => sesskey()));
3355 * Class for HTML editors
3357 class plugininfo_editor
extends plugininfo_base
{
3359 public function get_settings_section_name() {
3360 return 'editorsettings' . $this->name
;
3363 public function load_settings(part_of_admin_tree
$adminroot, $parentnodename, $hassiteconfig) {
3364 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3365 $ADMIN = $adminroot; // may be used in settings.php
3366 $editor = $this; // also can be used inside settings.php
3367 $section = $this->get_settings_section_name();
3370 if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3371 $settings = new admin_settingpage($section, $this->displayname
,
3372 'moodle/site:config', $this->is_enabled() === false);
3373 include($this->full_path('settings.php')); // this may also set $settings to null
3376 $ADMIN->add($parentnodename, $settings);
3381 * Returns the information about plugin availability
3383 * True means that the plugin is enabled. False means that the plugin is
3384 * disabled. Null means that the information is not available, or the
3385 * plugin does not support configurable availability or the availability
3386 * can not be changed.
3390 public function is_enabled() {
3392 if (empty($CFG->texteditors
)) {
3393 $CFG->texteditors
= 'tinymce,textarea';
3395 if (in_array($this->name
, explode(',', $CFG->texteditors
))) {
3403 * Class for plagiarism plugins
3405 class plugininfo_plagiarism
extends plugininfo_base
{
3407 public function get_settings_section_name() {
3408 return 'plagiarism'. $this->name
;
3411 public function load_settings(part_of_admin_tree
$adminroot, $parentnodename, $hassiteconfig) {
3412 // plagiarism plugin just redirect to settings.php in the plugins directory
3413 if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3414 $section = $this->get_settings_section_name();
3415 $settingsurl = new moodle_url($this->get_dir().'/settings.php');
3416 $settings = new admin_externalpage($section, $this->displayname
,
3417 $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
3418 $adminroot->add($parentnodename, $settings);
3424 * Class for webservice protocols
3426 class plugininfo_webservice
extends plugininfo_base
{
3428 public function get_settings_section_name() {
3429 return 'webservicesetting' . $this->name
;
3432 public function load_settings(part_of_admin_tree
$adminroot, $parentnodename, $hassiteconfig) {
3433 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3434 $ADMIN = $adminroot; // may be used in settings.php
3435 $webservice = $this; // also can be used inside settings.php
3436 $section = $this->get_settings_section_name();
3439 if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3440 $settings = new admin_settingpage($section, $this->displayname
,
3441 'moodle/site:config', $this->is_enabled() === false);
3442 include($this->full_path('settings.php')); // this may also set $settings to null
3445 $ADMIN->add($parentnodename, $settings);
3449 public function is_enabled() {
3451 if (empty($CFG->enablewebservices
)) {
3454 $active_webservices = empty($CFG->webserviceprotocols
) ?
array() : explode(',', $CFG->webserviceprotocols
);
3455 if (in_array($this->name
, $active_webservices)) {
3461 public function get_uninstall_url() {
3462 return new moodle_url('/admin/webservice/protocols.php',
3463 array('sesskey' => sesskey(), 'action' => 'uninstall', 'webservice' => $this->name
));
3468 * Class for course formats
3470 class plugininfo_format
extends plugininfo_base
{
3473 * Gathers and returns the information about all plugins of the given type
3475 * @param string $type the name of the plugintype, eg. mod, auth or workshopform
3476 * @param string $typerootdir full path to the location of the plugin dir
3477 * @param string $typeclass the name of the actually called class
3478 * @return array of plugintype classes, indexed by the plugin name
3480 public static function get_plugins($type, $typerootdir, $typeclass) {
3482 $formats = parent
::get_plugins($type, $typerootdir, $typeclass);
3483 require_once($CFG->dirroot
.'/course/lib.php');
3484 $order = get_sorted_course_formats();
3485 $sortedformats = array();
3486 foreach ($order as $formatname) {
3487 $sortedformats[$formatname] = $formats[$formatname];
3489 return $sortedformats;
3492 public function get_settings_section_name() {
3493 return 'formatsetting' . $this->name
;
3496 public function load_settings(part_of_admin_tree
$adminroot, $parentnodename, $hassiteconfig) {
3497 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3498 $ADMIN = $adminroot; // also may be used in settings.php
3499 $section = $this->get_settings_section_name();
3502 if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3503 $settings = new admin_settingpage($section, $this->displayname
,
3504 'moodle/site:config', $this->is_enabled() === false);
3505 include($this->full_path('settings.php')); // this may also set $settings to null
3508 $ADMIN->add($parentnodename, $settings);
3512 public function is_enabled() {
3513 return !get_config($this->component
, 'disabled');
3516 public function get_uninstall_url() {
3517 if ($this->name
!== get_config('moodlecourse', 'format') && $this->name
!== 'site') {
3518 return new moodle_url('/admin/courseformats.php',
3519 array('sesskey' => sesskey(), 'action' => 'uninstall', 'format' => $this->name
));
3521 return parent
::get_uninstall_url();