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 the result of {@link get_plugin_types()} ordered for humans
102 * @see self::reorder_plugin_types()
103 * @param bool $fullpaths false means relative paths from dirroot
104 * @return array (string)name => (string)location
106 public function get_plugin_types($fullpaths = true) {
107 return $this->reorder_plugin_types(get_plugin_types($fullpaths));
111 * Returns a tree of known plugins and information about them
113 * @param bool $disablecache force reload, cache can be used otherwise
114 * @return array 2D array. The first keys are plugin type names (e.g. qtype);
115 * the second keys are the plugin local name (e.g. multichoice); and
116 * the values are the corresponding objects extending {@link plugininfo_base}
118 public function get_plugins($disablecache=false) {
121 if ($disablecache or is_null($this->pluginsinfo)) {
122 // Hack: include mod and editor subplugin management classes first,
123 // the adminlib.php is supposed to contain extra admin settings too.
124 require_once($CFG->libdir.'/adminlib.php');
125 foreach(array('mod', 'editor') as $type) {
126 foreach (get_plugin_list($type) as $dir) {
127 if (file_exists("$dir/adminlib.php")) {
128 include_once("$dir/adminlib.php");
132 $this->pluginsinfo = array();
133 $plugintypes = $this->get_plugin_types();
134 foreach ($plugintypes as $plugintype => $plugintyperootdir) {
135 if (in_array($plugintype, array('base', 'general'))) {
136 throw new coding_exception('Illegal usage of reserved word for plugin type');
138 if (class_exists('plugininfo_' . $plugintype)) {
139 $plugintypeclass = 'plugininfo_' . $plugintype;
141 $plugintypeclass = 'plugininfo_general';
143 if (!in_array('plugininfo_base', class_parents($plugintypeclass))) {
144 throw new coding_exception('Class ' . $plugintypeclass . ' must extend plugininfo_base');
146 $plugins = call_user_func(array($plugintypeclass, 'get_plugins'), $plugintype, $plugintyperootdir, $plugintypeclass);
147 $this->pluginsinfo[$plugintype] = $plugins;
150 if (empty($CFG->disableupdatenotifications) and !during_initial_install()) {
151 // append the information about available updates provided by {@link available_update_checker()}
152 $provider = available_update_checker::instance();
153 foreach ($this->pluginsinfo as $plugintype => $plugins) {
154 foreach ($plugins as $plugininfoholder) {
155 $plugininfoholder->check_available_updates($provider);
161 return $this->pluginsinfo;
165 * Returns list of plugins that define their subplugins and the information
166 * about them from the db/subplugins.php file.
168 * At the moment, only activity modules and editors can define subplugins.
170 * @param bool $disablecache force reload, cache can be used otherwise
171 * @return array with keys like 'mod_quiz', and values the data from the
172 * corresponding db/subplugins.php file.
174 public function get_subplugins($disablecache=false) {
176 if ($disablecache or is_null($this->subpluginsinfo)) {
177 $this->subpluginsinfo = array();
178 foreach (array('mod', 'editor') as $type) {
179 $owners = get_plugin_list($type);
180 foreach ($owners as $component => $ownerdir) {
181 $componentsubplugins = array();
182 if (file_exists($ownerdir . '/db/subplugins.php')) {
183 $subplugins = array();
184 include($ownerdir . '/db/subplugins.php');
185 foreach ($subplugins as $subplugintype => $subplugintyperootdir) {
186 $subplugin = new stdClass();
187 $subplugin->type = $subplugintype;
188 $subplugin->typerootdir = $subplugintyperootdir;
189 $componentsubplugins[$subplugintype] = $subplugin;
191 $this->subpluginsinfo[$type . '_' . $component] = $componentsubplugins;
197 return $this->subpluginsinfo;
201 * Returns the name of the plugin that defines the given subplugin type
203 * If the given subplugin type is not actually a subplugin, returns false.
205 * @param string $subplugintype the name of subplugin type, eg. workshopform or quiz
206 * @return false|string the name of the parent plugin, eg. mod_workshop
208 public function get_parent_of_subplugin($subplugintype) {
211 foreach ($this->get_subplugins() as $pluginname => $subplugintypes) {
212 if (isset($subplugintypes[$subplugintype])) {
213 $parent = $pluginname;
222 * Returns a localized name of a given plugin
224 * @param string $plugin name of the plugin, eg mod_workshop or auth_ldap
227 public function plugin_name($plugin) {
228 list($type, $name) = normalize_component($plugin);
229 return $this->pluginsinfo[$type][$name]->displayname;
233 * Returns a localized name of a plugin typed in singular form
235 * Most plugin types define their names in core_plugin lang file. In case of subplugins,
236 * we try to ask the parent plugin for the name. In the worst case, we will return
237 * the value of the passed $type parameter.
239 * @param string $type the type of the plugin, e.g. mod or workshopform
242 public function plugintype_name($type) {
244 if (get_string_manager()->string_exists('type_' . $type, 'core_plugin')) {
245 // for most plugin types, their names are defined in core_plugin lang file
246 return get_string('type_' . $type, 'core_plugin');
248 } else if ($parent = $this->get_parent_of_subplugin($type)) {
249 // if this is a subplugin, try to ask the parent plugin for the name
250 if (get_string_manager()->string_exists('subplugintype_' . $type, $parent)) {
251 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type, $parent);
253 return $this->plugin_name($parent) . ' / ' . $type;
262 * Returns a localized name of a plugin type in plural form
264 * Most plugin types define their names in core_plugin lang file. In case of subplugins,
265 * we try to ask the parent plugin for the name. In the worst case, we will return
266 * the value of the passed $type parameter.
268 * @param string $type the type of the plugin, e.g. mod or workshopform
271 public function plugintype_name_plural($type) {
273 if (get_string_manager()->string_exists('type_' . $type . '_plural', 'core_plugin')) {
274 // for most plugin types, their names are defined in core_plugin lang file
275 return get_string('type_' . $type . '_plural', 'core_plugin');
277 } else if ($parent = $this->get_parent_of_subplugin($type)) {
278 // if this is a subplugin, try to ask the parent plugin for the name
279 if (get_string_manager()->string_exists('subplugintype_' . $type . '_plural', $parent)) {
280 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type . '_plural', $parent);
282 return $this->plugin_name($parent) . ' / ' . $type;
291 * @param string $component frankenstyle component name.
292 * @return plugininfo_base|null the corresponding plugin information.
294 public function get_plugin_info($component) {
295 list($type, $name) = normalize_component($component);
296 $plugins = $this->get_plugins();
297 if (isset($plugins[$type][$name])) {
298 return $plugins[$type][$name];
305 * Get a list of any other plugins that require this one.
306 * @param string $component frankenstyle component name.
307 * @return array of frankensyle component names that require this one.
309 public function other_plugins_that_require($component) {
311 foreach ($this->get_plugins() as $type => $plugins) {
312 foreach ($plugins as $plugin) {
313 $required = $plugin->get_other_required_plugins();
314 if (isset($required[$component])) {
315 $others[] = $plugin->component;
323 * Check a dependencies list against the list of installed plugins.
324 * @param array $dependencies compenent name to required version or ANY_VERSION.
325 * @return bool true if all the dependencies are satisfied.
327 public function are_dependencies_satisfied($dependencies) {
328 foreach ($dependencies as $component => $requiredversion) {
329 $otherplugin = $this->get_plugin_info($component);
330 if (is_null($otherplugin)) {
334 if ($requiredversion != ANY_VERSION and $otherplugin->versiondisk < $requiredversion) {
343 * Checks all dependencies for all installed plugins
345 * This is used by install and upgrade. The array passed by reference as the second
346 * argument is populated with the list of plugins that have failed dependencies (note that
347 * a single plugin can appear multiple times in the $failedplugins).
349 * @param int $moodleversion the version from version.php.
350 * @param array $failedplugins to return the list of plugins with non-satisfied dependencies
351 * @return bool true if all the dependencies are satisfied for all plugins.
353 public function all_plugins_ok($moodleversion, &$failedplugins = array()) {
356 foreach ($this->get_plugins() as $type => $plugins) {
357 foreach ($plugins as $plugin) {
359 if (!$plugin->is_core_dependency_satisfied($moodleversion)) {
361 $failedplugins[] = $plugin->component;
364 if (!$this->are_dependencies_satisfied($plugin->get_other_required_plugins())) {
366 $failedplugins[] = $plugin->component;
375 * Checks if there are some plugins with a known available update
377 * @return bool true if there is at least one available update
379 public function some_plugins_updatable() {
380 foreach ($this->get_plugins() as $type => $plugins) {
381 foreach ($plugins as $plugin) {
382 if ($plugin->available_updates()) {
392 * Defines a list of all plugins that were originally shipped in the standard Moodle distribution,
393 * but are not anymore and are deleted during upgrades.
395 * The main purpose of this list is to hide missing plugins during upgrade.
397 * @param string $type plugin type
398 * @param string $name plugin name
401 public static function is_deleted_standard_plugin($type, $name) {
403 // Example of the array structure:
405 // 'block' => array('admin', 'admin_tree'),
406 // 'mod' => array('assignment'),
408 // Do not include plugins that were removed during upgrades to versions that are
409 // not supported as source versions for upgrade any more. For example, at MOODLE_23_STABLE
410 // branch, listed should be no plugins that were removed at 1.9.x - 2.1.x versions as
411 // Moodle 2.3 supports upgrades from 2.2.x only.
413 'qformat' => array('blackboard'),
416 if (!isset($plugins[$type])) {
419 return in_array($name, $plugins[$type]);
423 * Defines a white list of all plugins shipped in the standard Moodle distribution
425 * @param string $type
426 * @return false|array array of standard plugins or false if the type is unknown
428 public static function standard_plugins_list($type) {
429 $standard_plugins = array(
431 'assignment' => array(
432 'offline', 'online', 'upload', 'uploadsingle'
435 'assignsubmission' => array(
436 'comments', 'file', 'onlinetext'
439 'assignfeedback' => array(
440 'comments', 'file', 'offline'
444 'cas', 'db', 'email', 'fc', 'imap', 'ldap', 'manual', 'mnet',
445 'nntp', 'nologin', 'none', 'pam', 'pop3', 'radius',
446 'shibboleth', 'webservice'
450 'activity_modules', 'admin_bookmarks', 'blog_menu',
451 'blog_recent', 'blog_tags', 'calendar_month',
452 'calendar_upcoming', 'comments', 'community',
453 'completionstatus', 'course_list', 'course_overview',
454 'course_summary', 'feedback', 'glossary_random', 'html',
455 'login', 'mentees', 'messages', 'mnet_hosts', 'myprofile',
456 'navigation', 'news_items', 'online_users', 'participants',
457 'private_files', 'quiz_results', 'recent_activity',
458 'rss_client', 'search_forums', 'section_links',
459 'selfcompletion', 'settings', 'site_main_menu',
460 'social_activities', 'tag_flickr', 'tag_youtube', 'tags'
464 'exportimscp', 'importhtml', 'print'
467 'cachelock' => array(
471 'cachestore' => array(
472 'file', 'memcache', 'memcached', 'mongodb', 'session', 'static'
475 'coursereport' => array(
479 'datafield' => array(
480 'checkbox', 'date', 'file', 'latlong', 'menu', 'multimenu',
481 'number', 'picture', 'radiobutton', 'text', 'textarea', 'url'
484 'datapreset' => array(
489 'textarea', 'tinymce'
493 'authorize', 'category', 'cohort', 'database', 'flatfile',
494 'guest', 'imsenterprise', 'ldap', 'manual', 'meta', 'mnet',
499 'activitynames', 'algebra', 'censor', 'emailprotect',
500 'emoticon', 'mediaplugin', 'multilang', 'tex', 'tidy',
501 'urltolink', 'data', 'glossary'
505 'scorm', 'social', 'topics', 'weeks'
508 'gradeexport' => array(
509 'ods', 'txt', 'xls', 'xml'
512 'gradeimport' => array(
516 'gradereport' => array(
517 'grader', 'outcomes', 'overview', 'user'
520 'gradingform' => array(
528 'email', 'jabber', 'popup'
531 'mnetservice' => array(
536 'assign', 'assignment', 'book', 'chat', 'choice', 'data', 'feedback', 'folder',
537 'forum', 'glossary', 'imscp', 'label', 'lesson', 'lti', 'page',
538 'quiz', 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop'
541 'plagiarism' => array(
544 'portfolio' => array(
545 'boxnet', 'download', 'flickr', 'googledocs', 'mahara', 'picasa'
548 'profilefield' => array(
549 'checkbox', 'datetime', 'menu', 'text', 'textarea'
552 'qbehaviour' => array(
553 'adaptive', 'adaptivenopenalty', 'deferredcbm',
554 'deferredfeedback', 'immediatecbm', 'immediatefeedback',
555 'informationitem', 'interactive', 'interactivecountback',
556 'manualgraded', 'missing'
560 'aiken', 'blackboard_six', 'examview', 'gift',
561 'learnwise', 'missingword', 'multianswer', 'webct',
566 'calculated', 'calculatedmulti', 'calculatedsimple',
567 'description', 'essay', 'match', 'missingtype', 'multianswer',
568 'multichoice', 'numerical', 'random', 'randomsamatch',
569 'shortanswer', 'truefalse'
573 'grading', 'overview', 'responses', 'statistics'
576 'quizaccess' => array(
577 'delaybetweenattempts', 'ipaddress', 'numattempts', 'openclosedate',
578 'password', 'safebrowser', 'securewindow', 'timelimit'
582 'backups', 'completion', 'configlog', 'courseoverview',
583 'log', 'loglive', 'outline', 'participation', 'progress', 'questioninstances', 'security', 'stats', 'performance'
586 'repository' => array(
587 'alfresco', 'boxnet', 'coursefiles', 'dropbox', 'equella', 'filesystem',
588 'flickr', 'flickr_public', 'googledocs', 'local', 'merlot',
589 'picasa', 'recent', 's3', 'upload', 'url', 'user', 'webdav',
590 'wikimedia', 'youtube'
593 'scormreport' => array(
600 'dragmath', 'moodleemoticon', 'moodleimage', 'moodlemedia', 'moodlenolink', 'spellchecker',
604 'afterburner', 'anomaly', 'arialist', 'base', 'binarius',
605 'boxxie', 'brick', 'canvas', 'formal_white', 'formfactor',
606 'fusion', 'leatherbound', 'magazine', 'mymobile', 'nimble',
607 'nonzero', 'overlay', 'serenity', 'sky_high', 'splash',
608 'standard', 'standardold'
612 'assignmentupgrade', 'behat', 'capability', 'customlang',
613 'dbtransfer', 'generator', 'health', 'innodb', 'installaddon',
614 'langimport', 'multilangupgrade', 'phpunit', 'profiling',
615 'qeupgradehelper', 'replace', 'spamcleaner', 'timezoneimport',
616 'unittest', 'uploaduser', 'unsuproles', 'xmldb'
619 'webservice' => array(
620 'amf', 'rest', 'soap', 'xmlrpc'
623 'workshopallocation' => array(
624 'manual', 'random', 'scheduled'
627 'workshopeval' => array(
631 'workshopform' => array(
632 'accumulative', 'comments', 'numerrors', 'rubric'
636 if (isset($standard_plugins[$type])) {
637 return $standard_plugins[$type];
644 * Reorders plugin types into a sequence to be displayed
646 * For technical reasons, plugin types returned by {@link get_plugin_types()} are
647 * in a certain order that does not need to fit the expected order for the display.
648 * Particularly, activity modules should be displayed first as they represent the
649 * real heart of Moodle. They should be followed by other plugin types that are
650 * used to build the courses (as that is what one expects from LMS). After that,
651 * other supportive plugin types follow.
653 * @param array $types associative array
654 * @return array same array with altered order of items
656 protected function reorder_plugin_types(array $types) {
658 'mod' => $types['mod'],
659 'block' => $types['block'],
660 'qtype' => $types['qtype'],
661 'qbehaviour' => $types['qbehaviour'],
662 'qformat' => $types['qformat'],
663 'filter' => $types['filter'],
664 'enrol' => $types['enrol'],
666 foreach ($types as $type => $path) {
667 if (!isset($fix[$type])) {
677 * General exception thrown by the {@link available_update_checker} class
679 class available_update_checker_exception extends moodle_exception {
682 * @param string $errorcode exception description identifier
683 * @param mixed $debuginfo debugging data to display
685 public function __construct($errorcode, $debuginfo=null) {
686 parent::__construct($errorcode, 'core_plugin', '', null, print_r($debuginfo, true));
692 * Singleton class that handles checking for available updates
694 class available_update_checker {
696 /** @var available_update_checker holds the singleton instance */
697 protected static $singletoninstance;
698 /** @var null|int the timestamp of when the most recent response was fetched */
699 protected $recentfetch = null;
700 /** @var null|array the recent response from the update notification provider */
701 protected $recentresponse = null;
702 /** @var null|string the numerical version of the local Moodle code */
703 protected $currentversion = null;
704 /** @var null|string the release info of the local Moodle code */
705 protected $currentrelease = null;
706 /** @var null|string branch of the local Moodle code */
707 protected $currentbranch = null;
708 /** @var array of (string)frankestyle => (string)version list of additional plugins deployed at this site */
709 protected $currentplugins = array();
712 * Direct initiation not allowed, use the factory method {@link self::instance()}
714 protected function __construct() {
718 * Sorry, this is singleton
720 protected function __clone() {
724 * Factory method for this class
726 * @return available_update_checker the singleton instance
728 public static function instance() {
729 if (is_null(self::$singletoninstance)) {
730 self::$singletoninstance = new self();
732 return self::$singletoninstance;
737 * @param bool $phpunitreset
739 public static function reset_caches($phpunitreset = false) {
741 self::$singletoninstance = null;
746 * Returns the timestamp of the last execution of {@link fetch()}
748 * @return int|null null if it has never been executed or we don't known
750 public function get_last_timefetched() {
752 $this->restore_response();
754 if (!empty($this->recentfetch)) {
755 return $this->recentfetch;
763 * Fetches the available update status from the remote site
765 * @throws available_update_checker_exception
767 public function fetch() {
768 $response = $this->get_response();
769 $this->validate_response($response);
770 $this->store_response($response);
774 * Returns the available update information for the given component
776 * This method returns null if the most recent response does not contain any information
777 * about it. The returned structure is an array of available updates for the given
778 * component. Each update info is an object with at least one property called
779 * 'version'. Other possible properties are 'release', 'maturity', 'url' and 'downloadurl'.
781 * For the 'core' component, the method returns real updates only (those with higher version).
782 * For all other components, the list of all known remote updates is returned and the caller
783 * (usually the {@link plugin_manager}) is supposed to make the actual comparison of versions.
785 * @param string $component frankenstyle
786 * @param array $options with supported keys 'minmaturity' and/or 'notifybuilds'
787 * @return null|array null or array of available_update_info objects
789 public function get_update_info($component, array $options = array()) {
791 if (!isset($options['minmaturity'])) {
792 $options['minmaturity'] = 0;
795 if (!isset($options['notifybuilds'])) {
796 $options['notifybuilds'] = false;
799 if ($component == 'core') {
800 $this->load_current_environment();
803 $this->restore_response();
805 if (empty($this->recentresponse['updates'][$component])) {
810 foreach ($this->recentresponse['updates'][$component] as $info) {
811 $update = new available_update_info($component, $info);
812 if (isset($update->maturity) and ($update->maturity < $options['minmaturity'])) {
815 if ($component == 'core') {
816 if ($update->version <= $this->currentversion) {
819 if (empty($options['notifybuilds']) and $this->is_same_release($update->release)) {
823 $updates[] = $update;
826 if (empty($updates)) {
834 * The method being run via cron.php
836 public function cron() {
839 if (!$this->cron_autocheck_enabled()) {
840 $this->cron_mtrace('Automatic check for available updates not enabled, skipping.');
844 $now = $this->cron_current_timestamp();
846 if ($this->cron_has_fresh_fetch($now)) {
847 $this->cron_mtrace('Recently fetched info about available updates is still fresh enough, skipping.');
851 if ($this->cron_has_outdated_fetch($now)) {
852 $this->cron_mtrace('Outdated or missing info about available updates, forced fetching ... ', '');
853 $this->cron_execute();
857 $offset = $this->cron_execution_offset();
858 $start = mktime(1, 0, 0, date('n', $now), date('j', $now), date('Y', $now)); // 01:00 AM today local time
859 if ($now > $start + $offset) {
860 $this->cron_mtrace('Regular daily check for available updates ... ', '');
861 $this->cron_execute();
866 /// end of public API //////////////////////////////////////////////////////
869 * Makes cURL request to get data from the remote site
871 * @return string raw request result
872 * @throws available_update_checker_exception
874 protected function get_response() {
876 require_once($CFG->libdir.'/filelib.php');
878 $curl = new curl(array('proxy' => true));
879 $response = $curl->post($this->prepare_request_url(), $this->prepare_request_params(), $this->prepare_request_options());
880 $curlerrno = $curl->get_errno();
881 if (!empty($curlerrno)) {
882 throw new available_update_checker_exception('err_response_curl', 'cURL error '.$curlerrno.': '.$curl->error);
884 $curlinfo = $curl->get_info();
885 if ($curlinfo['http_code'] != 200) {
886 throw new available_update_checker_exception('err_response_http_code', $curlinfo['http_code']);
892 * Makes sure the response is valid, has correct API format etc.
894 * @param string $response raw response as returned by the {@link self::get_response()}
895 * @throws available_update_checker_exception
897 protected function validate_response($response) {
899 $response = $this->decode_response($response);
901 if (empty($response)) {
902 throw new available_update_checker_exception('err_response_empty');
905 if (empty($response['status']) or $response['status'] !== 'OK') {
906 throw new available_update_checker_exception('err_response_status', $response['status']);
909 if (empty($response['apiver']) or $response['apiver'] !== '1.1') {
910 throw new available_update_checker_exception('err_response_format_version', $response['apiver']);
913 if (empty($response['forbranch']) or $response['forbranch'] !== moodle_major_version(true)) {
914 throw new available_update_checker_exception('err_response_target_version', $response['forbranch']);
919 * Decodes the raw string response from the update notifications provider
921 * @param string $response as returned by {@link self::get_response()}
922 * @return array decoded response structure
924 protected function decode_response($response) {
925 return json_decode($response, true);
929 * Stores the valid fetched response for later usage
931 * This implementation uses the config_plugins table as the permanent storage.
933 * @param string $response raw valid data returned by {@link self::get_response()}
935 protected function store_response($response) {
937 set_config('recentfetch', time(), 'core_plugin');
938 set_config('recentresponse', $response, 'core_plugin');
940 $this->restore_response(true);
944 * Loads the most recent raw response record we have fetched
946 * After this method is called, $this->recentresponse is set to an array. If the
947 * array is empty, then either no data have been fetched yet or the fetched data
948 * do not have expected format (and thence they are ignored and a debugging
949 * message is displayed).
951 * This implementation uses the config_plugins table as the permanent storage.
953 * @param bool $forcereload reload even if it was already loaded
955 protected function restore_response($forcereload = false) {
957 if (!$forcereload and !is_null($this->recentresponse)) {
958 // we already have it, nothing to do
962 $config = get_config('core_plugin');
964 if (!empty($config->recentresponse) and !empty($config->recentfetch)) {
966 $this->validate_response($config->recentresponse);
967 $this->recentfetch = $config->recentfetch;
968 $this->recentresponse = $this->decode_response($config->recentresponse);
969 } catch (available_update_checker_exception $e) {
970 // The server response is not valid. Behave as if no data were fetched yet.
971 // This may happen when the most recent update info (cached locally) has been
972 // fetched with the previous branch of Moodle (like during an upgrade from 2.x
973 // to 2.y) or when the API of the response has changed.
974 $this->recentresponse = array();
978 $this->recentresponse = array();
983 * Compares two raw {@link $recentresponse} records and returns the list of changed updates
985 * This method is used to populate potential update info to be sent to site admins.
989 * @throws available_update_checker_exception
990 * @return array parts of $new['updates'] that have changed
992 protected function compare_responses(array $old, array $new) {
998 if (!array_key_exists('updates', $new)) {
999 throw new available_update_checker_exception('err_response_format');
1003 return $new['updates'];
1006 if (!array_key_exists('updates', $old)) {
1007 throw new available_update_checker_exception('err_response_format');
1012 foreach ($new['updates'] as $newcomponent => $newcomponentupdates) {
1013 if (empty($old['updates'][$newcomponent])) {
1014 $changes[$newcomponent] = $newcomponentupdates;
1017 foreach ($newcomponentupdates as $newcomponentupdate) {
1019 foreach ($old['updates'][$newcomponent] as $oldcomponentupdate) {
1020 if ($newcomponentupdate['version'] == $oldcomponentupdate['version']) {
1025 if (!isset($changes[$newcomponent])) {
1026 $changes[$newcomponent] = array();
1028 $changes[$newcomponent][] = $newcomponentupdate;
1037 * Returns the URL to send update requests to
1039 * During the development or testing, you can set $CFG->alternativeupdateproviderurl
1040 * to a custom URL that will be used. Otherwise the standard URL will be returned.
1042 * @return string URL
1044 protected function prepare_request_url() {
1047 if (!empty($CFG->config_php_settings['alternativeupdateproviderurl'])) {
1048 return $CFG->config_php_settings['alternativeupdateproviderurl'];
1050 return 'https://download.moodle.org/api/1.1/updates.php';
1055 * Sets the properties currentversion, currentrelease, currentbranch and currentplugins
1057 * @param bool $forcereload
1059 protected function load_current_environment($forcereload=false) {
1062 if (!is_null($this->currentversion) and !$forcereload) {
1070 require($CFG->dirroot.'/version.php');
1071 $this->currentversion = $version;
1072 $this->currentrelease = $release;
1073 $this->currentbranch = moodle_major_version(true);
1075 $pluginman = plugin_manager::instance();
1076 foreach ($pluginman->get_plugins() as $type => $plugins) {
1077 foreach ($plugins as $plugin) {
1078 if (!$plugin->is_standard()) {
1079 $this->currentplugins[$plugin->component] = $plugin->versiondisk;
1086 * Returns the list of HTTP params to be sent to the updates provider URL
1088 * @return array of (string)param => (string)value
1090 protected function prepare_request_params() {
1093 $this->load_current_environment();
1094 $this->restore_response();
1097 $params['format'] = 'json';
1099 if (isset($this->recentresponse['ticket'])) {
1100 $params['ticket'] = $this->recentresponse['ticket'];
1103 if (isset($this->currentversion)) {
1104 $params['version'] = $this->currentversion;
1106 throw new coding_exception('Main Moodle version must be already known here');
1109 if (isset($this->currentbranch)) {
1110 $params['branch'] = $this->currentbranch;
1112 throw new coding_exception('Moodle release must be already known here');
1116 foreach ($this->currentplugins as $plugin => $version) {
1117 $plugins[] = $plugin.'@'.$version;
1119 if (!empty($plugins)) {
1120 $params['plugins'] = implode(',', $plugins);
1127 * Returns the list of cURL options to use when fetching available updates data
1129 * @return array of (string)param => (string)value
1131 protected function prepare_request_options() {
1135 'CURLOPT_SSL_VERIFYHOST' => 2, // this is the default in {@link curl} class but just in case
1136 'CURLOPT_SSL_VERIFYPEER' => true,
1139 $cacertfile = $CFG->dataroot.'/moodleorgca.crt';
1140 if (is_readable($cacertfile)) {
1141 // Do not use CA certs provided by the operating system. Instead,
1142 // use this CA cert to verify the updates provider.
1143 $options['CURLOPT_CAINFO'] = $cacertfile;
1150 * Returns the current timestamp
1152 * @return int the timestamp
1154 protected function cron_current_timestamp() {
1159 * Output cron debugging info
1162 * @param string $msg output message
1163 * @param string $eol end of line
1165 protected function cron_mtrace($msg, $eol = PHP_EOL) {
1170 * Decide if the autocheck feature is disabled in the server setting
1172 * @return bool true if autocheck enabled, false if disabled
1174 protected function cron_autocheck_enabled() {
1177 if (empty($CFG->updateautocheck)) {
1185 * Decide if the recently fetched data are still fresh enough
1187 * @param int $now current timestamp
1188 * @return bool true if no need to re-fetch, false otherwise
1190 protected function cron_has_fresh_fetch($now) {
1191 $recent = $this->get_last_timefetched();
1193 if (empty($recent)) {
1197 if ($now < $recent) {
1198 $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1202 if ($now - $recent > 24 * HOURSECS) {
1210 * Decide if the fetch is outadated or even missing
1212 * @param int $now current timestamp
1213 * @return bool false if no need to re-fetch, true otherwise
1215 protected function cron_has_outdated_fetch($now) {
1216 $recent = $this->get_last_timefetched();
1218 if (empty($recent)) {
1222 if ($now < $recent) {
1223 $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1227 if ($now - $recent > 48 * HOURSECS) {
1235 * Returns the cron execution offset for this site
1237 * The main {@link self::cron()} is supposed to run every night in some random time
1238 * between 01:00 and 06:00 AM (local time). The exact moment is defined by so called
1239 * execution offset, that is the amount of time after 01:00 AM. The offset value is
1240 * initially generated randomly and then used consistently at the site. This way, the
1241 * regular checks against the download.moodle.org server are spread in time.
1243 * @return int the offset number of seconds from range 1 sec to 5 hours
1245 protected function cron_execution_offset() {
1248 if (empty($CFG->updatecronoffset)) {
1249 set_config('updatecronoffset', rand(1, 5 * HOURSECS));
1252 return $CFG->updatecronoffset;
1256 * Fetch available updates info and eventually send notification to site admins
1258 protected function cron_execute() {
1261 $this->restore_response();
1262 $previous = $this->recentresponse;
1264 $this->restore_response(true);
1265 $current = $this->recentresponse;
1266 $changes = $this->compare_responses($previous, $current);
1267 $notifications = $this->cron_notifications($changes);
1268 $this->cron_notify($notifications);
1269 $this->cron_mtrace('done');
1270 } catch (available_update_checker_exception $e) {
1271 $this->cron_mtrace('FAILED!');
1276 * Given the list of changes in available updates, pick those to send to site admins
1278 * @param array $changes as returned by {@link self::compare_responses()}
1279 * @return array of available_update_info objects to send to site admins
1281 protected function cron_notifications(array $changes) {
1284 $notifications = array();
1285 $pluginman = plugin_manager::instance();
1286 $plugins = $pluginman->get_plugins(true);
1288 foreach ($changes as $component => $componentchanges) {
1289 if (empty($componentchanges)) {
1292 $componentupdates = $this->get_update_info($component,
1293 array('minmaturity' => $CFG->updateminmaturity, 'notifybuilds' => $CFG->updatenotifybuilds));
1294 if (empty($componentupdates)) {
1297 // notify only about those $componentchanges that are present in $componentupdates
1298 // to respect the preferences
1299 foreach ($componentchanges as $componentchange) {
1300 foreach ($componentupdates as $componentupdate) {
1301 if ($componentupdate->version == $componentchange['version']) {
1302 if ($component == 'core') {
1303 // In case of 'core', we already know that the $componentupdate
1304 // is a real update with higher version ({@see self::get_update_info()}).
1305 // We just perform additional check for the release property as there
1306 // can be two Moodle releases having the same version (e.g. 2.4.0 and 2.5dev shortly
1307 // after the release). We can do that because we have the release info
1308 // always available for the core.
1309 if ((string)$componentupdate->release === (string)$componentchange['release']) {
1310 $notifications[] = $componentupdate;
1313 // Use the plugin_manager to check if the detected $componentchange
1314 // is a real update with higher version. That is, the $componentchange
1315 // is present in the array of {@link available_update_info} objects
1316 // returned by the plugin's available_updates() method.
1317 list($plugintype, $pluginname) = normalize_component($component);
1318 if (!empty($plugins[$plugintype][$pluginname])) {
1319 $availableupdates = $plugins[$plugintype][$pluginname]->available_updates();
1320 if (!empty($availableupdates)) {
1321 foreach ($availableupdates as $availableupdate) {
1322 if ($availableupdate->version == $componentchange['version']) {
1323 $notifications[] = $componentupdate;
1334 return $notifications;
1338 * Sends the given notifications to site admins via messaging API
1340 * @param array $notifications array of available_update_info objects to send
1342 protected function cron_notify(array $notifications) {
1345 if (empty($notifications)) {
1349 $admins = get_admins();
1351 if (empty($admins)) {
1355 $this->cron_mtrace('sending notifications ... ', '');
1357 $text = get_string('updatenotifications', 'core_admin') . PHP_EOL;
1358 $html = html_writer::tag('h1', get_string('updatenotifications', 'core_admin')) . PHP_EOL;
1360 $coreupdates = array();
1361 $pluginupdates = array();
1363 foreach ($notifications as $notification) {
1364 if ($notification->component == 'core') {
1365 $coreupdates[] = $notification;
1367 $pluginupdates[] = $notification;
1371 if (!empty($coreupdates)) {
1372 $text .= PHP_EOL . get_string('updateavailable', 'core_admin') . PHP_EOL;
1373 $html .= html_writer::tag('h2', get_string('updateavailable', 'core_admin')) . PHP_EOL;
1374 $html .= html_writer::start_tag('ul') . PHP_EOL;
1375 foreach ($coreupdates as $coreupdate) {
1376 $html .= html_writer::start_tag('li');
1377 if (isset($coreupdate->release)) {
1378 $text .= get_string('updateavailable_release', 'core_admin', $coreupdate->release);
1379 $html .= html_writer::tag('strong', get_string('updateavailable_release', 'core_admin', $coreupdate->release));
1381 if (isset($coreupdate->version)) {
1382 $text .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
1383 $html .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
1385 if (isset($coreupdate->maturity)) {
1386 $text .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
1387 $html .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
1390 $html .= html_writer::end_tag('li') . PHP_EOL;
1393 $html .= html_writer::end_tag('ul') . PHP_EOL;
1395 $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/index.php');
1396 $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
1397 $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/index.php', $CFG->wwwroot.'/'.$CFG->admin.'/index.php'));
1398 $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
1401 if (!empty($pluginupdates)) {
1402 $text .= PHP_EOL . get_string('updateavailableforplugin', 'core_admin') . PHP_EOL;
1403 $html .= html_writer::tag('h2', get_string('updateavailableforplugin', 'core_admin')) . PHP_EOL;
1405 $html .= html_writer::start_tag('ul') . PHP_EOL;
1406 foreach ($pluginupdates as $pluginupdate) {
1407 $html .= html_writer::start_tag('li');
1408 $text .= get_string('pluginname', $pluginupdate->component);
1409 $html .= html_writer::tag('strong', get_string('pluginname', $pluginupdate->component));
1411 $text .= ' ('.$pluginupdate->component.')';
1412 $html .= ' ('.$pluginupdate->component.')';
1414 $text .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
1415 $html .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
1418 $html .= html_writer::end_tag('li') . PHP_EOL;
1421 $html .= html_writer::end_tag('ul') . PHP_EOL;
1423 $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php');
1424 $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
1425 $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/plugins.php', $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php'));
1426 $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
1429 $a = array('siteurl' => $CFG->wwwroot);
1430 $text .= get_string('updatenotificationfooter', 'core_admin', $a) . PHP_EOL;
1431 $a = array('siteurl' => html_writer::link($CFG->wwwroot, $CFG->wwwroot));
1432 $html .= html_writer::tag('footer', html_writer::tag('p', get_string('updatenotificationfooter', 'core_admin', $a),
1433 array('style' => 'font-size:smaller; color:#333;')));
1435 foreach ($admins as $admin) {
1436 $message = new stdClass();
1437 $message->component = 'moodle';
1438 $message->name = 'availableupdate';
1439 $message->userfrom = get_admin();
1440 $message->userto = $admin;
1441 $message->subject = get_string('updatenotificationsubject', 'core_admin', array('siteurl' => $CFG->wwwroot));
1442 $message->fullmessage = $text;
1443 $message->fullmessageformat = FORMAT_PLAIN;
1444 $message->fullmessagehtml = $html;
1445 $message->smallmessage = get_string('updatenotifications', 'core_admin');
1446 $message->notification = 1;
1447 message_send($message);
1452 * Compare two release labels and decide if they are the same
1454 * @param string $remote release info of the available update
1455 * @param null|string $local release info of the local code, defaults to $release defined in version.php
1456 * @return boolean true if the releases declare the same minor+major version
1458 protected function is_same_release($remote, $local=null) {
1460 if (is_null($local)) {
1461 $this->load_current_environment();
1462 $local = $this->currentrelease;
1465 $pattern = '/^([0-9\.\+]+)([^(]*)/';
1467 preg_match($pattern, $remote, $remotematches);
1468 preg_match($pattern, $local, $localmatches);
1470 $remotematches[1] = str_replace('+', '', $remotematches[1]);
1471 $localmatches[1] = str_replace('+', '', $localmatches[1]);
1473 if ($remotematches[1] === $localmatches[1] and rtrim($remotematches[2]) === rtrim($localmatches[2])) {
1483 * Defines the structure of objects returned by {@link available_update_checker::get_update_info()}
1485 class available_update_info {
1487 /** @var string frankenstyle component name */
1489 /** @var int the available version of the component */
1491 /** @var string|null optional release name */
1492 public $release = null;
1493 /** @var int|null optional maturity info, eg {@link MATURITY_STABLE} */
1494 public $maturity = null;
1495 /** @var string|null optional URL of a page with more info about the update */
1497 /** @var string|null optional URL of a ZIP package that can be downloaded and installed */
1498 public $download = null;
1499 /** @var string|null of self::download is set, then this must be the MD5 hash of the ZIP */
1500 public $downloadmd5 = null;
1503 * Creates new instance of the class
1505 * The $info array must provide at least the 'version' value and optionally all other
1506 * values to populate the object's properties.
1508 * @param string $name the frankenstyle component name
1509 * @param array $info associative array with other properties
1511 public function __construct($name, array $info) {
1512 $this->component = $name;
1513 foreach ($info as $k => $v) {
1514 if (property_exists('available_update_info', $k) and $k != 'component') {
1523 * Implements a communication bridge to the mdeploy.php utility
1525 class available_update_deployer {
1527 const HTTP_PARAM_PREFIX = 'updteautodpldata_'; // Hey, even Google has not heard of such a prefix! So it MUST be safe :-p
1528 const HTTP_PARAM_CHECKER = 'datapackagesize'; // Name of the parameter that holds the number of items in the received data items
1530 /** @var available_update_deployer holds the singleton instance */
1531 protected static $singletoninstance;
1532 /** @var moodle_url URL of a page that includes the deployer UI */
1533 protected $callerurl;
1534 /** @var moodle_url URL to return after the deployment */
1535 protected $returnurl;
1538 * Direct instantiation not allowed, use the factory method {@link self::instance()}
1540 protected function __construct() {
1544 * Sorry, this is singleton
1546 protected function __clone() {
1550 * Factory method for this class
1552 * @return available_update_deployer the singleton instance
1554 public static function instance() {
1555 if (is_null(self::$singletoninstance)) {
1556 self::$singletoninstance = new self();
1558 return self::$singletoninstance;
1562 * Reset caches used by this script
1564 * @param bool $phpunitreset is this called as a part of PHPUnit reset?
1566 public static function reset_caches($phpunitreset = false) {
1567 if ($phpunitreset) {
1568 self::$singletoninstance = null;
1573 * Is automatic deployment enabled?
1577 public function enabled() {
1580 if (!empty($CFG->disableupdateautodeploy)) {
1581 // The feature is prohibited via config.php
1585 return get_config('updateautodeploy');
1589 * Sets some base properties of the class to make it usable.
1591 * @param moodle_url $callerurl the base URL of a script that will handle the class'es form data
1592 * @param moodle_url $returnurl the final URL to return to when the deployment is finished
1594 public function initialize(moodle_url $callerurl, moodle_url $returnurl) {
1596 if (!$this->enabled()) {
1597 throw new coding_exception('Unable to initialize the deployer, the feature is not enabled.');
1600 $this->callerurl = $callerurl;
1601 $this->returnurl = $returnurl;
1605 * Has the deployer been initialized?
1607 * Initialized deployer means that the following properties were set:
1608 * callerurl, returnurl
1612 public function initialized() {
1614 if (!$this->enabled()) {
1618 if (empty($this->callerurl)) {
1622 if (empty($this->returnurl)) {
1630 * Returns a list of reasons why the deployment can not happen
1632 * If the returned array is empty, the deployment seems to be possible. The returned
1633 * structure is an associative array with keys representing individual impediments.
1634 * Possible keys are: missingdownloadurl, missingdownloadmd5, notwritable.
1636 * @param available_update_info $info
1639 public function deployment_impediments(available_update_info $info) {
1641 $impediments = array();
1643 if (empty($info->download)) {
1644 $impediments['missingdownloadurl'] = true;
1647 if (empty($info->downloadmd5)) {
1648 $impediments['missingdownloadmd5'] = true;
1651 if (!empty($info->download) and !$this->update_downloadable($info->download)) {
1652 $impediments['notdownloadable'] = true;
1655 if (!$this->component_writable($info->component)) {
1656 $impediments['notwritable'] = true;
1659 return $impediments;
1663 * Check to see if the current version of the plugin seems to be a checkout of an external repository.
1665 * @param available_update_info $info
1666 * @return false|string
1668 public function plugin_external_source(available_update_info $info) {
1670 $paths = get_plugin_types(true);
1671 list($plugintype, $pluginname) = normalize_component($info->component);
1672 $pluginroot = $paths[$plugintype].'/'.$pluginname;
1674 if (is_dir($pluginroot.'/.git')) {
1678 if (is_dir($pluginroot.'/CVS')) {
1682 if (is_dir($pluginroot.'/.svn')) {
1690 * Prepares a renderable widget to confirm installation of an available update.
1692 * @param available_update_info $info component version to deploy
1693 * @return renderable
1695 public function make_confirm_widget(available_update_info $info) {
1697 if (!$this->initialized()) {
1698 throw new coding_exception('Illegal method call - deployer not initialized.');
1701 $params = $this->data_to_params(array(
1702 'updateinfo' => (array)$info, // see http://www.php.net/manual/en/language.types.array.php#language.types.array.casting
1705 $widget = new single_button(
1706 new moodle_url($this->callerurl, $params),
1707 get_string('updateavailableinstall', 'core_admin'),
1715 * Prepares a renderable widget to execute installation of an available update.
1717 * @param available_update_info $info component version to deploy
1718 * @return renderable
1720 public function make_execution_widget(available_update_info $info) {
1723 if (!$this->initialized()) {
1724 throw new coding_exception('Illegal method call - deployer not initialized.');
1727 $pluginrootpaths = get_plugin_types(true);
1729 list($plugintype, $pluginname) = normalize_component($info->component);
1731 if (empty($pluginrootpaths[$plugintype])) {
1732 throw new coding_exception('Unknown plugin type root location', $plugintype);
1735 list($passfile, $password) = $this->prepare_authorization();
1737 $upgradeurl = new moodle_url('/admin');
1741 'type' => $plugintype,
1742 'name' => $pluginname,
1743 'typeroot' => $pluginrootpaths[$plugintype],
1744 'package' => $info->download,
1745 'md5' => $info->downloadmd5,
1746 'dataroot' => $CFG->dataroot,
1747 'dirroot' => $CFG->dirroot,
1748 'passfile' => $passfile,
1749 'password' => $password,
1750 'returnurl' => $upgradeurl->out(true),
1753 if (!empty($CFG->proxyhost)) {
1754 // MDL-36973 - Beware - we should call just !is_proxybypass() here. But currently, our
1755 // cURL wrapper class does not do it. So, to have consistent behaviour, we pass proxy
1756 // setting regardless the $CFG->proxybypass setting. Once the {@link curl} class is
1757 // fixed, the condition should be amended.
1758 if (true or !is_proxybypass($info->download)) {
1759 if (empty($CFG->proxyport)) {
1760 $params['proxy'] = $CFG->proxyhost;
1762 $params['proxy'] = $CFG->proxyhost.':'.$CFG->proxyport;
1765 if (!empty($CFG->proxyuser) and !empty($CFG->proxypassword)) {
1766 $params['proxyuserpwd'] = $CFG->proxyuser.':'.$CFG->proxypassword;
1769 if (!empty($CFG->proxytype)) {
1770 $params['proxytype'] = $CFG->proxytype;
1775 $widget = new single_button(
1776 new moodle_url('/mdeploy.php', $params),
1777 get_string('updateavailableinstall', 'core_admin'),
1785 * Returns array of data objects passed to this tool.
1789 public function submitted_data() {
1791 $data = $this->params_to_data($_POST);
1793 if (empty($data) or empty($data[self::HTTP_PARAM_CHECKER])) {
1797 if (!empty($data['updateinfo']) and is_object($data['updateinfo'])) {
1798 $updateinfo = $data['updateinfo'];
1799 if (!empty($updateinfo->component) and !empty($updateinfo->version)) {
1800 $data['updateinfo'] = new available_update_info($updateinfo->component, (array)$updateinfo);
1804 if (!empty($data['callerurl'])) {
1805 $data['callerurl'] = new moodle_url($data['callerurl']);
1808 if (!empty($data['returnurl'])) {
1809 $data['returnurl'] = new moodle_url($data['returnurl']);
1816 * Handles magic getters and setters for protected properties.
1818 * @param string $name method name, e.g. set_returnurl()
1819 * @param array $arguments arguments to be passed to the array
1821 public function __call($name, array $arguments = array()) {
1823 if (substr($name, 0, 4) === 'set_') {
1824 $property = substr($name, 4);
1825 if (empty($property)) {
1826 throw new coding_exception('Invalid property name (empty)');
1828 if (empty($arguments)) {
1829 $arguments = array(true); // Default value for flag-like properties.
1831 // Make sure it is a protected property.
1832 $isprotected = false;
1833 $reflection = new ReflectionObject($this);
1834 foreach ($reflection->getProperties(ReflectionProperty::IS_PROTECTED) as $reflectionproperty) {
1835 if ($reflectionproperty->getName() === $property) {
1836 $isprotected = true;
1840 if (!$isprotected) {
1841 throw new coding_exception('Unable to set property - it does not exist or it is not protected');
1843 $value = reset($arguments);
1844 $this->$property = $value;
1848 if (substr($name, 0, 4) === 'get_') {
1849 $property = substr($name, 4);
1850 if (empty($property)) {
1851 throw new coding_exception('Invalid property name (empty)');
1853 if (!empty($arguments)) {
1854 throw new coding_exception('No parameter expected');
1856 // Make sure it is a protected property.
1857 $isprotected = false;
1858 $reflection = new ReflectionObject($this);
1859 foreach ($reflection->getProperties(ReflectionProperty::IS_PROTECTED) as $reflectionproperty) {
1860 if ($reflectionproperty->getName() === $property) {
1861 $isprotected = true;
1865 if (!$isprotected) {
1866 throw new coding_exception('Unable to get property - it does not exist or it is not protected');
1868 return $this->$property;
1873 * Generates a random token and stores it in a file in moodledata directory.
1875 * @return array of the (string)filename and (string)password in this order
1877 public function prepare_authorization() {
1880 make_upload_directory('mdeploy/auth/');
1885 while (!$success and $attempts < 5) {
1888 $passfile = $this->generate_passfile();
1889 $password = $this->generate_password();
1892 $filepath = $CFG->dataroot.'/mdeploy/auth/'.$passfile;
1894 if (!file_exists($filepath)) {
1895 $success = file_put_contents($filepath, $password . PHP_EOL . $now . PHP_EOL, LOCK_EX);
1900 return array($passfile, $password);
1903 throw new moodle_exception('unable_prepare_authorization', 'core_plugin');
1907 // End of external API
1910 * Prepares an array of HTTP parameters that can be passed to another page.
1912 * @param array|object $data associative array or an object holding the data, data JSON-able
1913 * @return array suitable as a param for moodle_url
1915 protected function data_to_params($data) {
1917 // Append some our own data
1918 if (!empty($this->callerurl)) {
1919 $data['callerurl'] = $this->callerurl->out(false);
1921 if (!empty($this->callerurl)) {
1922 $data['returnurl'] = $this->returnurl->out(false);
1925 // Finally append the count of items in the package.
1926 $data[self::HTTP_PARAM_CHECKER] = count($data);
1930 foreach ($data as $name => $value) {
1931 $transname = self::HTTP_PARAM_PREFIX.$name;
1932 $transvalue = json_encode($value);
1933 $params[$transname] = $transvalue;
1940 * Converts HTTP parameters passed to the script into native PHP data
1942 * @param array $params such as $_REQUEST or $_POST
1943 * @return array data passed for this class
1945 protected function params_to_data(array $params) {
1947 if (empty($params)) {
1952 foreach ($params as $name => $value) {
1953 if (strpos($name, self::HTTP_PARAM_PREFIX) === 0) {
1954 $realname = substr($name, strlen(self::HTTP_PARAM_PREFIX));
1955 $realvalue = json_decode($value);
1956 $data[$realname] = $realvalue;
1964 * Returns a random string to be used as a filename of the password storage.
1968 protected function generate_passfile() {
1969 return clean_param(uniqid('mdeploy_', true), PARAM_FILE);
1973 * Returns a random string to be used as the authorization token
1977 protected function generate_password() {
1978 return complex_random_string();
1982 * Checks if the given component's directory is writable
1984 * For the purpose of the deployment, the web server process has to have
1985 * write access to all files in the component's directory (recursively) and for the
1988 * @see worker::move_directory_source_precheck()
1989 * @param string $component normalized component name
1992 protected function component_writable($component) {
1994 list($plugintype, $pluginname) = normalize_component($component);
1996 $directory = get_plugin_directory($plugintype, $pluginname);
1998 if (is_null($directory)) {
1999 throw new coding_exception('Unknown component location', $component);
2002 return $this->directory_writable($directory);
2006 * Checks if the mdeploy.php will be able to fetch the ZIP from the given URL
2008 * This is mainly supposed to check if the transmission over HTTPS would
2009 * work. That is, if the CA certificates are present at the server.
2011 * @param string $downloadurl the URL of the ZIP package to download
2014 protected function update_downloadable($downloadurl) {
2017 $curloptions = array(
2018 'CURLOPT_SSL_VERIFYHOST' => 2, // this is the default in {@link curl} class but just in case
2019 'CURLOPT_SSL_VERIFYPEER' => true,
2022 $cacertfile = $CFG->dataroot.'/moodleorgca.crt';
2023 if (is_readable($cacertfile)) {
2024 // Do not use CA certs provided by the operating system. Instead,
2025 // use this CA cert to verify the updates provider.
2026 $curloptions['CURLOPT_CAINFO'] = $cacertfile;
2029 $curl = new curl(array('proxy' => true));
2030 $result = $curl->head($downloadurl, $curloptions);
2031 $errno = $curl->get_errno();
2032 if (empty($errno)) {
2040 * Checks if the directory and all its contents (recursively) is writable
2042 * @param string $path full path to a directory
2045 private function directory_writable($path) {
2047 if (!is_writable($path)) {
2051 if (is_dir($path)) {
2052 $handle = opendir($path);
2059 while ($filename = readdir($handle)) {
2060 $filepath = $path.'/'.$filename;
2062 if ($filename === '.' or $filename === '..') {
2066 if (is_dir($filepath)) {
2067 $result = $result && $this->directory_writable($filepath);
2070 $result = $result && is_writable($filepath);
2082 * Factory class producing required subclasses of {@link plugininfo_base}
2084 class plugininfo_default_factory {
2087 * Makes a new instance of the plugininfo class
2089 * @param string $type the plugin type, eg. 'mod'
2090 * @param string $typerootdir full path to the location of all the plugins of this type
2091 * @param string $name the plugin name, eg. 'workshop'
2092 * @param string $namerootdir full path to the location of the plugin
2093 * @param string $typeclass the name of class that holds the info about the plugin
2094 * @return plugininfo_base the instance of $typeclass
2096 public static function make($type, $typerootdir, $name, $namerootdir, $typeclass) {
2097 $plugin = new $typeclass();
2098 $plugin->type = $type;
2099 $plugin->typerootdir = $typerootdir;
2100 $plugin->name = $name;
2101 $plugin->rootdir = $namerootdir;
2103 $plugin->init_display_name();
2104 $plugin->load_disk_version();
2105 $plugin->load_db_version();
2106 $plugin->load_required_main_version();
2107 $plugin->init_is_standard();
2115 * Base class providing access to the information about a plugin
2117 * @property-read string component the component name, type_name
2119 abstract class plugininfo_base {
2121 /** @var string the plugintype name, eg. mod, auth or workshopform */
2123 /** @var string full path to the location of all the plugins of this type */
2124 public $typerootdir;
2125 /** @var string the plugin name, eg. assignment, ldap */
2127 /** @var string the localized plugin name */
2128 public $displayname;
2129 /** @var string the plugin source, one of plugin_manager::PLUGIN_SOURCE_xxx constants */
2131 /** @var fullpath to the location of this plugin */
2133 /** @var int|string the version of the plugin's source code */
2134 public $versiondisk;
2135 /** @var int|string the version of the installed plugin */
2137 /** @var int|float|string required version of Moodle core */
2138 public $versionrequires;
2139 /** @var array other plugins that this one depends on, lazy-loaded by {@link get_other_required_plugins()} */
2140 public $dependencies;
2141 /** @var int number of instances of the plugin - not supported yet */
2143 /** @var int order of the plugin among other plugins of the same type - not supported yet */
2145 /** @var array|null array of {@link available_update_info} for this plugin */
2146 public $availableupdates;
2149 * Gathers and returns the information about all plugins of the given type
2151 * @param string $type the name of the plugintype, eg. mod, auth or workshopform
2152 * @param string $typerootdir full path to the location of the plugin dir
2153 * @param string $typeclass the name of the actually called class
2154 * @return array of plugintype classes, indexed by the plugin name
2156 public static function get_plugins($type, $typerootdir, $typeclass) {
2158 // get the information about plugins at the disk
2159 $plugins = get_plugin_list($type);
2161 foreach ($plugins as $pluginname => $pluginrootdir) {
2162 $ondisk[$pluginname] = plugininfo_default_factory::make($type, $typerootdir,
2163 $pluginname, $pluginrootdir, $typeclass);
2169 * Sets {@link $displayname} property to a localized name of the plugin
2171 public function init_display_name() {
2172 if (!get_string_manager()->string_exists('pluginname', $this->component)) {
2173 $this->displayname = '[pluginname,' . $this->component . ']';
2175 $this->displayname = get_string('pluginname', $this->component);
2180 * Magic method getter, redirects to read only values.
2182 * @param string $name
2185 public function __get($name) {
2187 case 'component': return $this->type . '_' . $this->name;
2190 debugging('Invalid plugin property accessed! '.$name);
2196 * Return the full path name of a file within the plugin.
2198 * No check is made to see if the file exists.
2200 * @param string $relativepath e.g. 'version.php'.
2201 * @return string e.g. $CFG->dirroot . '/mod/quiz/version.php'.
2203 public function full_path($relativepath) {
2204 if (empty($this->rootdir)) {
2207 return $this->rootdir . '/' . $relativepath;
2211 * Load the data from version.php.
2213 * @param bool $disablecache do not attempt to obtain data from the cache
2214 * @return stdClass the object called $plugin defined in version.php
2216 protected function load_version_php($disablecache=false) {
2218 $cache = cache::make('core', 'plugininfo_base');
2220 $versionsphp = $cache->get('versions_php');
2222 if (!$disablecache and $versionsphp !== false and isset($versionsphp[$this->component])) {
2223 return $versionsphp[$this->component];
2226 $versionfile = $this->full_path('version.php');
2228 $plugin = new stdClass();
2229 if (is_readable($versionfile)) {
2230 include($versionfile);
2232 $versionsphp[$this->component] = $plugin;
2233 $cache->set('versions_php', $versionsphp);
2239 * Sets {@link $versiondisk} property to a numerical value representing the
2240 * version of the plugin's source code.
2242 * If the value is null after calling this method, either the plugin
2243 * does not use versioning (typically does not have any database
2244 * data) or is missing from disk.
2246 public function load_disk_version() {
2247 $plugin = $this->load_version_php();
2248 if (isset($plugin->version)) {
2249 $this->versiondisk = $plugin->version;
2254 * Sets {@link $versionrequires} property to a numerical value representing
2255 * the version of Moodle core that this plugin requires.
2257 public function load_required_main_version() {
2258 $plugin = $this->load_version_php();
2259 if (isset($plugin->requires)) {
2260 $this->versionrequires = $plugin->requires;
2265 * Initialise {@link $dependencies} to the list of other plugins (in any)
2266 * that this one requires to be installed.
2268 protected function load_other_required_plugins() {
2269 $plugin = $this->load_version_php();
2270 if (!empty($plugin->dependencies)) {
2271 $this->dependencies = $plugin->dependencies;
2273 $this->dependencies = array(); // By default, no dependencies.
2278 * Get the list of other plugins that this plugin requires to be installed.
2280 * @return array with keys the frankenstyle plugin name, and values either
2281 * a version string (like '2011101700') or the constant ANY_VERSION.
2283 public function get_other_required_plugins() {
2284 if (is_null($this->dependencies)) {
2285 $this->load_other_required_plugins();
2287 return $this->dependencies;
2291 * Sets {@link $versiondb} property to a numerical value representing the
2292 * currently installed version of the plugin.
2294 * If the value is null after calling this method, either the plugin
2295 * does not use versioning (typically does not have any database
2296 * data) or has not been installed yet.
2298 public function load_db_version() {
2299 if ($ver = self::get_version_from_config_plugins($this->component)) {
2300 $this->versiondb = $ver;
2305 * Sets {@link $source} property to one of plugin_manager::PLUGIN_SOURCE_xxx
2308 * If the property's value is null after calling this method, then
2309 * the type of the plugin has not been recognized and you should throw
2312 public function init_is_standard() {
2314 $standard = plugin_manager::standard_plugins_list($this->type);
2316 if ($standard !== false) {
2317 $standard = array_flip($standard);
2318 if (isset($standard[$this->name])) {
2319 $this->source = plugin_manager::PLUGIN_SOURCE_STANDARD;
2320 } else if (!is_null($this->versiondb) and is_null($this->versiondisk)
2321 and plugin_manager::is_deleted_standard_plugin($this->type, $this->name)) {
2322 $this->source = plugin_manager::PLUGIN_SOURCE_STANDARD; // to be deleted
2324 $this->source = plugin_manager::PLUGIN_SOURCE_EXTENSION;
2330 * Returns true if the plugin is shipped with the official distribution
2331 * of the current Moodle version, false otherwise.
2335 public function is_standard() {
2336 return $this->source === plugin_manager::PLUGIN_SOURCE_STANDARD;
2340 * Returns true if the the given Moodle version is enough to run this plugin
2342 * @param string|int|double $moodleversion
2345 public function is_core_dependency_satisfied($moodleversion) {
2347 if (empty($this->versionrequires)) {
2351 return (double)$this->versionrequires <= (double)$moodleversion;
2356 * Returns the status of the plugin
2358 * @return string one of plugin_manager::PLUGIN_STATUS_xxx constants
2360 public function get_status() {
2362 if (is_null($this->versiondb) and is_null($this->versiondisk)) {
2363 return plugin_manager::PLUGIN_STATUS_NODB;
2365 } else if (is_null($this->versiondb) and !is_null($this->versiondisk)) {
2366 return plugin_manager::PLUGIN_STATUS_NEW;
2368 } else if (!is_null($this->versiondb) and is_null($this->versiondisk)) {
2369 if (plugin_manager::is_deleted_standard_plugin($this->type, $this->name)) {
2370 return plugin_manager::PLUGIN_STATUS_DELETE;
2372 return plugin_manager::PLUGIN_STATUS_MISSING;
2375 } else if ((string)$this->versiondb === (string)$this->versiondisk) {
2376 return plugin_manager::PLUGIN_STATUS_UPTODATE;
2378 } else if ($this->versiondb < $this->versiondisk) {
2379 return plugin_manager::PLUGIN_STATUS_UPGRADE;
2381 } else if ($this->versiondb > $this->versiondisk) {
2382 return plugin_manager::PLUGIN_STATUS_DOWNGRADE;
2385 // $version = pi(); and similar funny jokes - hopefully Donald E. Knuth will never contribute to Moodle ;-)
2386 throw new coding_exception('Unable to determine plugin state, check the plugin versions');
2391 * Returns the information about plugin availability
2393 * True means that the plugin is enabled. False means that the plugin is
2394 * disabled. Null means that the information is not available, or the
2395 * plugin does not support configurable availability or the availability
2396 * can not be changed.
2400 public function is_enabled() {
2405 * Populates the property {@link $availableupdates} with the information provided by
2406 * available update checker
2408 * @param available_update_checker $provider the class providing the available update info
2410 public function check_available_updates(available_update_checker $provider) {
2413 if (isset($CFG->updateminmaturity)) {
2414 $minmaturity = $CFG->updateminmaturity;
2416 // this can happen during the very first upgrade to 2.3
2417 $minmaturity = MATURITY_STABLE;
2420 $this->availableupdates = $provider->get_update_info($this->component,
2421 array('minmaturity' => $minmaturity));
2425 * If there are updates for this plugin available, returns them.
2427 * Returns array of {@link available_update_info} objects, if some update
2428 * is available. Returns null if there is no update available or if the update
2429 * availability is unknown.
2431 * @return array|null
2433 public function available_updates() {
2435 if (empty($this->availableupdates) or !is_array($this->availableupdates)) {
2441 foreach ($this->availableupdates as $availableupdate) {
2442 if ($availableupdate->version > $this->versiondisk) {
2443 $updates[] = $availableupdate;
2447 if (empty($updates)) {
2455 * Returns the node name used in admin settings menu for this plugin settings (if applicable)
2457 * @return null|string node name or null if plugin does not create settings node (default)
2459 public function get_settings_section_name() {
2464 * Returns the URL of the plugin settings screen
2466 * Null value means that the plugin either does not have the settings screen
2467 * or its location is not available via this library.
2469 * @return null|moodle_url
2471 public function get_settings_url() {
2472 $section = $this->get_settings_section_name();
2473 if ($section === null) {
2476 $settings = admin_get_root()->locate($section);
2477 if ($settings && $settings instanceof admin_settingpage) {
2478 return new moodle_url('/admin/settings.php', array('section' => $section));
2479 } else if ($settings && $settings instanceof admin_externalpage) {
2480 return new moodle_url($settings->url);
2487 * Loads plugin settings to the settings tree
2489 * This function usually includes settings.php file in plugins folder.
2490 * Alternatively it can create a link to some settings page (instance of admin_externalpage)
2492 * @param part_of_admin_tree $adminroot
2493 * @param string $parentnodename
2494 * @param bool $hassiteconfig whether the current user has moodle/site:config capability
2496 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
2500 * Returns the URL of the screen where this plugin can be uninstalled
2502 * Visiting that URL must be safe, that is a manual confirmation is needed
2503 * for actual uninstallation of the plugin. Null value means that the
2504 * plugin either does not support uninstallation, or does not require any
2505 * database cleanup or the location of the screen is not available via this
2508 * @return null|moodle_url
2510 public function get_uninstall_url() {
2515 * Returns relative directory of the plugin with heading '/'
2519 public function get_dir() {
2522 return substr($this->rootdir, strlen($CFG->dirroot));
2526 * Provides access to plugin versions from the {config_plugins} table
2528 * @param string $plugin plugin name
2529 * @param bool $disablecache do not attempt to obtain data from the cache
2530 * @return int|bool the stored value or false if not found
2532 protected function get_version_from_config_plugins($plugin, $disablecache=false) {
2535 $cache = cache::make('core', 'plugininfo_base');
2537 $pluginversions = $cache->get('versions_db');
2539 if ($pluginversions === false or $disablecache) {
2541 $pluginversions = $DB->get_records_menu('config_plugins', array('name' => 'version'), 'plugin', 'plugin,value');
2542 } catch (dml_exception $e) {
2544 $pluginversions = array();
2546 $cache->set('versions_db', $pluginversions);
2549 if (isset($pluginversions[$plugin])) {
2550 return $pluginversions[$plugin];
2559 * General class for all plugin types that do not have their own class
2561 class plugininfo_general extends plugininfo_base {
2566 * Class for page side blocks
2568 class plugininfo_block extends plugininfo_base {
2570 public static function get_plugins($type, $typerootdir, $typeclass) {
2572 // get the information about blocks at the disk
2573 $blocks = parent::get_plugins($type, $typerootdir, $typeclass);
2575 // add blocks missing from disk
2576 $blocksinfo = self::get_blocks_info();
2577 foreach ($blocksinfo as $blockname => $blockinfo) {
2578 if (isset($blocks[$blockname])) {
2581 $plugin = new $typeclass();
2582 $plugin->type = $type;
2583 $plugin->typerootdir = $typerootdir;
2584 $plugin->name = $blockname;
2585 $plugin->rootdir = null;
2586 $plugin->displayname = $blockname;
2587 $plugin->versiondb = $blockinfo->version;
2588 $plugin->init_is_standard();
2590 $blocks[$blockname] = $plugin;
2597 * Magic method getter, redirects to read only values.
2599 * For block plugins pretends the object has 'visible' property for compatibility
2600 * with plugins developed for Moodle version below 2.4
2602 * @param string $name
2605 public function __get($name) {
2606 if ($name === 'visible') {
2607 debugging('This is now an instance of plugininfo_block, please use $block->is_enabled() instead of $block->visible', DEBUG_DEVELOPER);
2608 return ($this->is_enabled() !== false);
2610 return parent::__get($name);
2613 public function init_display_name() {
2615 if (get_string_manager()->string_exists('pluginname', 'block_' . $this->name)) {
2616 $this->displayname = get_string('pluginname', 'block_' . $this->name);
2618 } else if (($block = block_instance($this->name)) !== false) {
2619 $this->displayname = $block->get_title();
2622 parent::init_display_name();
2626 public function load_db_version() {
2629 $blocksinfo = self::get_blocks_info();
2630 if (isset($blocksinfo[$this->name]->version)) {
2631 $this->versiondb = $blocksinfo[$this->name]->version;
2635 public function is_enabled() {
2637 $blocksinfo = self::get_blocks_info();
2638 if (isset($blocksinfo[$this->name]->visible)) {
2639 if ($blocksinfo[$this->name]->visible) {
2645 return parent::is_enabled();
2649 public function get_settings_section_name() {
2650 return 'blocksetting' . $this->name;
2653 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
2654 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
2655 $ADMIN = $adminroot; // may be used in settings.php
2656 $block = $this; // also can be used inside settings.php
2657 $section = $this->get_settings_section_name();
2659 if (!$hassiteconfig || (($blockinstance = block_instance($this->name)) === false)) {
2664 if ($blockinstance->has_config()) {
2665 if (file_exists($this->full_path('settings.php'))) {
2666 $settings = new admin_settingpage($section, $this->displayname,
2667 'moodle/site:config', $this->is_enabled() === false);
2668 include($this->full_path('settings.php')); // this may also set $settings to null
2670 $blocksinfo = self::get_blocks_info();
2671 $settingsurl = new moodle_url('/admin/block.php', array('block' => $blocksinfo[$this->name]->id));
2672 $settings = new admin_externalpage($section, $this->displayname,
2673 $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
2677 $ADMIN->add($parentnodename, $settings);
2681 public function get_uninstall_url() {
2683 $blocksinfo = self::get_blocks_info();
2684 return new moodle_url('/admin/blocks.php', array('delete' => $blocksinfo[$this->name]->id, 'sesskey' => sesskey()));
2688 * Provides access to the records in {block} table
2690 * @param bool $disablecache do not attempt to obtain data from the cache
2691 * @return array array of stdClasses
2693 protected static function get_blocks_info($disablecache=false) {
2696 $cache = cache::make('core', 'plugininfo_block');
2698 $blocktypes = $cache->get('blocktypes');
2700 if ($blocktypes === false or $disablecache) {
2702 $blocktypes = $DB->get_records('block', null, 'name', 'name,id,version,visible');
2703 } catch (dml_exception $e) {
2705 $blocktypes = array();
2707 $cache->set('blocktypes', $blocktypes);
2716 * Class for text filters
2718 class plugininfo_filter extends plugininfo_base {
2720 public static function get_plugins($type, $typerootdir, $typeclass) {
2725 // get the list of filters in /filter location
2726 $installed = filter_get_all_installed();
2728 foreach ($installed as $name => $displayname) {
2729 $plugin = new $typeclass();
2730 $plugin->type = $type;
2731 $plugin->typerootdir = $typerootdir;
2732 $plugin->name = $name;
2733 $plugin->rootdir = "$CFG->dirroot/filter/$name";
2734 $plugin->displayname = $displayname;
2736 $plugin->load_disk_version();
2737 $plugin->load_db_version();
2738 $plugin->load_required_main_version();
2739 $plugin->init_is_standard();
2741 $filters[$plugin->name] = $plugin;
2744 // Do not mess with filter registration here!
2746 $globalstates = self::get_global_states();
2748 // make sure that all registered filters are installed, just in case
2749 foreach ($globalstates as $name => $info) {
2750 if (!isset($filters[$name])) {
2751 // oops, there is a record in filter_active but the filter is not installed
2752 $plugin = new $typeclass();
2753 $plugin->type = $type;
2754 $plugin->typerootdir = $typerootdir;
2755 $plugin->name = $name;
2756 $plugin->rootdir = "$CFG->dirroot/filter/$name";
2757 $plugin->displayname = $name;
2759 $plugin->load_db_version();
2761 if (is_null($plugin->versiondb)) {
2762 // this is a hack to stimulate 'Missing from disk' error
2763 // because $plugin->versiondisk will be null !== false
2764 $plugin->versiondb = false;
2767 $filters[$plugin->name] = $plugin;
2774 public function init_display_name() {
2775 // do nothing, the name is set in self::get_plugins()
2778 public function is_enabled() {
2780 $globalstates = self::get_global_states();
2782 foreach ($globalstates as $name => $info) {
2783 if ($name === $this->name) {
2784 if ($info->active == TEXTFILTER_DISABLED) {
2787 // it may be 'On' or 'Off, but available'
2796 public function get_settings_section_name() {
2797 return 'filtersetting' . $this->name;
2800 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
2801 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
2802 $ADMIN = $adminroot; // may be used in settings.php
2803 $filter = $this; // also can be used inside settings.php
2806 if ($hassiteconfig && file_exists($this->full_path('filtersettings.php'))) {
2807 $section = $this->get_settings_section_name();
2808 $settings = new admin_settingpage($section, $this->displayname,
2809 'moodle/site:config', $this->is_enabled() === false);
2810 include($this->full_path('filtersettings.php')); // this may also set $settings to null
2813 $ADMIN->add($parentnodename, $settings);
2817 public function get_uninstall_url() {
2818 return new moodle_url('/admin/filters.php', array('sesskey' => sesskey(), 'filterpath' => $this->name, 'action' => 'delete'));
2822 * Provides access to the results of {@link filter_get_global_states()}
2823 * but indexed by the normalized filter name
2825 * The legacy filter name is available as ->legacyname property.
2827 * @param bool $disablecache do not attempt to obtain data from the cache
2830 protected static function get_global_states($disablecache=false) {
2833 $cache = cache::make('core', 'plugininfo_filter');
2835 $globalstates = $cache->get('globalstates');
2837 if ($globalstates === false or $disablecache) {
2839 if (!$DB->get_manager()->table_exists('filter_active')) {
2840 // Not installed yet.
2841 $cache->set('globalstates', array());
2845 $globalstates = array();
2847 foreach (filter_get_global_states() as $name => $info) {
2848 if (strpos($name, '/') !== false) {
2849 // Skip existing before upgrade to new names.
2853 $filterinfo = new stdClass();
2854 $filterinfo->active = $info->active;
2855 $filterinfo->sortorder = $info->sortorder;
2856 $globalstates[$name] = $filterinfo;
2859 $cache->set('globalstates', $globalstates);
2862 return $globalstates;
2868 * Class for activity modules
2870 class plugininfo_mod extends plugininfo_base {
2872 public static function get_plugins($type, $typerootdir, $typeclass) {
2874 // get the information about plugins at the disk
2875 $modules = parent::get_plugins($type, $typerootdir, $typeclass);
2877 // add modules missing from disk
2878 $modulesinfo = self::get_modules_info();
2879 foreach ($modulesinfo as $modulename => $moduleinfo) {
2880 if (isset($modules[$modulename])) {
2883 $plugin = new $typeclass();
2884 $plugin->type = $type;
2885 $plugin->typerootdir = $typerootdir;
2886 $plugin->name = $modulename;
2887 $plugin->rootdir = null;
2888 $plugin->displayname = $modulename;
2889 $plugin->versiondb = $moduleinfo->version;
2890 $plugin->init_is_standard();
2892 $modules[$modulename] = $plugin;
2899 * Magic method getter, redirects to read only values.
2901 * For module plugins we pretend the object has 'visible' property for compatibility
2902 * with plugins developed for Moodle version below 2.4
2904 * @param string $name
2907 public function __get($name) {
2908 if ($name === 'visible') {
2909 debugging('This is now an instance of plugininfo_mod, please use $module->is_enabled() instead of $module->visible', DEBUG_DEVELOPER);
2910 return ($this->is_enabled() !== false);
2912 return parent::__get($name);
2915 public function init_display_name() {
2916 if (get_string_manager()->string_exists('pluginname', $this->component)) {
2917 $this->displayname = get_string('pluginname', $this->component);
2919 $this->displayname = get_string('modulename', $this->component);
2924 * Load the data from version.php.
2926 * @param bool $disablecache do not attempt to obtain data from the cache
2927 * @return object the data object defined in version.php.
2929 protected function load_version_php($disablecache=false) {
2931 $cache = cache::make('core', 'plugininfo_base');
2933 $versionsphp = $cache->get('versions_php');
2935 if (!$disablecache and $versionsphp !== false and isset($versionsphp[$this->component])) {
2936 return $versionsphp[$this->component];
2939 $versionfile = $this->full_path('version.php');
2941 $module = new stdClass();
2942 $plugin = new stdClass();
2943 if (is_readable($versionfile)) {
2944 include($versionfile);
2946 if (!isset($module->version) and isset($plugin->version)) {
2949 $versionsphp[$this->component] = $module;
2950 $cache->set('versions_php', $versionsphp);
2955 public function load_db_version() {
2958 $modulesinfo = self::get_modules_info();
2959 if (isset($modulesinfo[$this->name]->version)) {
2960 $this->versiondb = $modulesinfo[$this->name]->version;
2964 public function is_enabled() {
2966 $modulesinfo = self::get_modules_info();
2967 if (isset($modulesinfo[$this->name]->visible)) {
2968 if ($modulesinfo[$this->name]->visible) {
2974 return parent::is_enabled();
2978 public function get_settings_section_name() {
2979 return 'modsetting' . $this->name;
2982 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
2983 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
2984 $ADMIN = $adminroot; // may be used in settings.php
2985 $module = $this; // also can be used inside settings.php
2986 $section = $this->get_settings_section_name();
2988 $modulesinfo = self::get_modules_info();
2990 if ($hassiteconfig && isset($modulesinfo[$this->name]) && file_exists($this->full_path('settings.php'))) {
2991 $settings = new admin_settingpage($section, $this->displayname,
2992 'moodle/site:config', $this->is_enabled() === false);
2993 include($this->full_path('settings.php')); // this may also set $settings to null
2996 $ADMIN->add($parentnodename, $settings);
3000 public function get_uninstall_url() {
3002 if ($this->name !== 'forum') {
3003 return new moodle_url('/admin/modules.php', array('delete' => $this->name, 'sesskey' => sesskey()));
3010 * Provides access to the records in {modules} table
3012 * @param bool $disablecache do not attempt to obtain data from the cache
3013 * @return array array of stdClasses
3015 protected static function get_modules_info($disablecache=false) {
3018 $cache = cache::make('core', 'plugininfo_mod');
3020 $modulesinfo = $cache->get('modulesinfo');
3022 if ($modulesinfo === false or $disablecache) {
3024 $modulesinfo = $DB->get_records('modules', null, 'name', 'name,id,version,visible');
3025 } catch (dml_exception $e) {
3027 $modulesinfo = array();
3029 $cache->set('modulesinfo', $modulesinfo);
3032 return $modulesinfo;
3038 * Class for question behaviours.
3040 class plugininfo_qbehaviour extends plugininfo_base {
3042 public function get_uninstall_url() {
3043 return new moodle_url('/admin/qbehaviours.php',
3044 array('delete' => $this->name, 'sesskey' => sesskey()));
3050 * Class for question types
3052 class plugininfo_qtype extends plugininfo_base {
3054 public function get_uninstall_url() {
3055 return new moodle_url('/admin/qtypes.php',
3056 array('delete' => $this->name, 'sesskey' => sesskey()));
3059 public function get_settings_section_name() {
3060 return 'qtypesetting' . $this->name;
3063 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3064 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3065 $ADMIN = $adminroot; // may be used in settings.php
3066 $qtype = $this; // also can be used inside settings.php
3067 $section = $this->get_settings_section_name();
3070 $systemcontext = context_system::instance();
3071 if (($hassiteconfig || has_capability('moodle/question:config', $systemcontext)) &&
3072 file_exists($this->full_path('settings.php'))) {
3073 $settings = new admin_settingpage($section, $this->displayname,
3074 'moodle/question:config', $this->is_enabled() === false);
3075 include($this->full_path('settings.php')); // this may also set $settings to null
3078 $ADMIN->add($parentnodename, $settings);
3085 * Class for authentication plugins
3087 class plugininfo_auth extends plugininfo_base {
3089 public function is_enabled() {
3092 if (in_array($this->name, array('nologin', 'manual'))) {
3093 // these two are always enabled and can't be disabled
3097 $enabled = array_flip(explode(',', $CFG->auth));
3099 return isset($enabled[$this->name]);
3102 public function get_settings_section_name() {
3103 return 'authsetting' . $this->name;
3106 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3107 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3108 $ADMIN = $adminroot; // may be used in settings.php
3109 $auth = $this; // also to be used inside settings.php
3110 $section = $this->get_settings_section_name();
3113 if ($hassiteconfig) {
3114 if (file_exists($this->full_path('settings.php'))) {
3115 // TODO: finish implementation of common settings - locking, etc.
3116 $settings = new admin_settingpage($section, $this->displayname,
3117 'moodle/site:config', $this->is_enabled() === false);
3118 include($this->full_path('settings.php')); // this may also set $settings to null
3120 $settingsurl = new moodle_url('/admin/auth_config.php', array('auth' => $this->name));
3121 $settings = new admin_externalpage($section, $this->displayname,
3122 $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
3126 $ADMIN->add($parentnodename, $settings);
3133 * Class for enrolment plugins
3135 class plugininfo_enrol extends plugininfo_base {
3137 public function is_enabled() {
3140 // We do not actually need whole enrolment classes here so we do not call
3141 // {@link enrol_get_plugins()}. Note that this may produce slightly different
3142 // results, for example if the enrolment plugin does not contain lib.php
3143 // but it is listed in $CFG->enrol_plugins_enabled
3145 $enabled = array_flip(explode(',', $CFG->enrol_plugins_enabled));
3147 return isset($enabled[$this->name]);
3150 public function get_settings_section_name() {
3151 if (file_exists($this->full_path('settings.php'))) {
3152 return 'enrolsettings' . $this->name;
3158 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3159 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3161 if (!$hassiteconfig or !file_exists($this->full_path('settings.php'))) {
3164 $section = $this->get_settings_section_name();
3166 $ADMIN = $adminroot; // may be used in settings.php
3167 $enrol = $this; // also can be used inside settings.php
3168 $settings = new admin_settingpage($section, $this->displayname,
3169 'moodle/site:config', $this->is_enabled() === false);
3171 include($this->full_path('settings.php')); // This may also set $settings to null!
3174 $ADMIN->add($parentnodename, $settings);
3178 public function get_uninstall_url() {
3179 return new moodle_url('/admin/enrol.php', array('action' => 'uninstall', 'enrol' => $this->name, 'sesskey' => sesskey()));
3185 * Class for messaging processors
3187 class plugininfo_message extends plugininfo_base {
3189 public function get_settings_section_name() {
3190 return 'messagesetting' . $this->name;
3193 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3194 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3195 $ADMIN = $adminroot; // may be used in settings.php
3196 if (!$hassiteconfig) {
3199 $section = $this->get_settings_section_name();
3202 $processors = get_message_processors();
3203 if (isset($processors[$this->name])) {
3204 $processor = $processors[$this->name];
3205 if ($processor->available && $processor->hassettings) {
3206 $settings = new admin_settingpage($section, $this->displayname,
3207 'moodle/site:config', $this->is_enabled() === false);
3208 include($this->full_path('settings.php')); // this may also set $settings to null
3212 $ADMIN->add($parentnodename, $settings);
3217 * @see plugintype_interface::is_enabled()
3219 public function is_enabled() {
3220 $processors = get_message_processors();
3221 if (isset($processors[$this->name])) {
3222 return $processors[$this->name]->configured && $processors[$this->name]->enabled;
3224 return parent::is_enabled();
3229 * @see plugintype_interface::get_uninstall_url()
3231 public function get_uninstall_url() {
3232 $processors = get_message_processors();
3233 if (isset($processors[$this->name])) {
3234 return new moodle_url('/admin/message.php', array('uninstall' => $processors[$this->name]->id, 'sesskey' => sesskey()));
3236 return parent::get_uninstall_url();
3243 * Class for repositories
3245 class plugininfo_repository extends plugininfo_base {
3247 public function is_enabled() {
3249 $enabled = self::get_enabled_repositories();
3251 return isset($enabled[$this->name]);
3254 public function get_settings_section_name() {
3255 return 'repositorysettings'.$this->name;
3258 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3259 if ($hassiteconfig && $this->is_enabled()) {
3260 // completely no access to repository setting when it is not enabled
3261 $sectionname = $this->get_settings_section_name();
3262 $settingsurl = new moodle_url('/admin/repository.php',
3263 array('sesskey' => sesskey(), 'action' => 'edit', 'repos' => $this->name));
3264 $settings = new admin_externalpage($sectionname, $this->displayname,
3265 $settingsurl, 'moodle/site:config', false);
3266 $adminroot->add($parentnodename, $settings);
3271 * Provides access to the records in {repository} table
3273 * @param bool $disablecache do not attempt to obtain data from the cache
3274 * @return array array of stdClasses
3276 protected static function get_enabled_repositories($disablecache=false) {
3279 $cache = cache::make('core', 'plugininfo_repository');
3281 $enabled = $cache->get('enabled');
3283 if ($enabled === false or $disablecache) {
3284 $enabled = $DB->get_records('repository', null, 'type', 'type,visible,sortorder');
3285 $cache->set('enabled', $enabled);
3294 * Class for portfolios
3296 class plugininfo_portfolio extends plugininfo_base {
3298 public function is_enabled() {
3300 $enabled = self::get_enabled_portfolios();
3302 return isset($enabled[$this->name]);
3306 * Returns list of enabled portfolio plugins
3308 * Portfolio plugin is enabled if there is at least one record in the {portfolio_instance}
3311 * @param bool $disablecache do not attempt to obtain data from the cache
3312 * @return array array of stdClasses with properties plugin and visible indexed by plugin
3314 protected static function get_enabled_portfolios($disablecache=false) {
3317 $cache = cache::make('core', 'plugininfo_portfolio');
3319 $enabled = $cache->get('enabled');
3321 if ($enabled === false or $disablecache) {
3323 $instances = $DB->get_recordset('portfolio_instance', null, '', 'plugin,visible');
3324 foreach ($instances as $instance) {
3325 if (isset($enabled[$instance->plugin])) {
3326 if ($instance->visible) {
3327 $enabled[$instance->plugin]->visible = $instance->visible;
3330 $enabled[$instance->plugin] = $instance;
3333 $instances->close();
3334 $cache->set('enabled', $enabled);
3345 class plugininfo_theme extends plugininfo_base {
3347 public function is_enabled() {
3350 if ((!empty($CFG->theme) and $CFG->theme === $this->name) or
3351 (!empty($CFG->themelegacy) and $CFG->themelegacy === $this->name)) {
3354 return parent::is_enabled();
3361 * Class representing an MNet service
3363 class plugininfo_mnetservice extends plugininfo_base {
3365 public function is_enabled() {
3368 if (empty($CFG->mnet_dispatcher_mode) || $CFG->mnet_dispatcher_mode !== 'strict') {
3371 return parent::is_enabled();
3378 * Class for admin tool plugins
3380 class plugininfo_tool extends plugininfo_base {
3382 public function get_uninstall_url() {
3383 return new moodle_url('/admin/tools.php', array('delete' => $this->name, 'sesskey' => sesskey()));
3389 * Class for admin tool plugins
3391 class plugininfo_report extends plugininfo_base {
3393 public function get_uninstall_url() {
3394 return new moodle_url('/admin/reports.php', array('delete' => $this->name, 'sesskey' => sesskey()));
3400 * Class for local plugins
3402 class plugininfo_local extends plugininfo_base {
3404 public function get_uninstall_url() {
3405 return new moodle_url('/admin/localplugins.php', array('delete' => $this->name, 'sesskey' => sesskey()));
3410 * Class for HTML editors
3412 class plugininfo_editor extends plugininfo_base {
3414 public function get_settings_section_name() {
3415 return 'editorsettings' . $this->name;
3418 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3419 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3420 $ADMIN = $adminroot; // may be used in settings.php
3421 $editor = $this; // also can be used inside settings.php
3422 $section = $this->get_settings_section_name();
3425 if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3426 $settings = new admin_settingpage($section, $this->displayname,
3427 'moodle/site:config', $this->is_enabled() === false);
3428 include($this->full_path('settings.php')); // this may also set $settings to null
3431 $ADMIN->add($parentnodename, $settings);
3436 * Returns the information about plugin availability
3438 * True means that the plugin is enabled. False means that the plugin is
3439 * disabled. Null means that the information is not available, or the
3440 * plugin does not support configurable availability or the availability
3441 * can not be changed.
3445 public function is_enabled() {
3447 if (empty($CFG->texteditors)) {
3448 $CFG->texteditors = 'tinymce,textarea';
3450 if (in_array($this->name, explode(',', $CFG->texteditors))) {
3458 * Class for plagiarism plugins
3460 class plugininfo_plagiarism extends plugininfo_base {
3462 public function get_settings_section_name() {
3463 return 'plagiarism'. $this->name;
3466 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3467 // plagiarism plugin just redirect to settings.php in the plugins directory
3468 if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3469 $section = $this->get_settings_section_name();
3470 $settingsurl = new moodle_url($this->get_dir().'/settings.php');
3471 $settings = new admin_externalpage($section, $this->displayname,
3472 $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
3473 $adminroot->add($parentnodename, $settings);
3479 * Class for webservice protocols
3481 class plugininfo_webservice extends plugininfo_base {
3483 public function get_settings_section_name() {
3484 return 'webservicesetting' . $this->name;
3487 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3488 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3489 $ADMIN = $adminroot; // may be used in settings.php
3490 $webservice = $this; // also can be used inside settings.php
3491 $section = $this->get_settings_section_name();
3494 if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3495 $settings = new admin_settingpage($section, $this->displayname,
3496 'moodle/site:config', $this->is_enabled() === false);
3497 include($this->full_path('settings.php')); // this may also set $settings to null
3500 $ADMIN->add($parentnodename, $settings);
3504 public function is_enabled() {
3506 if (empty($CFG->enablewebservices)) {
3509 $active_webservices = empty($CFG->webserviceprotocols) ? array() : explode(',', $CFG->webserviceprotocols);
3510 if (in_array($this->name, $active_webservices)) {
3516 public function get_uninstall_url() {
3517 return new moodle_url('/admin/webservice/protocols.php',
3518 array('sesskey' => sesskey(), 'action' => 'uninstall', 'webservice' => $this->name));
3523 * Class for course formats
3525 class plugininfo_format extends plugininfo_base {
3528 * Gathers and returns the information about all plugins of the given type
3530 * @param string $type the name of the plugintype, eg. mod, auth or workshopform
3531 * @param string $typerootdir full path to the location of the plugin dir
3532 * @param string $typeclass the name of the actually called class
3533 * @return array of plugintype classes, indexed by the plugin name
3535 public static function get_plugins($type, $typerootdir, $typeclass) {
3537 $formats = parent::get_plugins($type, $typerootdir, $typeclass);
3538 require_once($CFG->dirroot.'/course/lib.php');
3539 $order = get_sorted_course_formats();
3540 $sortedformats = array();
3541 foreach ($order as $formatname) {
3542 $sortedformats[$formatname] = $formats[$formatname];
3544 return $sortedformats;
3547 public function get_settings_section_name() {
3548 return 'formatsetting' . $this->name;
3551 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3552 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3553 $ADMIN = $adminroot; // also may be used in settings.php
3554 $section = $this->get_settings_section_name();
3557 if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3558 $settings = new admin_settingpage($section, $this->displayname,
3559 'moodle/site:config', $this->is_enabled() === false);
3560 include($this->full_path('settings.php')); // this may also set $settings to null
3563 $ADMIN->add($parentnodename, $settings);
3567 public function is_enabled() {
3568 return !get_config($this->component, 'disabled');
3571 public function get_uninstall_url() {
3572 if ($this->name !== get_config('moodlecourse', 'format') && $this->name !== 'site') {
3573 return new moodle_url('/admin/courseformats.php',
3574 array('sesskey' => sesskey(), 'action' => 'uninstall', 'format' => $this->name));
3576 return parent::get_uninstall_url();