Merge branch 'MDL-38487_23' of git://github.com/timhunt/moodle into MOODLE_23_STABLE
[moodle.git] / lib / pluginlib.php
blob1bc425ae00f491d786fdd2eb98cc7d8e8fc8e768
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
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.
9 //
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/>.
18 /**
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.
25 * @package core
26 * @subpackage admin
27 * @copyright 2011 David Mudrak <david@moodle.com>
28 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
31 defined('MOODLE_INTERNAL') || die();
33 require_once($CFG->libdir.'/filelib.php'); // curl class needed here
35 /**
36 * Singleton class providing general plugins management functionality
38 class plugin_manager {
40 /** the plugin is shipped with standard Moodle distribution */
41 const PLUGIN_SOURCE_STANDARD = 'std';
42 /** the plugin is added extension */
43 const PLUGIN_SOURCE_EXTENSION = 'ext';
45 /** the plugin uses neither database nor capabilities, no versions */
46 const PLUGIN_STATUS_NODB = 'nodb';
47 /** the plugin is up-to-date */
48 const PLUGIN_STATUS_UPTODATE = 'uptodate';
49 /** the plugin is about to be installed */
50 const PLUGIN_STATUS_NEW = 'new';
51 /** the plugin is about to be upgraded */
52 const PLUGIN_STATUS_UPGRADE = 'upgrade';
53 /** the standard plugin is about to be deleted */
54 const PLUGIN_STATUS_DELETE = 'delete';
55 /** the version at the disk is lower than the one already installed */
56 const PLUGIN_STATUS_DOWNGRADE = 'downgrade';
57 /** the plugin is installed but missing from disk */
58 const PLUGIN_STATUS_MISSING = 'missing';
60 /** @var plugin_manager holds the singleton instance */
61 protected static $singletoninstance;
62 /** @var array of raw plugins information */
63 protected $pluginsinfo = null;
64 /** @var array of raw subplugins information */
65 protected $subpluginsinfo = null;
67 /**
68 * Direct initiation not allowed, use the factory method {@link self::instance()}
70 protected function __construct() {
73 /**
74 * Sorry, this is singleton
76 protected function __clone() {
79 /**
80 * Factory method for this class
82 * @return plugin_manager the singleton instance
84 public static function instance() {
85 if (is_null(self::$singletoninstance)) {
86 self::$singletoninstance = new self();
88 return self::$singletoninstance;
91 /**
92 * Reset any caches
93 * @param bool $phpunitreset
95 public static function reset_caches($phpunitreset = false) {
96 if ($phpunitreset) {
97 self::$singletoninstance = null;
102 * Returns a tree of known plugins and information about them
104 * @param bool $disablecache force reload, cache can be used otherwise
105 * @return array 2D array. The first keys are plugin type names (e.g. qtype);
106 * the second keys are the plugin local name (e.g. multichoice); and
107 * the values are the corresponding objects extending {@link plugininfo_base}
109 public function get_plugins($disablecache=false) {
110 global $CFG;
112 if ($disablecache or is_null($this->pluginsinfo)) {
113 $this->pluginsinfo = array();
114 $plugintypes = get_plugin_types();
115 $plugintypes = $this->reorder_plugin_types($plugintypes);
116 foreach ($plugintypes as $plugintype => $plugintyperootdir) {
117 if (in_array($plugintype, array('base', 'general'))) {
118 throw new coding_exception('Illegal usage of reserved word for plugin type');
120 if (class_exists('plugininfo_' . $plugintype)) {
121 $plugintypeclass = 'plugininfo_' . $plugintype;
122 } else {
123 $plugintypeclass = 'plugininfo_general';
125 if (!in_array('plugininfo_base', class_parents($plugintypeclass))) {
126 throw new coding_exception('Class ' . $plugintypeclass . ' must extend plugininfo_base');
128 $plugins = call_user_func(array($plugintypeclass, 'get_plugins'), $plugintype, $plugintyperootdir, $plugintypeclass);
129 $this->pluginsinfo[$plugintype] = $plugins;
132 if (empty($CFG->disableupdatenotifications) and !during_initial_install()) {
133 // append the information about available updates provided by {@link available_update_checker()}
134 $provider = available_update_checker::instance();
135 foreach ($this->pluginsinfo as $plugintype => $plugins) {
136 foreach ($plugins as $plugininfoholder) {
137 $plugininfoholder->check_available_updates($provider);
143 return $this->pluginsinfo;
147 * Returns list of plugins that define their subplugins and the information
148 * about them from the db/subplugins.php file.
150 * At the moment, only activity modules can define subplugins.
152 * @param bool $disablecache force reload, cache can be used otherwise
153 * @return array with keys like 'mod_quiz', and values the data from the
154 * corresponding db/subplugins.php file.
156 public function get_subplugins($disablecache=false) {
158 if ($disablecache or is_null($this->subpluginsinfo)) {
159 $this->subpluginsinfo = array();
160 $mods = get_plugin_list('mod');
161 foreach ($mods as $mod => $moddir) {
162 $modsubplugins = array();
163 if (file_exists($moddir . '/db/subplugins.php')) {
164 include($moddir . '/db/subplugins.php');
165 foreach ($subplugins as $subplugintype => $subplugintyperootdir) {
166 $subplugin = new stdClass();
167 $subplugin->type = $subplugintype;
168 $subplugin->typerootdir = $subplugintyperootdir;
169 $modsubplugins[$subplugintype] = $subplugin;
171 $this->subpluginsinfo['mod_' . $mod] = $modsubplugins;
176 return $this->subpluginsinfo;
180 * Returns the name of the plugin that defines the given subplugin type
182 * If the given subplugin type is not actually a subplugin, returns false.
184 * @param string $subplugintype the name of subplugin type, eg. workshopform or quiz
185 * @return false|string the name of the parent plugin, eg. mod_workshop
187 public function get_parent_of_subplugin($subplugintype) {
189 $parent = false;
190 foreach ($this->get_subplugins() as $pluginname => $subplugintypes) {
191 if (isset($subplugintypes[$subplugintype])) {
192 $parent = $pluginname;
193 break;
197 return $parent;
201 * Returns a localized name of a given plugin
203 * @param string $plugin name of the plugin, eg mod_workshop or auth_ldap
204 * @return string
206 public function plugin_name($plugin) {
207 list($type, $name) = normalize_component($plugin);
208 return $this->pluginsinfo[$type][$name]->displayname;
212 * Returns a localized name of a plugin type in plural form
214 * Most plugin types define their names in core_plugin lang file. In case of subplugins,
215 * we try to ask the parent plugin for the name. In the worst case, we will return
216 * the value of the passed $type parameter.
218 * @param string $type the type of the plugin, e.g. mod or workshopform
219 * @return string
221 public function plugintype_name_plural($type) {
223 if (get_string_manager()->string_exists('type_' . $type . '_plural', 'core_plugin')) {
224 // for most plugin types, their names are defined in core_plugin lang file
225 return get_string('type_' . $type . '_plural', 'core_plugin');
227 } else if ($parent = $this->get_parent_of_subplugin($type)) {
228 // if this is a subplugin, try to ask the parent plugin for the name
229 if (get_string_manager()->string_exists('subplugintype_' . $type . '_plural', $parent)) {
230 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type . '_plural', $parent);
231 } else {
232 return $this->plugin_name($parent) . ' / ' . $type;
235 } else {
236 return $type;
241 * @param string $component frankenstyle component name.
242 * @return plugininfo_base|null the corresponding plugin information.
244 public function get_plugin_info($component) {
245 list($type, $name) = normalize_component($component);
246 $plugins = $this->get_plugins();
247 if (isset($plugins[$type][$name])) {
248 return $plugins[$type][$name];
249 } else {
250 return null;
255 * Get a list of any other plugins that require this one.
256 * @param string $component frankenstyle component name.
257 * @return array of frankensyle component names that require this one.
259 public function other_plugins_that_require($component) {
260 $others = array();
261 foreach ($this->get_plugins() as $type => $plugins) {
262 foreach ($plugins as $plugin) {
263 $required = $plugin->get_other_required_plugins();
264 if (isset($required[$component])) {
265 $others[] = $plugin->component;
269 return $others;
273 * Check a dependencies list against the list of installed plugins.
274 * @param array $dependencies compenent name to required version or ANY_VERSION.
275 * @return bool true if all the dependencies are satisfied.
277 public function are_dependencies_satisfied($dependencies) {
278 foreach ($dependencies as $component => $requiredversion) {
279 $otherplugin = $this->get_plugin_info($component);
280 if (is_null($otherplugin)) {
281 return false;
284 if ($requiredversion != ANY_VERSION and $otherplugin->versiondisk < $requiredversion) {
285 return false;
289 return true;
293 * Checks all dependencies for all installed plugins
295 * This is used by install and upgrade. The array passed by reference as the second
296 * argument is populated with the list of plugins that have failed dependencies (note that
297 * a single plugin can appear multiple times in the $failedplugins).
299 * @param int $moodleversion the version from version.php.
300 * @param array $failedplugins to return the list of plugins with non-satisfied dependencies
301 * @return bool true if all the dependencies are satisfied for all plugins.
303 public function all_plugins_ok($moodleversion, &$failedplugins = array()) {
305 $return = true;
306 foreach ($this->get_plugins() as $type => $plugins) {
307 foreach ($plugins as $plugin) {
309 if (!$plugin->is_core_dependency_satisfied($moodleversion)) {
310 $return = false;
311 $failedplugins[] = $plugin->component;
314 if (!$this->are_dependencies_satisfied($plugin->get_other_required_plugins())) {
315 $return = false;
316 $failedplugins[] = $plugin->component;
321 return $return;
325 * Checks if there are some plugins with a known available update
327 * @return bool true if there is at least one available update
329 public function some_plugins_updatable() {
330 foreach ($this->get_plugins() as $type => $plugins) {
331 foreach ($plugins as $plugin) {
332 if ($plugin->available_updates()) {
333 return true;
338 return false;
342 * Defines a list of all plugins that were originally shipped in the standard Moodle distribution,
343 * but are not anymore and are deleted during upgrades.
345 * The main purpose of this list is to hide missing plugins during upgrade.
347 * @param string $type plugin type
348 * @param string $name plugin name
349 * @return bool
351 public static function is_deleted_standard_plugin($type, $name) {
352 static $plugins = array(
353 // do not add 1.9-2.2 plugin removals here
356 if (!isset($plugins[$type])) {
357 return false;
359 return in_array($name, $plugins[$type]);
363 * Defines a white list of all plugins shipped in the standard Moodle distribution
365 * @param string $type
366 * @return false|array array of standard plugins or false if the type is unknown
368 public static function standard_plugins_list($type) {
369 static $standard_plugins = array(
371 'assignment' => array(
372 'offline', 'online', 'upload', 'uploadsingle'
375 'assignsubmission' => array(
376 'comments', 'file', 'onlinetext'
379 'assignfeedback' => array(
380 'comments', 'file'
383 'auth' => array(
384 'cas', 'db', 'email', 'fc', 'imap', 'ldap', 'manual', 'mnet',
385 'nntp', 'nologin', 'none', 'pam', 'pop3', 'radius',
386 'shibboleth', 'webservice'
389 'block' => array(
390 'activity_modules', 'admin_bookmarks', 'blog_menu',
391 'blog_recent', 'blog_tags', 'calendar_month',
392 'calendar_upcoming', 'comments', 'community',
393 'completionstatus', 'course_list', 'course_overview',
394 'course_summary', 'feedback', 'glossary_random', 'html',
395 'login', 'mentees', 'messages', 'mnet_hosts', 'myprofile',
396 'navigation', 'news_items', 'online_users', 'participants',
397 'private_files', 'quiz_results', 'recent_activity',
398 'rss_client', 'search_forums', 'section_links',
399 'selfcompletion', 'settings', 'site_main_menu',
400 'social_activities', 'tag_flickr', 'tag_youtube', 'tags'
403 'booktool' => array(
404 'exportimscp', 'importhtml', 'print'
407 'coursereport' => array(
408 //deprecated!
411 'datafield' => array(
412 'checkbox', 'date', 'file', 'latlong', 'menu', 'multimenu',
413 'number', 'picture', 'radiobutton', 'text', 'textarea', 'url'
416 'datapreset' => array(
417 'imagegallery'
420 'editor' => array(
421 'textarea', 'tinymce'
424 'enrol' => array(
425 'authorize', 'category', 'cohort', 'database', 'flatfile',
426 'guest', 'imsenterprise', 'ldap', 'manual', 'meta', 'mnet',
427 'paypal', 'self'
430 'filter' => array(
431 'activitynames', 'algebra', 'censor', 'emailprotect',
432 'emoticon', 'mediaplugin', 'multilang', 'tex', 'tidy',
433 'urltolink', 'data', 'glossary'
436 'format' => array(
437 'scorm', 'social', 'topics', 'weeks'
440 'gradeexport' => array(
441 'ods', 'txt', 'xls', 'xml'
444 'gradeimport' => array(
445 'csv', 'xml'
448 'gradereport' => array(
449 'grader', 'outcomes', 'overview', 'user'
452 'gradingform' => array(
453 'rubric', 'guide'
456 'local' => array(
459 'message' => array(
460 'email', 'jabber', 'popup'
463 'mnetservice' => array(
464 'enrol'
467 'mod' => array(
468 'assign', 'assignment', 'book', 'chat', 'choice', 'data', 'feedback', 'folder',
469 'forum', 'glossary', 'imscp', 'label', 'lesson', 'lti', 'page',
470 'quiz', 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop'
473 'plagiarism' => array(
476 'portfolio' => array(
477 'boxnet', 'download', 'flickr', 'googledocs', 'mahara', 'picasa'
480 'profilefield' => array(
481 'checkbox', 'datetime', 'menu', 'text', 'textarea'
484 'qbehaviour' => array(
485 'adaptive', 'adaptivenopenalty', 'deferredcbm',
486 'deferredfeedback', 'immediatecbm', 'immediatefeedback',
487 'informationitem', 'interactive', 'interactivecountback',
488 'manualgraded', 'missing'
491 'qformat' => array(
492 'aiken', 'blackboard', 'blackboard_six', 'examview', 'gift',
493 'learnwise', 'missingword', 'multianswer', 'webct',
494 'xhtml', 'xml'
497 'qtype' => array(
498 'calculated', 'calculatedmulti', 'calculatedsimple',
499 'description', 'essay', 'match', 'missingtype', 'multianswer',
500 'multichoice', 'numerical', 'random', 'randomsamatch',
501 'shortanswer', 'truefalse'
504 'quiz' => array(
505 'grading', 'overview', 'responses', 'statistics'
508 'quizaccess' => array(
509 'delaybetweenattempts', 'ipaddress', 'numattempts', 'openclosedate',
510 'password', 'safebrowser', 'securewindow', 'timelimit'
513 'report' => array(
514 'backups', 'completion', 'configlog', 'courseoverview',
515 'log', 'loglive', 'outline', 'participation', 'progress', 'questioninstances', 'security', 'stats'
518 'repository' => array(
519 'alfresco', 'boxnet', 'coursefiles', 'dropbox', 'equella', 'filesystem',
520 'flickr', 'flickr_public', 'googledocs', 'local', 'merlot',
521 'picasa', 'recent', 's3', 'upload', 'url', 'user', 'webdav',
522 'wikimedia', 'youtube'
525 'scormreport' => array(
526 'basic',
527 'interactions',
528 'graphs'
531 'theme' => array(
532 'afterburner', 'anomaly', 'arialist', 'base', 'binarius',
533 'boxxie', 'brick', 'canvas', 'formal_white', 'formfactor',
534 'fusion', 'leatherbound', 'magazine', 'mymobile', 'nimble',
535 'nonzero', 'overlay', 'serenity', 'sky_high', 'splash',
536 'standard', 'standardold'
539 'tool' => array(
540 'assignmentupgrade', 'bloglevelupgrade', 'capability', 'customlang', 'dbtransfer', 'generator',
541 'health', 'innodb', 'langimport', 'multilangupgrade', 'phpunit', 'profiling',
542 'qeupgradehelper', 'replace', 'spamcleaner', 'timezoneimport', 'unittest',
543 'uploaduser', 'unsuproles', 'xmldb'
546 'webservice' => array(
547 'amf', 'rest', 'soap', 'xmlrpc'
550 'workshopallocation' => array(
551 'manual', 'random', 'scheduled'
554 'workshopeval' => array(
555 'best'
558 'workshopform' => array(
559 'accumulative', 'comments', 'numerrors', 'rubric'
563 if (isset($standard_plugins[$type])) {
564 return $standard_plugins[$type];
565 } else {
566 return false;
571 * Reorders plugin types into a sequence to be displayed
573 * For technical reasons, plugin types returned by {@link get_plugin_types()} are
574 * in a certain order that does not need to fit the expected order for the display.
575 * Particularly, activity modules should be displayed first as they represent the
576 * real heart of Moodle. They should be followed by other plugin types that are
577 * used to build the courses (as that is what one expects from LMS). After that,
578 * other supportive plugin types follow.
580 * @param array $types associative array
581 * @return array same array with altered order of items
583 protected function reorder_plugin_types(array $types) {
584 $fix = array(
585 'mod' => $types['mod'],
586 'block' => $types['block'],
587 'qtype' => $types['qtype'],
588 'qbehaviour' => $types['qbehaviour'],
589 'qformat' => $types['qformat'],
590 'filter' => $types['filter'],
591 'enrol' => $types['enrol'],
593 foreach ($types as $type => $path) {
594 if (!isset($fix[$type])) {
595 $fix[$type] = $path;
598 return $fix;
604 * General exception thrown by the {@link available_update_checker} class
606 class available_update_checker_exception extends moodle_exception {
609 * @param string $errorcode exception description identifier
610 * @param mixed $debuginfo debugging data to display
612 public function __construct($errorcode, $debuginfo=null) {
613 parent::__construct($errorcode, 'core_plugin', '', null, print_r($debuginfo, true));
619 * Singleton class that handles checking for available updates
621 class available_update_checker {
623 /** @var available_update_checker holds the singleton instance */
624 protected static $singletoninstance;
625 /** @var null|int the timestamp of when the most recent response was fetched */
626 protected $recentfetch = null;
627 /** @var null|array the recent response from the update notification provider */
628 protected $recentresponse = null;
629 /** @var null|string the numerical version of the local Moodle code */
630 protected $currentversion = null;
631 /** @var null|string the release info of the local Moodle code */
632 protected $currentrelease = null;
633 /** @var null|string branch of the local Moodle code */
634 protected $currentbranch = null;
635 /** @var array of (string)frankestyle => (string)version list of additional plugins deployed at this site */
636 protected $currentplugins = array();
639 * Direct initiation not allowed, use the factory method {@link self::instance()}
641 protected function __construct() {
645 * Sorry, this is singleton
647 protected function __clone() {
651 * Factory method for this class
653 * @return available_update_checker the singleton instance
655 public static function instance() {
656 if (is_null(self::$singletoninstance)) {
657 self::$singletoninstance = new self();
659 return self::$singletoninstance;
663 * Reset any caches
664 * @param bool $phpunitreset
666 public static function reset_caches($phpunitreset = false) {
667 if ($phpunitreset) {
668 self::$singletoninstance = null;
673 * Returns the timestamp of the last execution of {@link fetch()}
675 * @return int|null null if it has never been executed or we don't known
677 public function get_last_timefetched() {
679 $this->restore_response();
681 if (!empty($this->recentfetch)) {
682 return $this->recentfetch;
684 } else {
685 return null;
690 * Fetches the available update status from the remote site
692 * @throws available_update_checker_exception
694 public function fetch() {
695 $response = $this->get_response();
696 $this->validate_response($response);
697 $this->store_response($response);
701 * Returns the available update information for the given component
703 * This method returns null if the most recent response does not contain any information
704 * about it. The returned structure is an array of available updates for the given
705 * component. Each update info is an object with at least one property called
706 * 'version'. Other possible properties are 'release', 'maturity', 'url' and 'downloadurl'.
708 * For the 'core' component, the method returns real updates only (those with higher version).
709 * For all other components, the list of all known remote updates is returned and the caller
710 * (usually the {@link plugin_manager}) is supposed to make the actual comparison of versions.
712 * @param string $component frankenstyle
713 * @param array $options with supported keys 'minmaturity' and/or 'notifybuilds'
714 * @return null|array null or array of available_update_info objects
716 public function get_update_info($component, array $options = array()) {
718 if (!isset($options['minmaturity'])) {
719 $options['minmaturity'] = 0;
722 if (!isset($options['notifybuilds'])) {
723 $options['notifybuilds'] = false;
726 if ($component == 'core') {
727 $this->load_current_environment();
730 $this->restore_response();
732 if (empty($this->recentresponse['updates'][$component])) {
733 return null;
736 $updates = array();
737 foreach ($this->recentresponse['updates'][$component] as $info) {
738 $update = new available_update_info($component, $info);
739 if (isset($update->maturity) and ($update->maturity < $options['minmaturity'])) {
740 continue;
742 if ($component == 'core') {
743 if ($update->version <= $this->currentversion) {
744 continue;
746 if (empty($options['notifybuilds']) and $this->is_same_release($update->release)) {
747 continue;
750 $updates[] = $update;
753 if (empty($updates)) {
754 return null;
757 return $updates;
761 * The method being run via cron.php
763 public function cron() {
764 global $CFG;
766 if (!$this->cron_autocheck_enabled()) {
767 $this->cron_mtrace('Automatic check for available updates not enabled, skipping.');
768 return;
771 $now = $this->cron_current_timestamp();
773 if ($this->cron_has_fresh_fetch($now)) {
774 $this->cron_mtrace('Recently fetched info about available updates is still fresh enough, skipping.');
775 return;
778 if ($this->cron_has_outdated_fetch($now)) {
779 $this->cron_mtrace('Outdated or missing info about available updates, forced fetching ... ', '');
780 $this->cron_execute();
781 return;
784 $offset = $this->cron_execution_offset();
785 $start = mktime(1, 0, 0, date('n', $now), date('j', $now), date('Y', $now)); // 01:00 AM today local time
786 if ($now > $start + $offset) {
787 $this->cron_mtrace('Regular daily check for available updates ... ', '');
788 $this->cron_execute();
789 return;
793 /// end of public API //////////////////////////////////////////////////////
796 * Makes cURL request to get data from the remote site
798 * @return string raw request result
799 * @throws available_update_checker_exception
801 protected function get_response() {
802 $curl = new curl(array('proxy' => true));
803 $response = $curl->post($this->prepare_request_url(), $this->prepare_request_params());
804 $curlinfo = $curl->get_info();
805 if ($curlinfo['http_code'] != 200) {
806 throw new available_update_checker_exception('err_response_http_code', $curlinfo['http_code']);
808 return $response;
812 * Makes sure the response is valid, has correct API format etc.
814 * @param string $response raw response as returned by the {@link self::get_response()}
815 * @throws available_update_checker_exception
817 protected function validate_response($response) {
819 $response = $this->decode_response($response);
821 if (empty($response)) {
822 throw new available_update_checker_exception('err_response_empty');
825 if (empty($response['status']) or $response['status'] !== 'OK') {
826 throw new available_update_checker_exception('err_response_status', $response['status']);
829 if (empty($response['apiver']) or $response['apiver'] !== '1.0') {
830 throw new available_update_checker_exception('err_response_format_version', $response['apiver']);
833 if (empty($response['forbranch']) or $response['forbranch'] !== moodle_major_version(true)) {
834 throw new available_update_checker_exception('err_response_target_version', $response['forbranch']);
839 * Decodes the raw string response from the update notifications provider
841 * @param string $response as returned by {@link self::get_response()}
842 * @return array decoded response structure
844 protected function decode_response($response) {
845 return json_decode($response, true);
849 * Stores the valid fetched response for later usage
851 * This implementation uses the config_plugins table as the permanent storage.
853 * @param string $response raw valid data returned by {@link self::get_response()}
855 protected function store_response($response) {
857 set_config('recentfetch', time(), 'core_plugin');
858 set_config('recentresponse', $response, 'core_plugin');
860 $this->restore_response(true);
864 * Loads the most recent raw response record we have fetched
866 * After this method is called, $this->recentresponse is set to an array. If the
867 * array is empty, then either no data have been fetched yet or the fetched data
868 * do not have expected format (and thence they are ignored and a debugging
869 * message is displayed).
871 * This implementation uses the config_plugins table as the permanent storage.
873 * @param bool $forcereload reload even if it was already loaded
875 protected function restore_response($forcereload = false) {
877 if (!$forcereload and !is_null($this->recentresponse)) {
878 // we already have it, nothing to do
879 return;
882 $config = get_config('core_plugin');
884 if (!empty($config->recentresponse) and !empty($config->recentfetch)) {
885 try {
886 $this->validate_response($config->recentresponse);
887 $this->recentfetch = $config->recentfetch;
888 $this->recentresponse = $this->decode_response($config->recentresponse);
889 } catch (available_update_checker_exception $e) {
890 debugging('Invalid info about available updates detected and will be ignored: '.$e->getMessage(), DEBUG_ALL);
891 $this->recentresponse = array();
894 } else {
895 $this->recentresponse = array();
900 * Compares two raw {@link $recentresponse} records and returns the list of changed updates
902 * This method is used to populate potential update info to be sent to site admins.
904 * @param array $old
905 * @param array $new
906 * @throws available_update_checker_exception
907 * @return array parts of $new['updates'] that have changed
909 protected function compare_responses(array $old, array $new) {
911 if (empty($new)) {
912 return array();
915 if (!array_key_exists('updates', $new)) {
916 throw new available_update_checker_exception('err_response_format');
919 if (empty($old)) {
920 return $new['updates'];
923 if (!array_key_exists('updates', $old)) {
924 throw new available_update_checker_exception('err_response_format');
927 $changes = array();
929 foreach ($new['updates'] as $newcomponent => $newcomponentupdates) {
930 if (empty($old['updates'][$newcomponent])) {
931 $changes[$newcomponent] = $newcomponentupdates;
932 continue;
934 foreach ($newcomponentupdates as $newcomponentupdate) {
935 $inold = false;
936 foreach ($old['updates'][$newcomponent] as $oldcomponentupdate) {
937 if ($newcomponentupdate['version'] == $oldcomponentupdate['version']) {
938 $inold = true;
941 if (!$inold) {
942 if (!isset($changes[$newcomponent])) {
943 $changes[$newcomponent] = array();
945 $changes[$newcomponent][] = $newcomponentupdate;
950 return $changes;
954 * Returns the URL to send update requests to
956 * During the development or testing, you can set $CFG->alternativeupdateproviderurl
957 * to a custom URL that will be used. Otherwise the standard URL will be returned.
959 * @return string URL
961 protected function prepare_request_url() {
962 global $CFG;
964 if (!empty($CFG->alternativeupdateproviderurl)) {
965 return $CFG->alternativeupdateproviderurl;
966 } else {
967 return 'http://download.moodle.org/api/1.0/updates.php';
972 * Sets the properties currentversion, currentrelease, currentbranch and currentplugins
974 * @param bool $forcereload
976 protected function load_current_environment($forcereload=false) {
977 global $CFG;
979 if (!is_null($this->currentversion) and !$forcereload) {
980 // nothing to do
981 return;
984 require($CFG->dirroot.'/version.php');
985 $this->currentversion = $version;
986 $this->currentrelease = $release;
987 $this->currentbranch = moodle_major_version(true);
989 $pluginman = plugin_manager::instance();
990 foreach ($pluginman->get_plugins() as $type => $plugins) {
991 foreach ($plugins as $plugin) {
992 if (!$plugin->is_standard()) {
993 $this->currentplugins[$plugin->component] = $plugin->versiondisk;
1000 * Returns the list of HTTP params to be sent to the updates provider URL
1002 * @return array of (string)param => (string)value
1004 protected function prepare_request_params() {
1005 global $CFG;
1007 $this->load_current_environment();
1008 $this->restore_response();
1010 $params = array();
1011 $params['format'] = 'json';
1013 if (isset($this->recentresponse['ticket'])) {
1014 $params['ticket'] = $this->recentresponse['ticket'];
1017 if (isset($this->currentversion)) {
1018 $params['version'] = $this->currentversion;
1019 } else {
1020 throw new coding_exception('Main Moodle version must be already known here');
1023 if (isset($this->currentbranch)) {
1024 $params['branch'] = $this->currentbranch;
1025 } else {
1026 throw new coding_exception('Moodle release must be already known here');
1029 $plugins = array();
1030 foreach ($this->currentplugins as $plugin => $version) {
1031 $plugins[] = $plugin.'@'.$version;
1033 if (!empty($plugins)) {
1034 $params['plugins'] = implode(',', $plugins);
1037 return $params;
1041 * Returns the current timestamp
1043 * @return int the timestamp
1045 protected function cron_current_timestamp() {
1046 return time();
1050 * Output cron debugging info
1052 * @see mtrace()
1053 * @param string $msg output message
1054 * @param string $eol end of line
1056 protected function cron_mtrace($msg, $eol = PHP_EOL) {
1057 mtrace($msg, $eol);
1061 * Decide if the autocheck feature is disabled in the server setting
1063 * @return bool true if autocheck enabled, false if disabled
1065 protected function cron_autocheck_enabled() {
1066 global $CFG;
1068 if (empty($CFG->updateautocheck)) {
1069 return false;
1070 } else {
1071 return true;
1076 * Decide if the recently fetched data are still fresh enough
1078 * @param int $now current timestamp
1079 * @return bool true if no need to re-fetch, false otherwise
1081 protected function cron_has_fresh_fetch($now) {
1082 $recent = $this->get_last_timefetched();
1084 if (empty($recent)) {
1085 return false;
1088 if ($now < $recent) {
1089 $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1090 return true;
1093 if ($now - $recent > 24 * HOURSECS) {
1094 return false;
1097 return true;
1101 * Decide if the fetch is outadated or even missing
1103 * @param int $now current timestamp
1104 * @return bool false if no need to re-fetch, true otherwise
1106 protected function cron_has_outdated_fetch($now) {
1107 $recent = $this->get_last_timefetched();
1109 if (empty($recent)) {
1110 return true;
1113 if ($now < $recent) {
1114 $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1115 return false;
1118 if ($now - $recent > 48 * HOURSECS) {
1119 return true;
1122 return false;
1126 * Returns the cron execution offset for this site
1128 * The main {@link self::cron()} is supposed to run every night in some random time
1129 * between 01:00 and 06:00 AM (local time). The exact moment is defined by so called
1130 * execution offset, that is the amount of time after 01:00 AM. The offset value is
1131 * initially generated randomly and then used consistently at the site. This way, the
1132 * regular checks against the download.moodle.org server are spread in time.
1134 * @return int the offset number of seconds from range 1 sec to 5 hours
1136 protected function cron_execution_offset() {
1137 global $CFG;
1139 if (empty($CFG->updatecronoffset)) {
1140 set_config('updatecronoffset', rand(1, 5 * HOURSECS));
1143 return $CFG->updatecronoffset;
1147 * Fetch available updates info and eventually send notification to site admins
1149 protected function cron_execute() {
1151 try {
1152 $this->restore_response();
1153 $previous = $this->recentresponse;
1154 $this->fetch();
1155 $this->restore_response(true);
1156 $current = $this->recentresponse;
1157 $changes = $this->compare_responses($previous, $current);
1158 $notifications = $this->cron_notifications($changes);
1159 $this->cron_notify($notifications);
1160 $this->cron_mtrace('done');
1161 } catch (available_update_checker_exception $e) {
1162 $this->cron_mtrace('FAILED!');
1167 * Given the list of changes in available updates, pick those to send to site admins
1169 * @param array $changes as returned by {@link self::compare_responses()}
1170 * @return array of available_update_info objects to send to site admins
1172 protected function cron_notifications(array $changes) {
1173 global $CFG;
1175 $notifications = array();
1176 $pluginman = plugin_manager::instance();
1177 $plugins = $pluginman->get_plugins(true);
1179 foreach ($changes as $component => $componentchanges) {
1180 if (empty($componentchanges)) {
1181 continue;
1183 $componentupdates = $this->get_update_info($component,
1184 array('minmaturity' => $CFG->updateminmaturity, 'notifybuilds' => $CFG->updatenotifybuilds));
1185 if (empty($componentupdates)) {
1186 continue;
1188 // notify only about those $componentchanges that are present in $componentupdates
1189 // to respect the preferences
1190 foreach ($componentchanges as $componentchange) {
1191 foreach ($componentupdates as $componentupdate) {
1192 if ($componentupdate->version == $componentchange['version']) {
1193 if ($component == 'core') {
1194 // In case of 'core', we already know that the $componentupdate
1195 // is a real update with higher version ({@see self::get_update_info()}).
1196 // We just perform additional check for the release property as there
1197 // can be two Moodle releases having the same version (e.g. 2.4.0 and 2.5dev shortly
1198 // after the release). We can do that because we have the release info
1199 // always available for the core.
1200 if ((string)$componentupdate->release === (string)$componentchange['release']) {
1201 $notifications[] = $componentupdate;
1203 } else {
1204 // Use the plugin_manager to check if the detected $componentchange
1205 // is a real update with higher version. That is, the $componentchange
1206 // is present in the array of {@link available_update_info} objects
1207 // returned by the plugin's available_updates() method.
1208 list($plugintype, $pluginname) = normalize_component($component);
1209 if (!empty($plugins[$plugintype][$pluginname])) {
1210 $availableupdates = $plugins[$plugintype][$pluginname]->available_updates();
1211 if (!empty($availableupdates)) {
1212 foreach ($availableupdates as $availableupdate) {
1213 if ($availableupdate->version == $componentchange['version']) {
1214 $notifications[] = $componentupdate;
1225 return $notifications;
1229 * Sends the given notifications to site admins via messaging API
1231 * @param array $notifications array of available_update_info objects to send
1233 protected function cron_notify(array $notifications) {
1234 global $CFG;
1236 if (empty($notifications)) {
1237 return;
1240 $admins = get_admins();
1242 if (empty($admins)) {
1243 return;
1246 $this->cron_mtrace('sending notifications ... ', '');
1248 $text = get_string('updatenotifications', 'core_admin') . PHP_EOL;
1249 $html = html_writer::tag('h1', get_string('updatenotifications', 'core_admin')) . PHP_EOL;
1251 $coreupdates = array();
1252 $pluginupdates = array();
1254 foreach ($notifications as $notification) {
1255 if ($notification->component == 'core') {
1256 $coreupdates[] = $notification;
1257 } else {
1258 $pluginupdates[] = $notification;
1262 if (!empty($coreupdates)) {
1263 $text .= PHP_EOL . get_string('updateavailable', 'core_admin') . PHP_EOL;
1264 $html .= html_writer::tag('h2', get_string('updateavailable', 'core_admin')) . PHP_EOL;
1265 $html .= html_writer::start_tag('ul') . PHP_EOL;
1266 foreach ($coreupdates as $coreupdate) {
1267 $html .= html_writer::start_tag('li');
1268 if (isset($coreupdate->release)) {
1269 $text .= get_string('updateavailable_release', 'core_admin', $coreupdate->release);
1270 $html .= html_writer::tag('strong', get_string('updateavailable_release', 'core_admin', $coreupdate->release));
1272 if (isset($coreupdate->version)) {
1273 $text .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
1274 $html .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
1276 if (isset($coreupdate->maturity)) {
1277 $text .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
1278 $html .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
1280 $text .= PHP_EOL;
1281 $html .= html_writer::end_tag('li') . PHP_EOL;
1283 $text .= PHP_EOL;
1284 $html .= html_writer::end_tag('ul') . PHP_EOL;
1286 $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/index.php');
1287 $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
1288 $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/index.php', $CFG->wwwroot.'/'.$CFG->admin.'/index.php'));
1289 $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
1292 if (!empty($pluginupdates)) {
1293 $text .= PHP_EOL . get_string('updateavailableforplugin', 'core_admin') . PHP_EOL;
1294 $html .= html_writer::tag('h2', get_string('updateavailableforplugin', 'core_admin')) . PHP_EOL;
1296 $html .= html_writer::start_tag('ul') . PHP_EOL;
1297 foreach ($pluginupdates as $pluginupdate) {
1298 $html .= html_writer::start_tag('li');
1299 $text .= get_string('pluginname', $pluginupdate->component);
1300 $html .= html_writer::tag('strong', get_string('pluginname', $pluginupdate->component));
1302 $text .= ' ('.$pluginupdate->component.')';
1303 $html .= ' ('.$pluginupdate->component.')';
1305 $text .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
1306 $html .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
1308 $text .= PHP_EOL;
1309 $html .= html_writer::end_tag('li') . PHP_EOL;
1311 $text .= PHP_EOL;
1312 $html .= html_writer::end_tag('ul') . PHP_EOL;
1314 $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php');
1315 $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
1316 $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/plugins.php', $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php'));
1317 $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
1320 $a = array('siteurl' => $CFG->wwwroot);
1321 $text .= get_string('updatenotificationfooter', 'core_admin', $a) . PHP_EOL;
1322 $a = array('siteurl' => html_writer::link($CFG->wwwroot, $CFG->wwwroot));
1323 $html .= html_writer::tag('footer', html_writer::tag('p', get_string('updatenotificationfooter', 'core_admin', $a),
1324 array('style' => 'font-size:smaller; color:#333;')));
1326 $mainadmin = reset($admins);
1328 foreach ($admins as $admin) {
1329 $message = new stdClass();
1330 $message->component = 'moodle';
1331 $message->name = 'availableupdate';
1332 $message->userfrom = $mainadmin;
1333 $message->userto = $admin;
1334 $message->subject = get_string('updatenotificationsubject', 'core_admin', array('siteurl' => $CFG->wwwroot));
1335 $message->fullmessage = $text;
1336 $message->fullmessageformat = FORMAT_PLAIN;
1337 $message->fullmessagehtml = $html;
1338 $message->smallmessage = get_string('updatenotifications', 'core_admin');
1339 $message->notification = 1;
1340 message_send($message);
1345 * Compare two release labels and decide if they are the same
1347 * @param string $remote release info of the available update
1348 * @param null|string $local release info of the local code, defaults to $release defined in version.php
1349 * @return boolean true if the releases declare the same minor+major version
1351 protected function is_same_release($remote, $local=null) {
1353 if (is_null($local)) {
1354 $this->load_current_environment();
1355 $local = $this->currentrelease;
1358 $pattern = '/^([0-9\.\+]+)([^(]*)/';
1360 preg_match($pattern, $remote, $remotematches);
1361 preg_match($pattern, $local, $localmatches);
1363 $remotematches[1] = str_replace('+', '', $remotematches[1]);
1364 $localmatches[1] = str_replace('+', '', $localmatches[1]);
1366 if ($remotematches[1] === $localmatches[1] and rtrim($remotematches[2]) === rtrim($localmatches[2])) {
1367 return true;
1368 } else {
1369 return false;
1376 * Defines the structure of objects returned by {@link available_update_checker::get_update_info()}
1378 class available_update_info {
1380 /** @var string frankenstyle component name */
1381 public $component;
1382 /** @var int the available version of the component */
1383 public $version;
1384 /** @var string|null optional release name */
1385 public $release = null;
1386 /** @var int|null optional maturity info, eg {@link MATURITY_STABLE} */
1387 public $maturity = null;
1388 /** @var string|null optional URL of a page with more info about the update */
1389 public $url = null;
1390 /** @var string|null optional URL of a ZIP package that can be downloaded and installed */
1391 public $download = null;
1394 * Creates new instance of the class
1396 * The $info array must provide at least the 'version' value and optionally all other
1397 * values to populate the object's properties.
1399 * @param string $name the frankenstyle component name
1400 * @param array $info associative array with other properties
1402 public function __construct($name, array $info) {
1403 $this->component = $name;
1404 foreach ($info as $k => $v) {
1405 if (property_exists('available_update_info', $k) and $k != 'component') {
1406 $this->$k = $v;
1414 * Factory class producing required subclasses of {@link plugininfo_base}
1416 class plugininfo_default_factory {
1419 * Makes a new instance of the plugininfo class
1421 * @param string $type the plugin type, eg. 'mod'
1422 * @param string $typerootdir full path to the location of all the plugins of this type
1423 * @param string $name the plugin name, eg. 'workshop'
1424 * @param string $namerootdir full path to the location of the plugin
1425 * @param string $typeclass the name of class that holds the info about the plugin
1426 * @return plugininfo_base the instance of $typeclass
1428 public static function make($type, $typerootdir, $name, $namerootdir, $typeclass) {
1429 $plugin = new $typeclass();
1430 $plugin->type = $type;
1431 $plugin->typerootdir = $typerootdir;
1432 $plugin->name = $name;
1433 $plugin->rootdir = $namerootdir;
1435 $plugin->init_display_name();
1436 $plugin->load_disk_version();
1437 $plugin->load_db_version();
1438 $plugin->load_required_main_version();
1439 $plugin->init_is_standard();
1441 return $plugin;
1447 * Base class providing access to the information about a plugin
1449 * @property-read string component the component name, type_name
1451 abstract class plugininfo_base {
1453 /** @var string the plugintype name, eg. mod, auth or workshopform */
1454 public $type;
1455 /** @var string full path to the location of all the plugins of this type */
1456 public $typerootdir;
1457 /** @var string the plugin name, eg. assignment, ldap */
1458 public $name;
1459 /** @var string the localized plugin name */
1460 public $displayname;
1461 /** @var string the plugin source, one of plugin_manager::PLUGIN_SOURCE_xxx constants */
1462 public $source;
1463 /** @var fullpath to the location of this plugin */
1464 public $rootdir;
1465 /** @var int|string the version of the plugin's source code */
1466 public $versiondisk;
1467 /** @var int|string the version of the installed plugin */
1468 public $versiondb;
1469 /** @var int|float|string required version of Moodle core */
1470 public $versionrequires;
1471 /** @var array other plugins that this one depends on, lazy-loaded by {@link get_other_required_plugins()} */
1472 public $dependencies;
1473 /** @var int number of instances of the plugin - not supported yet */
1474 public $instances;
1475 /** @var int order of the plugin among other plugins of the same type - not supported yet */
1476 public $sortorder;
1477 /** @var array|null array of {@link available_update_info} for this plugin */
1478 public $availableupdates;
1481 * Gathers and returns the information about all plugins of the given type
1483 * @param string $type the name of the plugintype, eg. mod, auth or workshopform
1484 * @param string $typerootdir full path to the location of the plugin dir
1485 * @param string $typeclass the name of the actually called class
1486 * @return array of plugintype classes, indexed by the plugin name
1488 public static function get_plugins($type, $typerootdir, $typeclass) {
1490 // get the information about plugins at the disk
1491 $plugins = get_plugin_list($type);
1492 $ondisk = array();
1493 foreach ($plugins as $pluginname => $pluginrootdir) {
1494 $ondisk[$pluginname] = plugininfo_default_factory::make($type, $typerootdir,
1495 $pluginname, $pluginrootdir, $typeclass);
1497 return $ondisk;
1501 * Sets {@link $displayname} property to a localized name of the plugin
1503 public function init_display_name() {
1504 if (!get_string_manager()->string_exists('pluginname', $this->component)) {
1505 $this->displayname = '[pluginname,' . $this->component . ']';
1506 } else {
1507 $this->displayname = get_string('pluginname', $this->component);
1512 * Magic method getter, redirects to read only values.
1514 * @param string $name
1515 * @return mixed
1517 public function __get($name) {
1518 switch ($name) {
1519 case 'component': return $this->type . '_' . $this->name;
1521 default:
1522 debugging('Invalid plugin property accessed! '.$name);
1523 return null;
1528 * Return the full path name of a file within the plugin.
1530 * No check is made to see if the file exists.
1532 * @param string $relativepath e.g. 'version.php'.
1533 * @return string e.g. $CFG->dirroot . '/mod/quiz/version.php'.
1535 public function full_path($relativepath) {
1536 if (empty($this->rootdir)) {
1537 return '';
1539 return $this->rootdir . '/' . $relativepath;
1543 * Load the data from version.php.
1545 * @return stdClass the object called $plugin defined in version.php
1547 protected function load_version_php() {
1548 $versionfile = $this->full_path('version.php');
1550 $plugin = new stdClass();
1551 if (is_readable($versionfile)) {
1552 include($versionfile);
1554 return $plugin;
1558 * Sets {@link $versiondisk} property to a numerical value representing the
1559 * version of the plugin's source code.
1561 * If the value is null after calling this method, either the plugin
1562 * does not use versioning (typically does not have any database
1563 * data) or is missing from disk.
1565 public function load_disk_version() {
1566 $plugin = $this->load_version_php();
1567 if (isset($plugin->version)) {
1568 $this->versiondisk = $plugin->version;
1573 * Sets {@link $versionrequires} property to a numerical value representing
1574 * the version of Moodle core that this plugin requires.
1576 public function load_required_main_version() {
1577 $plugin = $this->load_version_php();
1578 if (isset($plugin->requires)) {
1579 $this->versionrequires = $plugin->requires;
1584 * Initialise {@link $dependencies} to the list of other plugins (in any)
1585 * that this one requires to be installed.
1587 protected function load_other_required_plugins() {
1588 $plugin = $this->load_version_php();
1589 if (!empty($plugin->dependencies)) {
1590 $this->dependencies = $plugin->dependencies;
1591 } else {
1592 $this->dependencies = array(); // By default, no dependencies.
1597 * Get the list of other plugins that this plugin requires to be installed.
1599 * @return array with keys the frankenstyle plugin name, and values either
1600 * a version string (like '2011101700') or the constant ANY_VERSION.
1602 public function get_other_required_plugins() {
1603 if (is_null($this->dependencies)) {
1604 $this->load_other_required_plugins();
1606 return $this->dependencies;
1610 * Sets {@link $versiondb} property to a numerical value representing the
1611 * currently installed version of the plugin.
1613 * If the value is null after calling this method, either the plugin
1614 * does not use versioning (typically does not have any database
1615 * data) or has not been installed yet.
1617 public function load_db_version() {
1618 if ($ver = self::get_version_from_config_plugins($this->component)) {
1619 $this->versiondb = $ver;
1624 * Sets {@link $source} property to one of plugin_manager::PLUGIN_SOURCE_xxx
1625 * constants.
1627 * If the property's value is null after calling this method, then
1628 * the type of the plugin has not been recognized and you should throw
1629 * an exception.
1631 public function init_is_standard() {
1633 $standard = plugin_manager::standard_plugins_list($this->type);
1635 if ($standard !== false) {
1636 $standard = array_flip($standard);
1637 if (isset($standard[$this->name])) {
1638 $this->source = plugin_manager::PLUGIN_SOURCE_STANDARD;
1639 } else if (!is_null($this->versiondb) and is_null($this->versiondisk)
1640 and plugin_manager::is_deleted_standard_plugin($this->type, $this->name)) {
1641 $this->source = plugin_manager::PLUGIN_SOURCE_STANDARD; // to be deleted
1642 } else {
1643 $this->source = plugin_manager::PLUGIN_SOURCE_EXTENSION;
1649 * Returns true if the plugin is shipped with the official distribution
1650 * of the current Moodle version, false otherwise.
1652 * @return bool
1654 public function is_standard() {
1655 return $this->source === plugin_manager::PLUGIN_SOURCE_STANDARD;
1659 * Returns true if the the given Moodle version is enough to run this plugin
1661 * @param string|int|double $moodleversion
1662 * @return bool
1664 public function is_core_dependency_satisfied($moodleversion) {
1666 if (empty($this->versionrequires)) {
1667 return true;
1669 } else {
1670 return (double)$this->versionrequires <= (double)$moodleversion;
1675 * Returns the status of the plugin
1677 * @return string one of plugin_manager::PLUGIN_STATUS_xxx constants
1679 public function get_status() {
1681 if (is_null($this->versiondb) and is_null($this->versiondisk)) {
1682 return plugin_manager::PLUGIN_STATUS_NODB;
1684 } else if (is_null($this->versiondb) and !is_null($this->versiondisk)) {
1685 return plugin_manager::PLUGIN_STATUS_NEW;
1687 } else if (!is_null($this->versiondb) and is_null($this->versiondisk)) {
1688 if (plugin_manager::is_deleted_standard_plugin($this->type, $this->name)) {
1689 return plugin_manager::PLUGIN_STATUS_DELETE;
1690 } else {
1691 return plugin_manager::PLUGIN_STATUS_MISSING;
1694 } else if ((string)$this->versiondb === (string)$this->versiondisk) {
1695 return plugin_manager::PLUGIN_STATUS_UPTODATE;
1697 } else if ($this->versiondb < $this->versiondisk) {
1698 return plugin_manager::PLUGIN_STATUS_UPGRADE;
1700 } else if ($this->versiondb > $this->versiondisk) {
1701 return plugin_manager::PLUGIN_STATUS_DOWNGRADE;
1703 } else {
1704 // $version = pi(); and similar funny jokes - hopefully Donald E. Knuth will never contribute to Moodle ;-)
1705 throw new coding_exception('Unable to determine plugin state, check the plugin versions');
1710 * Returns the information about plugin availability
1712 * True means that the plugin is enabled. False means that the plugin is
1713 * disabled. Null means that the information is not available, or the
1714 * plugin does not support configurable availability or the availability
1715 * can not be changed.
1717 * @return null|bool
1719 public function is_enabled() {
1720 return null;
1724 * Populates the property {@link $availableupdates} with the information provided by
1725 * available update checker
1727 * @param available_update_checker $provider the class providing the available update info
1729 public function check_available_updates(available_update_checker $provider) {
1730 global $CFG;
1732 if (isset($CFG->updateminmaturity)) {
1733 $minmaturity = $CFG->updateminmaturity;
1734 } else {
1735 // this can happen during the very first upgrade to 2.3
1736 $minmaturity = MATURITY_STABLE;
1739 $this->availableupdates = $provider->get_update_info($this->component,
1740 array('minmaturity' => $minmaturity));
1744 * If there are updates for this plugin available, returns them.
1746 * Returns array of {@link available_update_info} objects, if some update
1747 * is available. Returns null if there is no update available or if the update
1748 * availability is unknown.
1750 * @return array|null
1752 public function available_updates() {
1754 if (empty($this->availableupdates) or !is_array($this->availableupdates)) {
1755 return null;
1758 $updates = array();
1760 foreach ($this->availableupdates as $availableupdate) {
1761 if ($availableupdate->version > $this->versiondisk) {
1762 $updates[] = $availableupdate;
1766 if (empty($updates)) {
1767 return null;
1770 return $updates;
1774 * Returns the URL of the plugin settings screen
1776 * Null value means that the plugin either does not have the settings screen
1777 * or its location is not available via this library.
1779 * @return null|moodle_url
1781 public function get_settings_url() {
1782 return null;
1786 * Returns the URL of the screen where this plugin can be uninstalled
1788 * Visiting that URL must be safe, that is a manual confirmation is needed
1789 * for actual uninstallation of the plugin. Null value means that the
1790 * plugin either does not support uninstallation, or does not require any
1791 * database cleanup or the location of the screen is not available via this
1792 * library.
1794 * @return null|moodle_url
1796 public function get_uninstall_url() {
1797 return null;
1801 * Returns relative directory of the plugin with heading '/'
1803 * @return string
1805 public function get_dir() {
1806 global $CFG;
1808 return substr($this->rootdir, strlen($CFG->dirroot));
1812 * Provides access to plugin versions from {config_plugins}
1814 * @param string $plugin plugin name
1815 * @param double $disablecache optional, defaults to false
1816 * @return int|false the stored value or false if not found
1818 protected function get_version_from_config_plugins($plugin, $disablecache=false) {
1819 global $DB;
1820 static $pluginversions = null;
1822 if (is_null($pluginversions) or $disablecache) {
1823 try {
1824 $pluginversions = $DB->get_records_menu('config_plugins', array('name' => 'version'), 'plugin', 'plugin,value');
1825 } catch (dml_exception $e) {
1826 // before install
1827 $pluginversions = array();
1831 if (!array_key_exists($plugin, $pluginversions)) {
1832 return false;
1835 return $pluginversions[$plugin];
1841 * General class for all plugin types that do not have their own class
1843 class plugininfo_general extends plugininfo_base {
1848 * Class for page side blocks
1850 class plugininfo_block extends plugininfo_base {
1852 public static function get_plugins($type, $typerootdir, $typeclass) {
1854 // get the information about blocks at the disk
1855 $blocks = parent::get_plugins($type, $typerootdir, $typeclass);
1857 // add blocks missing from disk
1858 $blocksinfo = self::get_blocks_info();
1859 foreach ($blocksinfo as $blockname => $blockinfo) {
1860 if (isset($blocks[$blockname])) {
1861 continue;
1863 $plugin = new $typeclass();
1864 $plugin->type = $type;
1865 $plugin->typerootdir = $typerootdir;
1866 $plugin->name = $blockname;
1867 $plugin->rootdir = null;
1868 $plugin->displayname = $blockname;
1869 $plugin->versiondb = $blockinfo->version;
1870 $plugin->init_is_standard();
1872 $blocks[$blockname] = $plugin;
1875 return $blocks;
1878 public function init_display_name() {
1880 if (get_string_manager()->string_exists('pluginname', 'block_' . $this->name)) {
1881 $this->displayname = get_string('pluginname', 'block_' . $this->name);
1883 } else if (($block = block_instance($this->name)) !== false) {
1884 $this->displayname = $block->get_title();
1886 } else {
1887 parent::init_display_name();
1891 public function load_db_version() {
1892 global $DB;
1894 $blocksinfo = self::get_blocks_info();
1895 if (isset($blocksinfo[$this->name]->version)) {
1896 $this->versiondb = $blocksinfo[$this->name]->version;
1900 public function is_enabled() {
1902 $blocksinfo = self::get_blocks_info();
1903 if (isset($blocksinfo[$this->name]->visible)) {
1904 if ($blocksinfo[$this->name]->visible) {
1905 return true;
1906 } else {
1907 return false;
1909 } else {
1910 return parent::is_enabled();
1914 public function get_settings_url() {
1916 if (($block = block_instance($this->name)) === false) {
1917 return parent::get_settings_url();
1919 } else if ($block->has_config()) {
1920 if (file_exists($this->full_path('settings.php'))) {
1921 return new moodle_url('/admin/settings.php', array('section' => 'blocksetting' . $this->name));
1922 } else {
1923 $blocksinfo = self::get_blocks_info();
1924 return new moodle_url('/admin/block.php', array('block' => $blocksinfo[$this->name]->id));
1927 } else {
1928 return parent::get_settings_url();
1932 public function get_uninstall_url() {
1934 $blocksinfo = self::get_blocks_info();
1935 return new moodle_url('/admin/blocks.php', array('delete' => $blocksinfo[$this->name]->id, 'sesskey' => sesskey()));
1939 * Provides access to the records in {block} table
1941 * @param bool $disablecache do not use internal static cache
1942 * @return array array of stdClasses
1944 protected static function get_blocks_info($disablecache=false) {
1945 global $DB;
1946 static $blocksinfocache = null;
1948 if (is_null($blocksinfocache) or $disablecache) {
1949 try {
1950 $blocksinfocache = $DB->get_records('block', null, 'name', 'name,id,version,visible');
1951 } catch (dml_exception $e) {
1952 // before install
1953 $blocksinfocache = array();
1957 return $blocksinfocache;
1963 * Class for text filters
1965 class plugininfo_filter extends plugininfo_base {
1967 public static function get_plugins($type, $typerootdir, $typeclass) {
1968 global $CFG, $DB;
1970 $filters = array();
1972 // get the list of filters from both /filter and /mod location
1973 $installed = filter_get_all_installed();
1975 foreach ($installed as $filterlegacyname => $displayname) {
1976 $plugin = new $typeclass();
1977 $plugin->type = $type;
1978 $plugin->typerootdir = $typerootdir;
1979 $plugin->name = self::normalize_legacy_name($filterlegacyname);
1980 $plugin->rootdir = $CFG->dirroot . '/' . $filterlegacyname;
1981 $plugin->displayname = $displayname;
1983 $plugin->load_disk_version();
1984 $plugin->load_db_version();
1985 $plugin->load_required_main_version();
1986 $plugin->init_is_standard();
1988 $filters[$plugin->name] = $plugin;
1991 $globalstates = self::get_global_states();
1993 if ($DB->get_manager()->table_exists('filter_active')) {
1994 // if we're upgrading from 1.9, the table does not exist yet
1995 // if it does, make sure that all installed filters are registered
1996 $needsreload = false;
1997 foreach (array_keys($installed) as $filterlegacyname) {
1998 if (!isset($globalstates[self::normalize_legacy_name($filterlegacyname)])) {
1999 filter_set_global_state($filterlegacyname, TEXTFILTER_DISABLED);
2000 $needsreload = true;
2003 if ($needsreload) {
2004 $globalstates = self::get_global_states(true);
2008 // make sure that all registered filters are installed, just in case
2009 foreach ($globalstates as $name => $info) {
2010 if (!isset($filters[$name])) {
2011 // oops, there is a record in filter_active but the filter is not installed
2012 $plugin = new $typeclass();
2013 $plugin->type = $type;
2014 $plugin->typerootdir = $typerootdir;
2015 $plugin->name = $name;
2016 $plugin->rootdir = $CFG->dirroot . '/' . $info->legacyname;
2017 $plugin->displayname = $info->legacyname;
2019 $plugin->load_db_version();
2021 if (is_null($plugin->versiondb)) {
2022 // this is a hack to stimulate 'Missing from disk' error
2023 // because $plugin->versiondisk will be null !== false
2024 $plugin->versiondb = false;
2027 $filters[$plugin->name] = $plugin;
2031 return $filters;
2034 public function init_display_name() {
2035 // do nothing, the name is set in self::get_plugins()
2039 * @see load_version_php()
2041 protected function load_version_php() {
2042 if (strpos($this->name, 'mod_') === 0) {
2043 // filters bundled with modules do not have a version.php and so
2044 // do not provide their own versioning information.
2045 return new stdClass();
2047 return parent::load_version_php();
2050 public function is_enabled() {
2052 $globalstates = self::get_global_states();
2054 foreach ($globalstates as $filterlegacyname => $info) {
2055 $name = self::normalize_legacy_name($filterlegacyname);
2056 if ($name === $this->name) {
2057 if ($info->active == TEXTFILTER_DISABLED) {
2058 return false;
2059 } else {
2060 // it may be 'On' or 'Off, but available'
2061 return null;
2066 return null;
2069 public function get_settings_url() {
2071 $globalstates = self::get_global_states();
2072 $legacyname = $globalstates[$this->name]->legacyname;
2073 if (filter_has_global_settings($legacyname)) {
2074 return new moodle_url('/admin/settings.php', array('section' => 'filtersetting' . str_replace('/', '', $legacyname)));
2075 } else {
2076 return null;
2080 public function get_uninstall_url() {
2082 if (strpos($this->name, 'mod_') === 0) {
2083 return null;
2084 } else {
2085 $globalstates = self::get_global_states();
2086 $legacyname = $globalstates[$this->name]->legacyname;
2087 return new moodle_url('/admin/filters.php', array('sesskey' => sesskey(), 'filterpath' => $legacyname, 'action' => 'delete'));
2092 * Convert legacy filter names like 'filter/foo' or 'mod/bar' into frankenstyle
2094 * @param string $legacyfiltername legacy filter name
2095 * @return string frankenstyle-like name
2097 protected static function normalize_legacy_name($legacyfiltername) {
2099 $name = str_replace('/', '_', $legacyfiltername);
2100 if (strpos($name, 'filter_') === 0) {
2101 $name = substr($name, 7);
2102 if (empty($name)) {
2103 throw new coding_exception('Unable to determine filter name: ' . $legacyfiltername);
2107 return $name;
2111 * Provides access to the results of {@link filter_get_global_states()}
2112 * but indexed by the normalized filter name
2114 * The legacy filter name is available as ->legacyname property.
2116 * @param bool $disablecache
2117 * @return array
2119 protected static function get_global_states($disablecache=false) {
2120 global $DB;
2121 static $globalstatescache = null;
2123 if ($disablecache or is_null($globalstatescache)) {
2125 if (!$DB->get_manager()->table_exists('filter_active')) {
2126 // we're upgrading from 1.9 and the table used by {@link filter_get_global_states()}
2127 // does not exist yet
2128 $globalstatescache = array();
2130 } else {
2131 foreach (filter_get_global_states() as $legacyname => $info) {
2132 $name = self::normalize_legacy_name($legacyname);
2133 $filterinfo = new stdClass();
2134 $filterinfo->legacyname = $legacyname;
2135 $filterinfo->active = $info->active;
2136 $filterinfo->sortorder = $info->sortorder;
2137 $globalstatescache[$name] = $filterinfo;
2142 return $globalstatescache;
2148 * Class for activity modules
2150 class plugininfo_mod extends plugininfo_base {
2152 public static function get_plugins($type, $typerootdir, $typeclass) {
2154 // get the information about plugins at the disk
2155 $modules = parent::get_plugins($type, $typerootdir, $typeclass);
2157 // add modules missing from disk
2158 $modulesinfo = self::get_modules_info();
2159 foreach ($modulesinfo as $modulename => $moduleinfo) {
2160 if (isset($modules[$modulename])) {
2161 continue;
2163 $plugin = new $typeclass();
2164 $plugin->type = $type;
2165 $plugin->typerootdir = $typerootdir;
2166 $plugin->name = $modulename;
2167 $plugin->rootdir = null;
2168 $plugin->displayname = $modulename;
2169 $plugin->versiondb = $moduleinfo->version;
2170 $plugin->init_is_standard();
2172 $modules[$modulename] = $plugin;
2175 return $modules;
2178 public function init_display_name() {
2179 if (get_string_manager()->string_exists('pluginname', $this->component)) {
2180 $this->displayname = get_string('pluginname', $this->component);
2181 } else {
2182 $this->displayname = get_string('modulename', $this->component);
2187 * Load the data from version.php.
2188 * @return object the data object defined in version.php.
2190 protected function load_version_php() {
2191 $versionfile = $this->full_path('version.php');
2193 $module = new stdClass();
2194 if (is_readable($versionfile)) {
2195 include($versionfile);
2197 return $module;
2200 public function load_db_version() {
2201 global $DB;
2203 $modulesinfo = self::get_modules_info();
2204 if (isset($modulesinfo[$this->name]->version)) {
2205 $this->versiondb = $modulesinfo[$this->name]->version;
2209 public function is_enabled() {
2211 $modulesinfo = self::get_modules_info();
2212 if (isset($modulesinfo[$this->name]->visible)) {
2213 if ($modulesinfo[$this->name]->visible) {
2214 return true;
2215 } else {
2216 return false;
2218 } else {
2219 return parent::is_enabled();
2223 public function get_settings_url() {
2225 if (file_exists($this->full_path('settings.php')) or file_exists($this->full_path('settingstree.php'))) {
2226 return new moodle_url('/admin/settings.php', array('section' => 'modsetting' . $this->name));
2227 } else {
2228 return parent::get_settings_url();
2232 public function get_uninstall_url() {
2234 if ($this->name !== 'forum') {
2235 return new moodle_url('/admin/modules.php', array('delete' => $this->name, 'sesskey' => sesskey()));
2236 } else {
2237 return null;
2242 * Provides access to the records in {modules} table
2244 * @param bool $disablecache do not use internal static cache
2245 * @return array array of stdClasses
2247 protected static function get_modules_info($disablecache=false) {
2248 global $DB;
2249 static $modulesinfocache = null;
2251 if (is_null($modulesinfocache) or $disablecache) {
2252 try {
2253 $modulesinfocache = $DB->get_records('modules', null, 'name', 'name,id,version,visible');
2254 } catch (dml_exception $e) {
2255 // before install
2256 $modulesinfocache = array();
2260 return $modulesinfocache;
2266 * Class for question behaviours.
2268 class plugininfo_qbehaviour extends plugininfo_base {
2270 public function get_uninstall_url() {
2271 return new moodle_url('/admin/qbehaviours.php',
2272 array('delete' => $this->name, 'sesskey' => sesskey()));
2278 * Class for question types
2280 class plugininfo_qtype extends plugininfo_base {
2282 public function get_uninstall_url() {
2283 return new moodle_url('/admin/qtypes.php',
2284 array('delete' => $this->name, 'sesskey' => sesskey()));
2290 * Class for authentication plugins
2292 class plugininfo_auth extends plugininfo_base {
2294 public function is_enabled() {
2295 global $CFG;
2296 /** @var null|array list of enabled authentication plugins */
2297 static $enabled = null;
2299 if (in_array($this->name, array('nologin', 'manual'))) {
2300 // these two are always enabled and can't be disabled
2301 return null;
2304 if (is_null($enabled)) {
2305 $enabled = array_flip(explode(',', $CFG->auth));
2308 return isset($enabled[$this->name]);
2311 public function get_settings_url() {
2312 if (file_exists($this->full_path('settings.php'))) {
2313 return new moodle_url('/admin/settings.php', array('section' => 'authsetting' . $this->name));
2314 } else {
2315 return new moodle_url('/admin/auth_config.php', array('auth' => $this->name));
2322 * Class for enrolment plugins
2324 class plugininfo_enrol extends plugininfo_base {
2326 public function is_enabled() {
2327 global $CFG;
2328 /** @var null|array list of enabled enrolment plugins */
2329 static $enabled = null;
2331 // We do not actually need whole enrolment classes here so we do not call
2332 // {@link enrol_get_plugins()}. Note that this may produce slightly different
2333 // results, for example if the enrolment plugin does not contain lib.php
2334 // but it is listed in $CFG->enrol_plugins_enabled
2336 if (is_null($enabled)) {
2337 $enabled = array_flip(explode(',', $CFG->enrol_plugins_enabled));
2340 return isset($enabled[$this->name]);
2343 public function get_settings_url() {
2345 if ($this->is_enabled() or file_exists($this->full_path('settings.php'))) {
2346 return new moodle_url('/admin/settings.php', array('section' => 'enrolsettings' . $this->name));
2347 } else {
2348 return parent::get_settings_url();
2352 public function get_uninstall_url() {
2353 return new moodle_url('/admin/enrol.php', array('action' => 'uninstall', 'enrol' => $this->name, 'sesskey' => sesskey()));
2359 * Class for messaging processors
2361 class plugininfo_message extends plugininfo_base {
2363 public function get_settings_url() {
2364 $processors = get_message_processors();
2365 if (isset($processors[$this->name])) {
2366 $processor = $processors[$this->name];
2367 if ($processor->available && $processor->hassettings) {
2368 return new moodle_url('settings.php', array('section' => 'messagesetting'.$processor->name));
2371 return parent::get_settings_url();
2375 * @see plugintype_interface::is_enabled()
2377 public function is_enabled() {
2378 $processors = get_message_processors();
2379 if (isset($processors[$this->name])) {
2380 return $processors[$this->name]->configured && $processors[$this->name]->enabled;
2381 } else {
2382 return parent::is_enabled();
2387 * @see plugintype_interface::get_uninstall_url()
2389 public function get_uninstall_url() {
2390 $processors = get_message_processors();
2391 if (isset($processors[$this->name])) {
2392 return new moodle_url('message.php', array('uninstall' => $processors[$this->name]->id, 'sesskey' => sesskey()));
2393 } else {
2394 return parent::get_uninstall_url();
2401 * Class for repositories
2403 class plugininfo_repository extends plugininfo_base {
2405 public function is_enabled() {
2407 $enabled = self::get_enabled_repositories();
2409 return isset($enabled[$this->name]);
2412 public function get_settings_url() {
2414 if ($this->is_enabled()) {
2415 return new moodle_url('/admin/repository.php', array('sesskey' => sesskey(), 'action' => 'edit', 'repos' => $this->name));
2416 } else {
2417 return parent::get_settings_url();
2422 * Provides access to the records in {repository} table
2424 * @param bool $disablecache do not use internal static cache
2425 * @return array array of stdClasses
2427 protected static function get_enabled_repositories($disablecache=false) {
2428 global $DB;
2429 static $repositories = null;
2431 if (is_null($repositories) or $disablecache) {
2432 $repositories = $DB->get_records('repository', null, 'type', 'type,visible,sortorder');
2435 return $repositories;
2441 * Class for portfolios
2443 class plugininfo_portfolio extends plugininfo_base {
2445 public function is_enabled() {
2447 $enabled = self::get_enabled_portfolios();
2449 return isset($enabled[$this->name]);
2453 * Provides access to the records in {portfolio_instance} table
2455 * @param bool $disablecache do not use internal static cache
2456 * @return array array of stdClasses
2458 protected static function get_enabled_portfolios($disablecache=false) {
2459 global $DB;
2460 static $portfolios = null;
2462 if (is_null($portfolios) or $disablecache) {
2463 $portfolios = array();
2464 $instances = $DB->get_recordset('portfolio_instance', null, 'plugin');
2465 foreach ($instances as $instance) {
2466 if (isset($portfolios[$instance->plugin])) {
2467 if ($instance->visible) {
2468 $portfolios[$instance->plugin]->visible = $instance->visible;
2470 } else {
2471 $portfolios[$instance->plugin] = $instance;
2476 return $portfolios;
2482 * Class for themes
2484 class plugininfo_theme extends plugininfo_base {
2486 public function is_enabled() {
2487 global $CFG;
2489 if ((!empty($CFG->theme) and $CFG->theme === $this->name) or
2490 (!empty($CFG->themelegacy) and $CFG->themelegacy === $this->name)) {
2491 return true;
2492 } else {
2493 return parent::is_enabled();
2500 * Class representing an MNet service
2502 class plugininfo_mnetservice extends plugininfo_base {
2504 public function is_enabled() {
2505 global $CFG;
2507 if (empty($CFG->mnet_dispatcher_mode) || $CFG->mnet_dispatcher_mode !== 'strict') {
2508 return false;
2509 } else {
2510 return parent::is_enabled();
2517 * Class for admin tool plugins
2519 class plugininfo_tool extends plugininfo_base {
2521 public function get_uninstall_url() {
2522 return new moodle_url('/admin/tools.php', array('delete' => $this->name, 'sesskey' => sesskey()));
2528 * Class for admin tool plugins
2530 class plugininfo_report extends plugininfo_base {
2532 public function get_uninstall_url() {
2533 return new moodle_url('/admin/reports.php', array('delete' => $this->name, 'sesskey' => sesskey()));
2539 * Class for local plugins
2541 class plugininfo_local extends plugininfo_base {
2543 public function get_uninstall_url() {
2544 return new moodle_url('/admin/localplugins.php', array('delete' => $this->name, 'sesskey' => sesskey()));