MDL-40531 mod_lti: Various 1.1/1.1.1 fixes.
[moodle.git] / lib / pluginlib.php
blobf2a8e780c4d705f694a09ad19c0d6aac71ac54f5
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 /**
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;
65 /**
66 * Direct initiation not allowed, use the factory method {@link self::instance()}
68 protected function __construct() {
71 /**
72 * Sorry, this is singleton
74 protected function __clone() {
77 /**
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;
89 /**
90 * Reset any caches
91 * @param bool $phpunitreset
93 public static function reset_caches($phpunitreset = false) {
94 if ($phpunitreset) {
95 self::$singletoninstance = null;
99 /**
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 list of known plugins of the given type
113 * This method returns the subset of the tree returned by {@link self::get_plugins()}.
114 * If the given type is not known, empty array is returned.
116 * @param string $type plugin type, e.g. 'mod' or 'workshopallocation'
117 * @param bool $disablecache force reload, cache can be used otherwise
118 * @return array (string)plugin name (e.g. 'workshop') => corresponding subclass of {@link plugininfo_base}
120 public function get_plugins_of_type($type, $disablecache=false) {
122 $plugins = $this->get_plugins($disablecache);
124 if (!isset($plugins[$type])) {
125 return array();
128 return $plugins[$type];
132 * Returns a tree of known plugins and information about them
134 * @param bool $disablecache force reload, cache can be used otherwise
135 * @return array 2D array. The first keys are plugin type names (e.g. qtype);
136 * the second keys are the plugin local name (e.g. multichoice); and
137 * the values are the corresponding objects extending {@link plugininfo_base}
139 public function get_plugins($disablecache=false) {
140 global $CFG;
142 if ($disablecache or is_null($this->pluginsinfo)) {
143 // Hack: include mod and editor subplugin management classes first,
144 // the adminlib.php is supposed to contain extra admin settings too.
145 require_once($CFG->libdir.'/adminlib.php');
146 foreach(array('mod', 'editor') as $type) {
147 foreach (get_plugin_list($type) as $dir) {
148 if (file_exists("$dir/adminlib.php")) {
149 include_once("$dir/adminlib.php");
153 $this->pluginsinfo = array();
154 $plugintypes = $this->get_plugin_types();
155 foreach ($plugintypes as $plugintype => $plugintyperootdir) {
156 if (in_array($plugintype, array('base', 'general'))) {
157 throw new coding_exception('Illegal usage of reserved word for plugin type');
159 if (class_exists('plugininfo_' . $plugintype)) {
160 $plugintypeclass = 'plugininfo_' . $plugintype;
161 } else {
162 $plugintypeclass = 'plugininfo_general';
164 if (!in_array('plugininfo_base', class_parents($plugintypeclass))) {
165 throw new coding_exception('Class ' . $plugintypeclass . ' must extend plugininfo_base');
167 $plugins = call_user_func(array($plugintypeclass, 'get_plugins'), $plugintype, $plugintyperootdir, $plugintypeclass);
168 $this->pluginsinfo[$plugintype] = $plugins;
171 if (empty($CFG->disableupdatenotifications) and !during_initial_install()) {
172 // append the information about available updates provided by {@link available_update_checker()}
173 $provider = available_update_checker::instance();
174 foreach ($this->pluginsinfo as $plugintype => $plugins) {
175 foreach ($plugins as $plugininfoholder) {
176 $plugininfoholder->check_available_updates($provider);
182 return $this->pluginsinfo;
186 * Returns list of all known subplugins of the given plugin
188 * For plugins that do not provide subplugins (i.e. there is no support for it),
189 * empty array is returned.
191 * @param string $component full component name, e.g. 'mod_workshop'
192 * @param bool $disablecache force reload, cache can be used otherwise
193 * @return array (string) component name (e.g. 'workshopallocation_random') => subclass of {@link plugininfo_base}
195 public function get_subplugins_of_plugin($component, $disablecache=false) {
197 $pluginfo = $this->get_plugin_info($component, $disablecache);
199 if (is_null($pluginfo)) {
200 return array();
203 $subplugins = $this->get_subplugins($disablecache);
205 if (!isset($subplugins[$pluginfo->component])) {
206 return array();
209 $list = array();
211 foreach ($subplugins[$pluginfo->component] as $subdata) {
212 foreach ($this->get_plugins_of_type($subdata->type) as $subpluginfo) {
213 $list[$subpluginfo->component] = $subpluginfo;
217 return $list;
221 * Returns list of plugins that define their subplugins and the information
222 * about them from the db/subplugins.php file.
224 * At the moment, only activity modules and editors can define subplugins.
226 * @param bool $disablecache force reload, cache can be used otherwise
227 * @return array with keys like 'mod_quiz', and values the data from the
228 * corresponding db/subplugins.php file.
230 public function get_subplugins($disablecache=false) {
232 if ($disablecache or is_null($this->subpluginsinfo)) {
233 $this->subpluginsinfo = array();
234 foreach (array('mod', 'editor') as $type) {
235 $owners = get_plugin_list($type);
236 foreach ($owners as $component => $ownerdir) {
237 $componentsubplugins = array();
238 if (file_exists($ownerdir . '/db/subplugins.php')) {
239 $subplugins = array();
240 include($ownerdir . '/db/subplugins.php');
241 foreach ($subplugins as $subplugintype => $subplugintyperootdir) {
242 $subplugin = new stdClass();
243 $subplugin->type = $subplugintype;
244 $subplugin->typerootdir = $subplugintyperootdir;
245 $componentsubplugins[$subplugintype] = $subplugin;
247 $this->subpluginsinfo[$type . '_' . $component] = $componentsubplugins;
253 return $this->subpluginsinfo;
257 * Returns the name of the plugin that defines the given subplugin type
259 * If the given subplugin type is not actually a subplugin, returns false.
261 * @param string $subplugintype the name of subplugin type, eg. workshopform or quiz
262 * @return false|string the name of the parent plugin, eg. mod_workshop
264 public function get_parent_of_subplugin($subplugintype) {
266 $parent = false;
267 foreach ($this->get_subplugins() as $pluginname => $subplugintypes) {
268 if (isset($subplugintypes[$subplugintype])) {
269 $parent = $pluginname;
270 break;
274 return $parent;
278 * Returns a localized name of a given plugin
280 * @param string $component name of the plugin, eg mod_workshop or auth_ldap
281 * @return string
283 public function plugin_name($component) {
285 $pluginfo = $this->get_plugin_info($component);
287 if (is_null($pluginfo)) {
288 throw new moodle_exception('err_unknown_plugin', 'core_plugin', '', array('plugin' => $component));
291 return $pluginfo->displayname;
295 * Returns a localized name of a plugin typed in singular form
297 * Most plugin types define their names in core_plugin lang file. In case of subplugins,
298 * we try to ask the parent plugin for the name. In the worst case, we will return
299 * the value of the passed $type parameter.
301 * @param string $type the type of the plugin, e.g. mod or workshopform
302 * @return string
304 public function plugintype_name($type) {
306 if (get_string_manager()->string_exists('type_' . $type, 'core_plugin')) {
307 // for most plugin types, their names are defined in core_plugin lang file
308 return get_string('type_' . $type, 'core_plugin');
310 } else if ($parent = $this->get_parent_of_subplugin($type)) {
311 // if this is a subplugin, try to ask the parent plugin for the name
312 if (get_string_manager()->string_exists('subplugintype_' . $type, $parent)) {
313 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type, $parent);
314 } else {
315 return $this->plugin_name($parent) . ' / ' . $type;
318 } else {
319 return $type;
324 * Returns a localized name of a plugin type in plural form
326 * Most plugin types define their names in core_plugin lang file. In case of subplugins,
327 * we try to ask the parent plugin for the name. In the worst case, we will return
328 * the value of the passed $type parameter.
330 * @param string $type the type of the plugin, e.g. mod or workshopform
331 * @return string
333 public function plugintype_name_plural($type) {
335 if (get_string_manager()->string_exists('type_' . $type . '_plural', 'core_plugin')) {
336 // for most plugin types, their names are defined in core_plugin lang file
337 return get_string('type_' . $type . '_plural', 'core_plugin');
339 } else if ($parent = $this->get_parent_of_subplugin($type)) {
340 // if this is a subplugin, try to ask the parent plugin for the name
341 if (get_string_manager()->string_exists('subplugintype_' . $type . '_plural', $parent)) {
342 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type . '_plural', $parent);
343 } else {
344 return $this->plugin_name($parent) . ' / ' . $type;
347 } else {
348 return $type;
353 * Returns information about the known plugin, or null
355 * @param string $component frankenstyle component name.
356 * @param bool $disablecache force reload, cache can be used otherwise
357 * @return plugininfo_base|null the corresponding plugin information.
359 public function get_plugin_info($component, $disablecache=false) {
360 list($type, $name) = $this->normalize_component($component);
361 $plugins = $this->get_plugins($disablecache);
362 if (isset($plugins[$type][$name])) {
363 return $plugins[$type][$name];
364 } else {
365 return null;
370 * Check to see if the current version of the plugin seems to be a checkout of an external repository.
372 * @see available_update_deployer::plugin_external_source()
373 * @param string $component frankenstyle component name
374 * @return false|string
376 public function plugin_external_source($component) {
378 $plugininfo = $this->get_plugin_info($component);
380 if (is_null($plugininfo)) {
381 return false;
384 $pluginroot = $plugininfo->rootdir;
386 if (is_dir($pluginroot.'/.git')) {
387 return 'git';
390 if (is_dir($pluginroot.'/CVS')) {
391 return 'cvs';
394 if (is_dir($pluginroot.'/.svn')) {
395 return 'svn';
398 return false;
402 * Get a list of any other plugins that require this one.
403 * @param string $component frankenstyle component name.
404 * @return array of frankensyle component names that require this one.
406 public function other_plugins_that_require($component) {
407 $others = array();
408 foreach ($this->get_plugins() as $type => $plugins) {
409 foreach ($plugins as $plugin) {
410 $required = $plugin->get_other_required_plugins();
411 if (isset($required[$component])) {
412 $others[] = $plugin->component;
416 return $others;
420 * Check a dependencies list against the list of installed plugins.
421 * @param array $dependencies compenent name to required version or ANY_VERSION.
422 * @return bool true if all the dependencies are satisfied.
424 public function are_dependencies_satisfied($dependencies) {
425 foreach ($dependencies as $component => $requiredversion) {
426 $otherplugin = $this->get_plugin_info($component);
427 if (is_null($otherplugin)) {
428 return false;
431 if ($requiredversion != ANY_VERSION and $otherplugin->versiondisk < $requiredversion) {
432 return false;
436 return true;
440 * Checks all dependencies for all installed plugins
442 * This is used by install and upgrade. The array passed by reference as the second
443 * argument is populated with the list of plugins that have failed dependencies (note that
444 * a single plugin can appear multiple times in the $failedplugins).
446 * @param int $moodleversion the version from version.php.
447 * @param array $failedplugins to return the list of plugins with non-satisfied dependencies
448 * @return bool true if all the dependencies are satisfied for all plugins.
450 public function all_plugins_ok($moodleversion, &$failedplugins = array()) {
452 $return = true;
453 foreach ($this->get_plugins() as $type => $plugins) {
454 foreach ($plugins as $plugin) {
456 if (!$plugin->is_core_dependency_satisfied($moodleversion)) {
457 $return = false;
458 $failedplugins[] = $plugin->component;
461 if (!$this->are_dependencies_satisfied($plugin->get_other_required_plugins())) {
462 $return = false;
463 $failedplugins[] = $plugin->component;
468 return $return;
472 * Is it possible to uninstall the given plugin?
474 * False is returned if the plugininfo subclass declares the uninstall should
475 * not be allowed via {@link plugininfo_base::is_uninstall_allowed()} or if the
476 * core vetoes it (e.g. becase the plugin or some of its subplugins is required
477 * by some other installed plugin).
479 * @param string $component full frankenstyle name, e.g. mod_foobar
480 * @return bool
482 public function can_uninstall_plugin($component) {
484 $pluginfo = $this->get_plugin_info($component);
486 if (is_null($pluginfo)) {
487 return false;
490 if (!$this->common_uninstall_check($pluginfo)) {
491 return false;
494 // If it has subplugins, check they can be uninstalled too.
495 $subplugins = $this->get_subplugins_of_plugin($pluginfo->component);
496 foreach ($subplugins as $subpluginfo) {
497 if (!$this->common_uninstall_check($subpluginfo)) {
498 return false;
500 // Check if there are some other plugins requiring this subplugin
501 // (but the parent and siblings).
502 foreach ($this->other_plugins_that_require($subpluginfo->component) as $requiresme) {
503 $ismyparent = ($pluginfo->component === $requiresme);
504 $ismysibling = in_array($requiresme, array_keys($subplugins));
505 if (!$ismyparent and !$ismysibling) {
506 return false;
511 // Check if there are some other plugins requiring this plugin
512 // (but its subplugins).
513 foreach ($this->other_plugins_that_require($pluginfo->component) as $requiresme) {
514 $ismysubplugin = in_array($requiresme, array_keys($subplugins));
515 if (!$ismysubplugin) {
516 return false;
520 return true;
524 * Uninstall the given plugin.
526 * Automatically cleans-up all remaining configuration data, log records, events,
527 * files from the file pool etc.
529 * In the future, the functionality of {@link uninstall_plugin()} function may be moved
530 * into this method and all the code should be refactored to use it. At the moment, we
531 * mimic this future behaviour by wrapping that function call.
533 * @param string $component
534 * @param progress_trace $progress traces the process
535 * @return bool true on success, false on errors/problems
537 public function uninstall_plugin($component, progress_trace $progress) {
539 $pluginfo = $this->get_plugin_info($component);
541 if (is_null($pluginfo)) {
542 return false;
545 // Give the pluginfo class a chance to execute some steps.
546 $result = $pluginfo->uninstall($progress);
547 if (!$result) {
548 return false;
551 // Call the legacy core function to uninstall the plugin.
552 ob_start();
553 uninstall_plugin($pluginfo->type, $pluginfo->name);
554 $progress->output(ob_get_clean());
556 return true;
560 * Checks if there are some plugins with a known available update
562 * @return bool true if there is at least one available update
564 public function some_plugins_updatable() {
565 foreach ($this->get_plugins() as $type => $plugins) {
566 foreach ($plugins as $plugin) {
567 if ($plugin->available_updates()) {
568 return true;
573 return false;
577 * Check to see if the given plugin folder can be removed by the web server process.
579 * @param string $component full frankenstyle component
580 * @return bool
582 public function is_plugin_folder_removable($component) {
584 $pluginfo = $this->get_plugin_info($component);
586 if (is_null($pluginfo)) {
587 return false;
590 // To be able to remove the plugin folder, its parent must be writable, too.
591 if (!is_writable(dirname($pluginfo->rootdir))) {
592 return false;
595 // Check that the folder and all its content is writable (thence removable).
596 return $this->is_directory_removable($pluginfo->rootdir);
600 * Defines a list of all plugins that were originally shipped in the standard Moodle distribution,
601 * but are not anymore and are deleted during upgrades.
603 * The main purpose of this list is to hide missing plugins during upgrade.
605 * @param string $type plugin type
606 * @param string $name plugin name
607 * @return bool
609 public static function is_deleted_standard_plugin($type, $name) {
611 // Example of the array structure:
612 // $plugins = array(
613 // 'block' => array('admin', 'admin_tree'),
614 // 'mod' => array('assignment'),
615 // );
616 // Do not include plugins that were removed during upgrades to versions that are
617 // not supported as source versions for upgrade any more. For example, at MOODLE_23_STABLE
618 // branch, listed should be no plugins that were removed at 1.9.x - 2.1.x versions as
619 // Moodle 2.3 supports upgrades from 2.2.x only.
620 $plugins = array(
621 'qformat' => array('blackboard'),
624 if (!isset($plugins[$type])) {
625 return false;
627 return in_array($name, $plugins[$type]);
631 * Defines a white list of all plugins shipped in the standard Moodle distribution
633 * @param string $type
634 * @return false|array array of standard plugins or false if the type is unknown
636 public static function standard_plugins_list($type) {
637 $standard_plugins = array(
639 'assignment' => array(
640 'offline', 'online', 'upload', 'uploadsingle'
643 'assignsubmission' => array(
644 'comments', 'file', 'onlinetext'
647 'assignfeedback' => array(
648 'comments', 'file', 'offline'
651 'auth' => array(
652 'cas', 'db', 'email', 'fc', 'imap', 'ldap', 'manual', 'mnet',
653 'nntp', 'nologin', 'none', 'pam', 'pop3', 'radius',
654 'shibboleth', 'webservice'
657 'block' => array(
658 'activity_modules', 'admin_bookmarks', 'badges', 'blog_menu',
659 'blog_recent', 'blog_tags', 'calendar_month',
660 'calendar_upcoming', 'comments', 'community',
661 'completionstatus', 'course_list', 'course_overview',
662 'course_summary', 'feedback', 'glossary_random', 'html',
663 'login', 'mentees', 'messages', 'mnet_hosts', 'myprofile',
664 'navigation', 'news_items', 'online_users', 'participants',
665 'private_files', 'quiz_results', 'recent_activity',
666 'rss_client', 'search_forums', 'section_links',
667 'selfcompletion', 'settings', 'site_main_menu',
668 'social_activities', 'tag_flickr', 'tag_youtube', 'tags'
671 'booktool' => array(
672 'exportimscp', 'importhtml', 'print'
675 'cachelock' => array(
676 'file'
679 'cachestore' => array(
680 'file', 'memcache', 'memcached', 'mongodb', 'session', 'static'
683 'coursereport' => array(
684 //deprecated!
687 'datafield' => array(
688 'checkbox', 'date', 'file', 'latlong', 'menu', 'multimenu',
689 'number', 'picture', 'radiobutton', 'text', 'textarea', 'url'
692 'datapreset' => array(
693 'imagegallery'
696 'editor' => array(
697 'textarea', 'tinymce'
700 'enrol' => array(
701 'authorize', 'category', 'cohort', 'database', 'flatfile',
702 'guest', 'imsenterprise', 'ldap', 'manual', 'meta', 'mnet',
703 'paypal', 'self'
706 'filter' => array(
707 'activitynames', 'algebra', 'censor', 'emailprotect',
708 'emoticon', 'mediaplugin', 'multilang', 'tex', 'tidy',
709 'urltolink', 'data', 'glossary'
712 'format' => array(
713 'scorm', 'social', 'topics', 'weeks'
716 'gradeexport' => array(
717 'ods', 'txt', 'xls', 'xml'
720 'gradeimport' => array(
721 'csv', 'xml'
724 'gradereport' => array(
725 'grader', 'outcomes', 'overview', 'user'
728 'gradingform' => array(
729 'rubric', 'guide'
732 'local' => array(
735 'message' => array(
736 'email', 'jabber', 'popup'
739 'mnetservice' => array(
740 'enrol'
743 'mod' => array(
744 'assign', 'assignment', 'book', 'chat', 'choice', 'data', 'feedback', 'folder',
745 'forum', 'glossary', 'imscp', 'label', 'lesson', 'lti', 'page',
746 'quiz', 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop'
749 'plagiarism' => array(
752 'portfolio' => array(
753 'boxnet', 'download', 'flickr', 'googledocs', 'mahara', 'picasa'
756 'profilefield' => array(
757 'checkbox', 'datetime', 'menu', 'text', 'textarea'
760 'qbehaviour' => array(
761 'adaptive', 'adaptivenopenalty', 'deferredcbm',
762 'deferredfeedback', 'immediatecbm', 'immediatefeedback',
763 'informationitem', 'interactive', 'interactivecountback',
764 'manualgraded', 'missing'
767 'qformat' => array(
768 'aiken', 'blackboard_six', 'examview', 'gift',
769 'learnwise', 'missingword', 'multianswer', 'webct',
770 'xhtml', 'xml'
773 'qtype' => array(
774 'calculated', 'calculatedmulti', 'calculatedsimple',
775 'description', 'essay', 'match', 'missingtype', 'multianswer',
776 'multichoice', 'numerical', 'random', 'randomsamatch',
777 'shortanswer', 'truefalse'
780 'quiz' => array(
781 'grading', 'overview', 'responses', 'statistics'
784 'quizaccess' => array(
785 'delaybetweenattempts', 'ipaddress', 'numattempts', 'openclosedate',
786 'password', 'safebrowser', 'securewindow', 'timelimit'
789 'report' => array(
790 'backups', 'completion', 'configlog', 'courseoverview',
791 'log', 'loglive', 'outline', 'participation', 'progress', 'questioninstances', 'security', 'stats', 'performance'
794 'repository' => array(
795 'alfresco', 'boxnet', 'coursefiles', 'dropbox', 'equella', 'filesystem',
796 'flickr', 'flickr_public', 'googledocs', 'local', 'merlot',
797 'picasa', 'recent', 's3', 'upload', 'url', 'user', 'webdav',
798 'wikimedia', 'youtube'
801 'scormreport' => array(
802 'basic',
803 'interactions',
804 'graphs'
807 'tinymce' => array(
808 'ctrlhelp', 'dragmath', 'moodleemoticon', 'moodleimage', 'moodlemedia', 'moodlenolink', 'spellchecker',
811 'theme' => array(
812 'afterburner', 'anomaly', 'arialist', 'base', 'binarius', 'bootstrapbase',
813 'boxxie', 'brick', 'canvas', 'clean', 'formal_white', 'formfactor',
814 'fusion', 'leatherbound', 'magazine', 'mymobile', 'nimble',
815 'nonzero', 'overlay', 'serenity', 'sky_high', 'splash',
816 'standard', 'standardold'
819 'tool' => array(
820 'assignmentupgrade', 'behat', 'capability', 'customlang',
821 'dbtransfer', 'generator', 'health', 'innodb', 'installaddon',
822 'langimport', 'multilangupgrade', 'phpunit', 'profiling',
823 'qeupgradehelper', 'replace', 'spamcleaner', 'timezoneimport',
824 'unittest', 'uploaduser', 'unsuproles', 'xmldb'
827 'webservice' => array(
828 'amf', 'rest', 'soap', 'xmlrpc'
831 'workshopallocation' => array(
832 'manual', 'random', 'scheduled'
835 'workshopeval' => array(
836 'best'
839 'workshopform' => array(
840 'accumulative', 'comments', 'numerrors', 'rubric'
844 if (isset($standard_plugins[$type])) {
845 return $standard_plugins[$type];
846 } else {
847 return false;
852 * Wrapper for the core function {@link normalize_component()}.
854 * This is here just to make it possible to mock it in unit tests.
856 * @param string $component
857 * @return array
859 protected function normalize_component($component) {
860 return normalize_component($component);
864 * Reorders plugin types into a sequence to be displayed
866 * For technical reasons, plugin types returned by {@link get_plugin_types()} are
867 * in a certain order that does not need to fit the expected order for the display.
868 * Particularly, activity modules should be displayed first as they represent the
869 * real heart of Moodle. They should be followed by other plugin types that are
870 * used to build the courses (as that is what one expects from LMS). After that,
871 * other supportive plugin types follow.
873 * @param array $types associative array
874 * @return array same array with altered order of items
876 protected function reorder_plugin_types(array $types) {
877 $fix = array(
878 'mod' => $types['mod'],
879 'block' => $types['block'],
880 'qtype' => $types['qtype'],
881 'qbehaviour' => $types['qbehaviour'],
882 'qformat' => $types['qformat'],
883 'filter' => $types['filter'],
884 'enrol' => $types['enrol'],
886 foreach ($types as $type => $path) {
887 if (!isset($fix[$type])) {
888 $fix[$type] = $path;
891 return $fix;
895 * Check if the given directory can be removed by the web server process.
897 * This recursively checks that the given directory and all its contents
898 * it writable.
900 * @param string $fullpath
901 * @return boolean
903 protected function is_directory_removable($fullpath) {
905 if (!is_writable($fullpath)) {
906 return false;
909 if (is_dir($fullpath)) {
910 $handle = opendir($fullpath);
911 } else {
912 return false;
915 $result = true;
917 while ($filename = readdir($handle)) {
919 if ($filename === '.' or $filename === '..') {
920 continue;
923 $subfilepath = $fullpath.'/'.$filename;
925 if (is_dir($subfilepath)) {
926 $result = $result && $this->is_directory_removable($subfilepath);
928 } else {
929 $result = $result && is_writable($subfilepath);
933 closedir($handle);
935 return $result;
939 * Helper method that implements common uninstall prerequisities
941 * @param plugininfo_base $pluginfo
942 * @return bool
944 protected function common_uninstall_check(plugininfo_base $pluginfo) {
946 if (!$pluginfo->is_uninstall_allowed()) {
947 // The plugin's plugininfo class declares it should not be uninstalled.
948 return false;
951 if ($pluginfo->get_status() === plugin_manager::PLUGIN_STATUS_NEW) {
952 // The plugin is not installed. It should be either installed or removed from the disk.
953 // Relying on this temporary state may be tricky.
954 return false;
957 if (is_null($pluginfo->get_uninstall_url())) {
958 // Backwards compatibility.
959 debugging('plugininfo_base subclasses should use is_uninstall_allowed() instead of returning null in get_uninstall_url()',
960 DEBUG_DEVELOPER);
961 return false;
964 return true;
970 * General exception thrown by the {@link available_update_checker} class
972 class available_update_checker_exception extends moodle_exception {
975 * @param string $errorcode exception description identifier
976 * @param mixed $debuginfo debugging data to display
978 public function __construct($errorcode, $debuginfo=null) {
979 parent::__construct($errorcode, 'core_plugin', '', null, print_r($debuginfo, true));
985 * Singleton class that handles checking for available updates
987 class available_update_checker {
989 /** @var available_update_checker holds the singleton instance */
990 protected static $singletoninstance;
991 /** @var null|int the timestamp of when the most recent response was fetched */
992 protected $recentfetch = null;
993 /** @var null|array the recent response from the update notification provider */
994 protected $recentresponse = null;
995 /** @var null|string the numerical version of the local Moodle code */
996 protected $currentversion = null;
997 /** @var null|string the release info of the local Moodle code */
998 protected $currentrelease = null;
999 /** @var null|string branch of the local Moodle code */
1000 protected $currentbranch = null;
1001 /** @var array of (string)frankestyle => (string)version list of additional plugins deployed at this site */
1002 protected $currentplugins = array();
1005 * Direct initiation not allowed, use the factory method {@link self::instance()}
1007 protected function __construct() {
1011 * Sorry, this is singleton
1013 protected function __clone() {
1017 * Factory method for this class
1019 * @return available_update_checker the singleton instance
1021 public static function instance() {
1022 if (is_null(self::$singletoninstance)) {
1023 self::$singletoninstance = new self();
1025 return self::$singletoninstance;
1029 * Reset any caches
1030 * @param bool $phpunitreset
1032 public static function reset_caches($phpunitreset = false) {
1033 if ($phpunitreset) {
1034 self::$singletoninstance = null;
1039 * Returns the timestamp of the last execution of {@link fetch()}
1041 * @return int|null null if it has never been executed or we don't known
1043 public function get_last_timefetched() {
1045 $this->restore_response();
1047 if (!empty($this->recentfetch)) {
1048 return $this->recentfetch;
1050 } else {
1051 return null;
1056 * Fetches the available update status from the remote site
1058 * @throws available_update_checker_exception
1060 public function fetch() {
1061 $response = $this->get_response();
1062 $this->validate_response($response);
1063 $this->store_response($response);
1067 * Returns the available update information for the given component
1069 * This method returns null if the most recent response does not contain any information
1070 * about it. The returned structure is an array of available updates for the given
1071 * component. Each update info is an object with at least one property called
1072 * 'version'. Other possible properties are 'release', 'maturity', 'url' and 'downloadurl'.
1074 * For the 'core' component, the method returns real updates only (those with higher version).
1075 * For all other components, the list of all known remote updates is returned and the caller
1076 * (usually the {@link plugin_manager}) is supposed to make the actual comparison of versions.
1078 * @param string $component frankenstyle
1079 * @param array $options with supported keys 'minmaturity' and/or 'notifybuilds'
1080 * @return null|array null or array of available_update_info objects
1082 public function get_update_info($component, array $options = array()) {
1084 if (!isset($options['minmaturity'])) {
1085 $options['minmaturity'] = 0;
1088 if (!isset($options['notifybuilds'])) {
1089 $options['notifybuilds'] = false;
1092 if ($component == 'core') {
1093 $this->load_current_environment();
1096 $this->restore_response();
1098 if (empty($this->recentresponse['updates'][$component])) {
1099 return null;
1102 $updates = array();
1103 foreach ($this->recentresponse['updates'][$component] as $info) {
1104 $update = new available_update_info($component, $info);
1105 if (isset($update->maturity) and ($update->maturity < $options['minmaturity'])) {
1106 continue;
1108 if ($component == 'core') {
1109 if ($update->version <= $this->currentversion) {
1110 continue;
1112 if (empty($options['notifybuilds']) and $this->is_same_release($update->release)) {
1113 continue;
1116 $updates[] = $update;
1119 if (empty($updates)) {
1120 return null;
1123 return $updates;
1127 * The method being run via cron.php
1129 public function cron() {
1130 global $CFG;
1132 if (!$this->cron_autocheck_enabled()) {
1133 $this->cron_mtrace('Automatic check for available updates not enabled, skipping.');
1134 return;
1137 $now = $this->cron_current_timestamp();
1139 if ($this->cron_has_fresh_fetch($now)) {
1140 $this->cron_mtrace('Recently fetched info about available updates is still fresh enough, skipping.');
1141 return;
1144 if ($this->cron_has_outdated_fetch($now)) {
1145 $this->cron_mtrace('Outdated or missing info about available updates, forced fetching ... ', '');
1146 $this->cron_execute();
1147 return;
1150 $offset = $this->cron_execution_offset();
1151 $start = mktime(1, 0, 0, date('n', $now), date('j', $now), date('Y', $now)); // 01:00 AM today local time
1152 if ($now > $start + $offset) {
1153 $this->cron_mtrace('Regular daily check for available updates ... ', '');
1154 $this->cron_execute();
1155 return;
1159 /// end of public API //////////////////////////////////////////////////////
1162 * Makes cURL request to get data from the remote site
1164 * @return string raw request result
1165 * @throws available_update_checker_exception
1167 protected function get_response() {
1168 global $CFG;
1169 require_once($CFG->libdir.'/filelib.php');
1171 $curl = new curl(array('proxy' => true));
1172 $response = $curl->post($this->prepare_request_url(), $this->prepare_request_params(), $this->prepare_request_options());
1173 $curlerrno = $curl->get_errno();
1174 if (!empty($curlerrno)) {
1175 throw new available_update_checker_exception('err_response_curl', 'cURL error '.$curlerrno.': '.$curl->error);
1177 $curlinfo = $curl->get_info();
1178 if ($curlinfo['http_code'] != 200) {
1179 throw new available_update_checker_exception('err_response_http_code', $curlinfo['http_code']);
1181 return $response;
1185 * Makes sure the response is valid, has correct API format etc.
1187 * @param string $response raw response as returned by the {@link self::get_response()}
1188 * @throws available_update_checker_exception
1190 protected function validate_response($response) {
1192 $response = $this->decode_response($response);
1194 if (empty($response)) {
1195 throw new available_update_checker_exception('err_response_empty');
1198 if (empty($response['status']) or $response['status'] !== 'OK') {
1199 throw new available_update_checker_exception('err_response_status', $response['status']);
1202 if (empty($response['apiver']) or $response['apiver'] !== '1.2') {
1203 throw new available_update_checker_exception('err_response_format_version', $response['apiver']);
1206 if (empty($response['forbranch']) or $response['forbranch'] !== moodle_major_version(true)) {
1207 throw new available_update_checker_exception('err_response_target_version', $response['forbranch']);
1212 * Decodes the raw string response from the update notifications provider
1214 * @param string $response as returned by {@link self::get_response()}
1215 * @return array decoded response structure
1217 protected function decode_response($response) {
1218 return json_decode($response, true);
1222 * Stores the valid fetched response for later usage
1224 * This implementation uses the config_plugins table as the permanent storage.
1226 * @param string $response raw valid data returned by {@link self::get_response()}
1228 protected function store_response($response) {
1230 set_config('recentfetch', time(), 'core_plugin');
1231 set_config('recentresponse', $response, 'core_plugin');
1233 $this->restore_response(true);
1237 * Loads the most recent raw response record we have fetched
1239 * After this method is called, $this->recentresponse is set to an array. If the
1240 * array is empty, then either no data have been fetched yet or the fetched data
1241 * do not have expected format (and thence they are ignored and a debugging
1242 * message is displayed).
1244 * This implementation uses the config_plugins table as the permanent storage.
1246 * @param bool $forcereload reload even if it was already loaded
1248 protected function restore_response($forcereload = false) {
1250 if (!$forcereload and !is_null($this->recentresponse)) {
1251 // we already have it, nothing to do
1252 return;
1255 $config = get_config('core_plugin');
1257 if (!empty($config->recentresponse) and !empty($config->recentfetch)) {
1258 try {
1259 $this->validate_response($config->recentresponse);
1260 $this->recentfetch = $config->recentfetch;
1261 $this->recentresponse = $this->decode_response($config->recentresponse);
1262 } catch (available_update_checker_exception $e) {
1263 // The server response is not valid. Behave as if no data were fetched yet.
1264 // This may happen when the most recent update info (cached locally) has been
1265 // fetched with the previous branch of Moodle (like during an upgrade from 2.x
1266 // to 2.y) or when the API of the response has changed.
1267 $this->recentresponse = array();
1270 } else {
1271 $this->recentresponse = array();
1276 * Compares two raw {@link $recentresponse} records and returns the list of changed updates
1278 * This method is used to populate potential update info to be sent to site admins.
1280 * @param array $old
1281 * @param array $new
1282 * @throws available_update_checker_exception
1283 * @return array parts of $new['updates'] that have changed
1285 protected function compare_responses(array $old, array $new) {
1287 if (empty($new)) {
1288 return array();
1291 if (!array_key_exists('updates', $new)) {
1292 throw new available_update_checker_exception('err_response_format');
1295 if (empty($old)) {
1296 return $new['updates'];
1299 if (!array_key_exists('updates', $old)) {
1300 throw new available_update_checker_exception('err_response_format');
1303 $changes = array();
1305 foreach ($new['updates'] as $newcomponent => $newcomponentupdates) {
1306 if (empty($old['updates'][$newcomponent])) {
1307 $changes[$newcomponent] = $newcomponentupdates;
1308 continue;
1310 foreach ($newcomponentupdates as $newcomponentupdate) {
1311 $inold = false;
1312 foreach ($old['updates'][$newcomponent] as $oldcomponentupdate) {
1313 if ($newcomponentupdate['version'] == $oldcomponentupdate['version']) {
1314 $inold = true;
1317 if (!$inold) {
1318 if (!isset($changes[$newcomponent])) {
1319 $changes[$newcomponent] = array();
1321 $changes[$newcomponent][] = $newcomponentupdate;
1326 return $changes;
1330 * Returns the URL to send update requests to
1332 * During the development or testing, you can set $CFG->alternativeupdateproviderurl
1333 * to a custom URL that will be used. Otherwise the standard URL will be returned.
1335 * @return string URL
1337 protected function prepare_request_url() {
1338 global $CFG;
1340 if (!empty($CFG->config_php_settings['alternativeupdateproviderurl'])) {
1341 return $CFG->config_php_settings['alternativeupdateproviderurl'];
1342 } else {
1343 return 'https://download.moodle.org/api/1.2/updates.php';
1348 * Sets the properties currentversion, currentrelease, currentbranch and currentplugins
1350 * @param bool $forcereload
1352 protected function load_current_environment($forcereload=false) {
1353 global $CFG;
1355 if (!is_null($this->currentversion) and !$forcereload) {
1356 // nothing to do
1357 return;
1360 $version = null;
1361 $release = null;
1363 require($CFG->dirroot.'/version.php');
1364 $this->currentversion = $version;
1365 $this->currentrelease = $release;
1366 $this->currentbranch = moodle_major_version(true);
1368 $pluginman = plugin_manager::instance();
1369 foreach ($pluginman->get_plugins() as $type => $plugins) {
1370 foreach ($plugins as $plugin) {
1371 if (!$plugin->is_standard()) {
1372 $this->currentplugins[$plugin->component] = $plugin->versiondisk;
1379 * Returns the list of HTTP params to be sent to the updates provider URL
1381 * @return array of (string)param => (string)value
1383 protected function prepare_request_params() {
1384 global $CFG;
1386 $this->load_current_environment();
1387 $this->restore_response();
1389 $params = array();
1390 $params['format'] = 'json';
1392 if (isset($this->recentresponse['ticket'])) {
1393 $params['ticket'] = $this->recentresponse['ticket'];
1396 if (isset($this->currentversion)) {
1397 $params['version'] = $this->currentversion;
1398 } else {
1399 throw new coding_exception('Main Moodle version must be already known here');
1402 if (isset($this->currentbranch)) {
1403 $params['branch'] = $this->currentbranch;
1404 } else {
1405 throw new coding_exception('Moodle release must be already known here');
1408 $plugins = array();
1409 foreach ($this->currentplugins as $plugin => $version) {
1410 $plugins[] = $plugin.'@'.$version;
1412 if (!empty($plugins)) {
1413 $params['plugins'] = implode(',', $plugins);
1416 return $params;
1420 * Returns the list of cURL options to use when fetching available updates data
1422 * @return array of (string)param => (string)value
1424 protected function prepare_request_options() {
1425 global $CFG;
1427 $options = array(
1428 'CURLOPT_SSL_VERIFYHOST' => 2, // this is the default in {@link curl} class but just in case
1429 'CURLOPT_SSL_VERIFYPEER' => true,
1432 return $options;
1436 * Returns the current timestamp
1438 * @return int the timestamp
1440 protected function cron_current_timestamp() {
1441 return time();
1445 * Output cron debugging info
1447 * @see mtrace()
1448 * @param string $msg output message
1449 * @param string $eol end of line
1451 protected function cron_mtrace($msg, $eol = PHP_EOL) {
1452 mtrace($msg, $eol);
1456 * Decide if the autocheck feature is disabled in the server setting
1458 * @return bool true if autocheck enabled, false if disabled
1460 protected function cron_autocheck_enabled() {
1461 global $CFG;
1463 if (empty($CFG->updateautocheck)) {
1464 return false;
1465 } else {
1466 return true;
1471 * Decide if the recently fetched data are still fresh enough
1473 * @param int $now current timestamp
1474 * @return bool true if no need to re-fetch, false otherwise
1476 protected function cron_has_fresh_fetch($now) {
1477 $recent = $this->get_last_timefetched();
1479 if (empty($recent)) {
1480 return false;
1483 if ($now < $recent) {
1484 $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1485 return true;
1488 if ($now - $recent > 24 * HOURSECS) {
1489 return false;
1492 return true;
1496 * Decide if the fetch is outadated or even missing
1498 * @param int $now current timestamp
1499 * @return bool false if no need to re-fetch, true otherwise
1501 protected function cron_has_outdated_fetch($now) {
1502 $recent = $this->get_last_timefetched();
1504 if (empty($recent)) {
1505 return true;
1508 if ($now < $recent) {
1509 $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1510 return false;
1513 if ($now - $recent > 48 * HOURSECS) {
1514 return true;
1517 return false;
1521 * Returns the cron execution offset for this site
1523 * The main {@link self::cron()} is supposed to run every night in some random time
1524 * between 01:00 and 06:00 AM (local time). The exact moment is defined by so called
1525 * execution offset, that is the amount of time after 01:00 AM. The offset value is
1526 * initially generated randomly and then used consistently at the site. This way, the
1527 * regular checks against the download.moodle.org server are spread in time.
1529 * @return int the offset number of seconds from range 1 sec to 5 hours
1531 protected function cron_execution_offset() {
1532 global $CFG;
1534 if (empty($CFG->updatecronoffset)) {
1535 set_config('updatecronoffset', rand(1, 5 * HOURSECS));
1538 return $CFG->updatecronoffset;
1542 * Fetch available updates info and eventually send notification to site admins
1544 protected function cron_execute() {
1546 try {
1547 $this->restore_response();
1548 $previous = $this->recentresponse;
1549 $this->fetch();
1550 $this->restore_response(true);
1551 $current = $this->recentresponse;
1552 $changes = $this->compare_responses($previous, $current);
1553 $notifications = $this->cron_notifications($changes);
1554 $this->cron_notify($notifications);
1555 $this->cron_mtrace('done');
1556 } catch (available_update_checker_exception $e) {
1557 $this->cron_mtrace('FAILED!');
1562 * Given the list of changes in available updates, pick those to send to site admins
1564 * @param array $changes as returned by {@link self::compare_responses()}
1565 * @return array of available_update_info objects to send to site admins
1567 protected function cron_notifications(array $changes) {
1568 global $CFG;
1570 $notifications = array();
1571 $pluginman = plugin_manager::instance();
1572 $plugins = $pluginman->get_plugins(true);
1574 foreach ($changes as $component => $componentchanges) {
1575 if (empty($componentchanges)) {
1576 continue;
1578 $componentupdates = $this->get_update_info($component,
1579 array('minmaturity' => $CFG->updateminmaturity, 'notifybuilds' => $CFG->updatenotifybuilds));
1580 if (empty($componentupdates)) {
1581 continue;
1583 // notify only about those $componentchanges that are present in $componentupdates
1584 // to respect the preferences
1585 foreach ($componentchanges as $componentchange) {
1586 foreach ($componentupdates as $componentupdate) {
1587 if ($componentupdate->version == $componentchange['version']) {
1588 if ($component == 'core') {
1589 // In case of 'core', we already know that the $componentupdate
1590 // is a real update with higher version ({@see self::get_update_info()}).
1591 // We just perform additional check for the release property as there
1592 // can be two Moodle releases having the same version (e.g. 2.4.0 and 2.5dev shortly
1593 // after the release). We can do that because we have the release info
1594 // always available for the core.
1595 if ((string)$componentupdate->release === (string)$componentchange['release']) {
1596 $notifications[] = $componentupdate;
1598 } else {
1599 // Use the plugin_manager to check if the detected $componentchange
1600 // is a real update with higher version. That is, the $componentchange
1601 // is present in the array of {@link available_update_info} objects
1602 // returned by the plugin's available_updates() method.
1603 list($plugintype, $pluginname) = normalize_component($component);
1604 if (!empty($plugins[$plugintype][$pluginname])) {
1605 $availableupdates = $plugins[$plugintype][$pluginname]->available_updates();
1606 if (!empty($availableupdates)) {
1607 foreach ($availableupdates as $availableupdate) {
1608 if ($availableupdate->version == $componentchange['version']) {
1609 $notifications[] = $componentupdate;
1620 return $notifications;
1624 * Sends the given notifications to site admins via messaging API
1626 * @param array $notifications array of available_update_info objects to send
1628 protected function cron_notify(array $notifications) {
1629 global $CFG;
1631 if (empty($notifications)) {
1632 return;
1635 $admins = get_admins();
1637 if (empty($admins)) {
1638 return;
1641 $this->cron_mtrace('sending notifications ... ', '');
1643 $text = get_string('updatenotifications', 'core_admin') . PHP_EOL;
1644 $html = html_writer::tag('h1', get_string('updatenotifications', 'core_admin')) . PHP_EOL;
1646 $coreupdates = array();
1647 $pluginupdates = array();
1649 foreach ($notifications as $notification) {
1650 if ($notification->component == 'core') {
1651 $coreupdates[] = $notification;
1652 } else {
1653 $pluginupdates[] = $notification;
1657 if (!empty($coreupdates)) {
1658 $text .= PHP_EOL . get_string('updateavailable', 'core_admin') . PHP_EOL;
1659 $html .= html_writer::tag('h2', get_string('updateavailable', 'core_admin')) . PHP_EOL;
1660 $html .= html_writer::start_tag('ul') . PHP_EOL;
1661 foreach ($coreupdates as $coreupdate) {
1662 $html .= html_writer::start_tag('li');
1663 if (isset($coreupdate->release)) {
1664 $text .= get_string('updateavailable_release', 'core_admin', $coreupdate->release);
1665 $html .= html_writer::tag('strong', get_string('updateavailable_release', 'core_admin', $coreupdate->release));
1667 if (isset($coreupdate->version)) {
1668 $text .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
1669 $html .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
1671 if (isset($coreupdate->maturity)) {
1672 $text .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
1673 $html .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
1675 $text .= PHP_EOL;
1676 $html .= html_writer::end_tag('li') . PHP_EOL;
1678 $text .= PHP_EOL;
1679 $html .= html_writer::end_tag('ul') . PHP_EOL;
1681 $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/index.php');
1682 $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
1683 $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/index.php', $CFG->wwwroot.'/'.$CFG->admin.'/index.php'));
1684 $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
1687 if (!empty($pluginupdates)) {
1688 $text .= PHP_EOL . get_string('updateavailableforplugin', 'core_admin') . PHP_EOL;
1689 $html .= html_writer::tag('h2', get_string('updateavailableforplugin', 'core_admin')) . PHP_EOL;
1691 $html .= html_writer::start_tag('ul') . PHP_EOL;
1692 foreach ($pluginupdates as $pluginupdate) {
1693 $html .= html_writer::start_tag('li');
1694 $text .= get_string('pluginname', $pluginupdate->component);
1695 $html .= html_writer::tag('strong', get_string('pluginname', $pluginupdate->component));
1697 $text .= ' ('.$pluginupdate->component.')';
1698 $html .= ' ('.$pluginupdate->component.')';
1700 $text .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
1701 $html .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
1703 $text .= PHP_EOL;
1704 $html .= html_writer::end_tag('li') . PHP_EOL;
1706 $text .= PHP_EOL;
1707 $html .= html_writer::end_tag('ul') . PHP_EOL;
1709 $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php');
1710 $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
1711 $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/plugins.php', $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php'));
1712 $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
1715 $a = array('siteurl' => $CFG->wwwroot);
1716 $text .= get_string('updatenotificationfooter', 'core_admin', $a) . PHP_EOL;
1717 $a = array('siteurl' => html_writer::link($CFG->wwwroot, $CFG->wwwroot));
1718 $html .= html_writer::tag('footer', html_writer::tag('p', get_string('updatenotificationfooter', 'core_admin', $a),
1719 array('style' => 'font-size:smaller; color:#333;')));
1721 foreach ($admins as $admin) {
1722 $message = new stdClass();
1723 $message->component = 'moodle';
1724 $message->name = 'availableupdate';
1725 $message->userfrom = get_admin();
1726 $message->userto = $admin;
1727 $message->subject = get_string('updatenotificationsubject', 'core_admin', array('siteurl' => $CFG->wwwroot));
1728 $message->fullmessage = $text;
1729 $message->fullmessageformat = FORMAT_PLAIN;
1730 $message->fullmessagehtml = $html;
1731 $message->smallmessage = get_string('updatenotifications', 'core_admin');
1732 $message->notification = 1;
1733 message_send($message);
1738 * Compare two release labels and decide if they are the same
1740 * @param string $remote release info of the available update
1741 * @param null|string $local release info of the local code, defaults to $release defined in version.php
1742 * @return boolean true if the releases declare the same minor+major version
1744 protected function is_same_release($remote, $local=null) {
1746 if (is_null($local)) {
1747 $this->load_current_environment();
1748 $local = $this->currentrelease;
1751 $pattern = '/^([0-9\.\+]+)([^(]*)/';
1753 preg_match($pattern, $remote, $remotematches);
1754 preg_match($pattern, $local, $localmatches);
1756 $remotematches[1] = str_replace('+', '', $remotematches[1]);
1757 $localmatches[1] = str_replace('+', '', $localmatches[1]);
1759 if ($remotematches[1] === $localmatches[1] and rtrim($remotematches[2]) === rtrim($localmatches[2])) {
1760 return true;
1761 } else {
1762 return false;
1769 * Defines the structure of objects returned by {@link available_update_checker::get_update_info()}
1771 class available_update_info {
1773 /** @var string frankenstyle component name */
1774 public $component;
1775 /** @var int the available version of the component */
1776 public $version;
1777 /** @var string|null optional release name */
1778 public $release = null;
1779 /** @var int|null optional maturity info, eg {@link MATURITY_STABLE} */
1780 public $maturity = null;
1781 /** @var string|null optional URL of a page with more info about the update */
1782 public $url = null;
1783 /** @var string|null optional URL of a ZIP package that can be downloaded and installed */
1784 public $download = null;
1785 /** @var string|null of self::download is set, then this must be the MD5 hash of the ZIP */
1786 public $downloadmd5 = null;
1789 * Creates new instance of the class
1791 * The $info array must provide at least the 'version' value and optionally all other
1792 * values to populate the object's properties.
1794 * @param string $name the frankenstyle component name
1795 * @param array $info associative array with other properties
1797 public function __construct($name, array $info) {
1798 $this->component = $name;
1799 foreach ($info as $k => $v) {
1800 if (property_exists('available_update_info', $k) and $k != 'component') {
1801 $this->$k = $v;
1809 * Implements a communication bridge to the mdeploy.php utility
1811 class available_update_deployer {
1813 const HTTP_PARAM_PREFIX = 'updteautodpldata_'; // Hey, even Google has not heard of such a prefix! So it MUST be safe :-p
1814 const HTTP_PARAM_CHECKER = 'datapackagesize'; // Name of the parameter that holds the number of items in the received data items
1816 /** @var available_update_deployer holds the singleton instance */
1817 protected static $singletoninstance;
1818 /** @var moodle_url URL of a page that includes the deployer UI */
1819 protected $callerurl;
1820 /** @var moodle_url URL to return after the deployment */
1821 protected $returnurl;
1824 * Direct instantiation not allowed, use the factory method {@link self::instance()}
1826 protected function __construct() {
1830 * Sorry, this is singleton
1832 protected function __clone() {
1836 * Factory method for this class
1838 * @return available_update_deployer the singleton instance
1840 public static function instance() {
1841 if (is_null(self::$singletoninstance)) {
1842 self::$singletoninstance = new self();
1844 return self::$singletoninstance;
1848 * Reset caches used by this script
1850 * @param bool $phpunitreset is this called as a part of PHPUnit reset?
1852 public static function reset_caches($phpunitreset = false) {
1853 if ($phpunitreset) {
1854 self::$singletoninstance = null;
1859 * Is automatic deployment enabled?
1861 * @return bool
1863 public function enabled() {
1864 global $CFG;
1866 if (!empty($CFG->disableupdateautodeploy)) {
1867 // The feature is prohibited via config.php
1868 return false;
1871 return get_config('updateautodeploy');
1875 * Sets some base properties of the class to make it usable.
1877 * @param moodle_url $callerurl the base URL of a script that will handle the class'es form data
1878 * @param moodle_url $returnurl the final URL to return to when the deployment is finished
1880 public function initialize(moodle_url $callerurl, moodle_url $returnurl) {
1882 if (!$this->enabled()) {
1883 throw new coding_exception('Unable to initialize the deployer, the feature is not enabled.');
1886 $this->callerurl = $callerurl;
1887 $this->returnurl = $returnurl;
1891 * Has the deployer been initialized?
1893 * Initialized deployer means that the following properties were set:
1894 * callerurl, returnurl
1896 * @return bool
1898 public function initialized() {
1900 if (!$this->enabled()) {
1901 return false;
1904 if (empty($this->callerurl)) {
1905 return false;
1908 if (empty($this->returnurl)) {
1909 return false;
1912 return true;
1916 * Returns a list of reasons why the deployment can not happen
1918 * If the returned array is empty, the deployment seems to be possible. The returned
1919 * structure is an associative array with keys representing individual impediments.
1920 * Possible keys are: missingdownloadurl, missingdownloadmd5, notwritable.
1922 * @param available_update_info $info
1923 * @return array
1925 public function deployment_impediments(available_update_info $info) {
1927 $impediments = array();
1929 if (empty($info->download)) {
1930 $impediments['missingdownloadurl'] = true;
1933 if (empty($info->downloadmd5)) {
1934 $impediments['missingdownloadmd5'] = true;
1937 if (!empty($info->download) and !$this->update_downloadable($info->download)) {
1938 $impediments['notdownloadable'] = true;
1941 if (!$this->component_writable($info->component)) {
1942 $impediments['notwritable'] = true;
1945 return $impediments;
1949 * Check to see if the current version of the plugin seems to be a checkout of an external repository.
1951 * @see plugin_manager::plugin_external_source()
1952 * @param available_update_info $info
1953 * @return false|string
1955 public function plugin_external_source(available_update_info $info) {
1957 $paths = get_plugin_types(true);
1958 list($plugintype, $pluginname) = normalize_component($info->component);
1959 $pluginroot = $paths[$plugintype].'/'.$pluginname;
1961 if (is_dir($pluginroot.'/.git')) {
1962 return 'git';
1965 if (is_dir($pluginroot.'/CVS')) {
1966 return 'cvs';
1969 if (is_dir($pluginroot.'/.svn')) {
1970 return 'svn';
1973 return false;
1977 * Prepares a renderable widget to confirm installation of an available update.
1979 * @param available_update_info $info component version to deploy
1980 * @return renderable
1982 public function make_confirm_widget(available_update_info $info) {
1984 if (!$this->initialized()) {
1985 throw new coding_exception('Illegal method call - deployer not initialized.');
1988 $params = $this->data_to_params(array(
1989 'updateinfo' => (array)$info, // see http://www.php.net/manual/en/language.types.array.php#language.types.array.casting
1992 $widget = new single_button(
1993 new moodle_url($this->callerurl, $params),
1994 get_string('updateavailableinstall', 'core_admin'),
1995 'post'
1998 return $widget;
2002 * Prepares a renderable widget to execute installation of an available update.
2004 * @param available_update_info $info component version to deploy
2005 * @param moodle_url $returnurl URL to return after the installation execution
2006 * @return renderable
2008 public function make_execution_widget(available_update_info $info, moodle_url $returnurl = null) {
2009 global $CFG;
2011 if (!$this->initialized()) {
2012 throw new coding_exception('Illegal method call - deployer not initialized.');
2015 $pluginrootpaths = get_plugin_types(true);
2017 list($plugintype, $pluginname) = normalize_component($info->component);
2019 if (empty($pluginrootpaths[$plugintype])) {
2020 throw new coding_exception('Unknown plugin type root location', $plugintype);
2023 list($passfile, $password) = $this->prepare_authorization();
2025 if (is_null($returnurl)) {
2026 $returnurl = new moodle_url('/admin');
2027 } else {
2028 $returnurl = $returnurl;
2031 $params = array(
2032 'upgrade' => true,
2033 'type' => $plugintype,
2034 'name' => $pluginname,
2035 'typeroot' => $pluginrootpaths[$plugintype],
2036 'package' => $info->download,
2037 'md5' => $info->downloadmd5,
2038 'dataroot' => $CFG->dataroot,
2039 'dirroot' => $CFG->dirroot,
2040 'passfile' => $passfile,
2041 'password' => $password,
2042 'returnurl' => $returnurl->out(false),
2045 if (!empty($CFG->proxyhost)) {
2046 // MDL-36973 - Beware - we should call just !is_proxybypass() here. But currently, our
2047 // cURL wrapper class does not do it. So, to have consistent behaviour, we pass proxy
2048 // setting regardless the $CFG->proxybypass setting. Once the {@link curl} class is
2049 // fixed, the condition should be amended.
2050 if (true or !is_proxybypass($info->download)) {
2051 if (empty($CFG->proxyport)) {
2052 $params['proxy'] = $CFG->proxyhost;
2053 } else {
2054 $params['proxy'] = $CFG->proxyhost.':'.$CFG->proxyport;
2057 if (!empty($CFG->proxyuser) and !empty($CFG->proxypassword)) {
2058 $params['proxyuserpwd'] = $CFG->proxyuser.':'.$CFG->proxypassword;
2061 if (!empty($CFG->proxytype)) {
2062 $params['proxytype'] = $CFG->proxytype;
2067 $widget = new single_button(
2068 new moodle_url('/mdeploy.php', $params),
2069 get_string('updateavailableinstall', 'core_admin'),
2070 'post'
2073 return $widget;
2077 * Returns array of data objects passed to this tool.
2079 * @return array
2081 public function submitted_data() {
2083 $data = $this->params_to_data($_POST);
2085 if (empty($data) or empty($data[self::HTTP_PARAM_CHECKER])) {
2086 return false;
2089 if (!empty($data['updateinfo']) and is_object($data['updateinfo'])) {
2090 $updateinfo = $data['updateinfo'];
2091 if (!empty($updateinfo->component) and !empty($updateinfo->version)) {
2092 $data['updateinfo'] = new available_update_info($updateinfo->component, (array)$updateinfo);
2096 if (!empty($data['callerurl'])) {
2097 $data['callerurl'] = new moodle_url($data['callerurl']);
2100 if (!empty($data['returnurl'])) {
2101 $data['returnurl'] = new moodle_url($data['returnurl']);
2104 return $data;
2108 * Handles magic getters and setters for protected properties.
2110 * @param string $name method name, e.g. set_returnurl()
2111 * @param array $arguments arguments to be passed to the array
2113 public function __call($name, array $arguments = array()) {
2115 if (substr($name, 0, 4) === 'set_') {
2116 $property = substr($name, 4);
2117 if (empty($property)) {
2118 throw new coding_exception('Invalid property name (empty)');
2120 if (empty($arguments)) {
2121 $arguments = array(true); // Default value for flag-like properties.
2123 // Make sure it is a protected property.
2124 $isprotected = false;
2125 $reflection = new ReflectionObject($this);
2126 foreach ($reflection->getProperties(ReflectionProperty::IS_PROTECTED) as $reflectionproperty) {
2127 if ($reflectionproperty->getName() === $property) {
2128 $isprotected = true;
2129 break;
2132 if (!$isprotected) {
2133 throw new coding_exception('Unable to set property - it does not exist or it is not protected');
2135 $value = reset($arguments);
2136 $this->$property = $value;
2137 return;
2140 if (substr($name, 0, 4) === 'get_') {
2141 $property = substr($name, 4);
2142 if (empty($property)) {
2143 throw new coding_exception('Invalid property name (empty)');
2145 if (!empty($arguments)) {
2146 throw new coding_exception('No parameter expected');
2148 // Make sure it is a protected property.
2149 $isprotected = false;
2150 $reflection = new ReflectionObject($this);
2151 foreach ($reflection->getProperties(ReflectionProperty::IS_PROTECTED) as $reflectionproperty) {
2152 if ($reflectionproperty->getName() === $property) {
2153 $isprotected = true;
2154 break;
2157 if (!$isprotected) {
2158 throw new coding_exception('Unable to get property - it does not exist or it is not protected');
2160 return $this->$property;
2165 * Generates a random token and stores it in a file in moodledata directory.
2167 * @return array of the (string)filename and (string)password in this order
2169 public function prepare_authorization() {
2170 global $CFG;
2172 make_upload_directory('mdeploy/auth/');
2174 $attempts = 0;
2175 $success = false;
2177 while (!$success and $attempts < 5) {
2178 $attempts++;
2180 $passfile = $this->generate_passfile();
2181 $password = $this->generate_password();
2182 $now = time();
2184 $filepath = $CFG->dataroot.'/mdeploy/auth/'.$passfile;
2186 if (!file_exists($filepath)) {
2187 $success = file_put_contents($filepath, $password . PHP_EOL . $now . PHP_EOL, LOCK_EX);
2191 if ($success) {
2192 return array($passfile, $password);
2194 } else {
2195 throw new moodle_exception('unable_prepare_authorization', 'core_plugin');
2199 // End of external API
2202 * Prepares an array of HTTP parameters that can be passed to another page.
2204 * @param array|object $data associative array or an object holding the data, data JSON-able
2205 * @return array suitable as a param for moodle_url
2207 protected function data_to_params($data) {
2209 // Append some our own data
2210 if (!empty($this->callerurl)) {
2211 $data['callerurl'] = $this->callerurl->out(false);
2213 if (!empty($this->returnurl)) {
2214 $data['returnurl'] = $this->returnurl->out(false);
2217 // Finally append the count of items in the package.
2218 $data[self::HTTP_PARAM_CHECKER] = count($data);
2220 // Generate params
2221 $params = array();
2222 foreach ($data as $name => $value) {
2223 $transname = self::HTTP_PARAM_PREFIX.$name;
2224 $transvalue = json_encode($value);
2225 $params[$transname] = $transvalue;
2228 return $params;
2232 * Converts HTTP parameters passed to the script into native PHP data
2234 * @param array $params such as $_REQUEST or $_POST
2235 * @return array data passed for this class
2237 protected function params_to_data(array $params) {
2239 if (empty($params)) {
2240 return array();
2243 $data = array();
2244 foreach ($params as $name => $value) {
2245 if (strpos($name, self::HTTP_PARAM_PREFIX) === 0) {
2246 $realname = substr($name, strlen(self::HTTP_PARAM_PREFIX));
2247 $realvalue = json_decode($value);
2248 $data[$realname] = $realvalue;
2252 return $data;
2256 * Returns a random string to be used as a filename of the password storage.
2258 * @return string
2260 protected function generate_passfile() {
2261 return clean_param(uniqid('mdeploy_', true), PARAM_FILE);
2265 * Returns a random string to be used as the authorization token
2267 * @return string
2269 protected function generate_password() {
2270 return complex_random_string();
2274 * Checks if the given component's directory is writable
2276 * For the purpose of the deployment, the web server process has to have
2277 * write access to all files in the component's directory (recursively) and for the
2278 * directory itself.
2280 * @see worker::move_directory_source_precheck()
2281 * @param string $component normalized component name
2282 * @return boolean
2284 protected function component_writable($component) {
2286 list($plugintype, $pluginname) = normalize_component($component);
2288 $directory = get_plugin_directory($plugintype, $pluginname);
2290 if (is_null($directory)) {
2291 throw new coding_exception('Unknown component location', $component);
2294 return $this->directory_writable($directory);
2298 * Checks if the mdeploy.php will be able to fetch the ZIP from the given URL
2300 * This is mainly supposed to check if the transmission over HTTPS would
2301 * work. That is, if the CA certificates are present at the server.
2303 * @param string $downloadurl the URL of the ZIP package to download
2304 * @return bool
2306 protected function update_downloadable($downloadurl) {
2307 global $CFG;
2309 $curloptions = array(
2310 'CURLOPT_SSL_VERIFYHOST' => 2, // this is the default in {@link curl} class but just in case
2311 'CURLOPT_SSL_VERIFYPEER' => true,
2314 $curl = new curl(array('proxy' => true));
2315 $result = $curl->head($downloadurl, $curloptions);
2316 $errno = $curl->get_errno();
2317 if (empty($errno)) {
2318 return true;
2319 } else {
2320 return false;
2325 * Checks if the directory and all its contents (recursively) is writable
2327 * @param string $path full path to a directory
2328 * @return boolean
2330 private function directory_writable($path) {
2332 if (!is_writable($path)) {
2333 return false;
2336 if (is_dir($path)) {
2337 $handle = opendir($path);
2338 } else {
2339 return false;
2342 $result = true;
2344 while ($filename = readdir($handle)) {
2345 $filepath = $path.'/'.$filename;
2347 if ($filename === '.' or $filename === '..') {
2348 continue;
2351 if (is_dir($filepath)) {
2352 $result = $result && $this->directory_writable($filepath);
2354 } else {
2355 $result = $result && is_writable($filepath);
2359 closedir($handle);
2361 return $result;
2367 * Factory class producing required subclasses of {@link plugininfo_base}
2369 class plugininfo_default_factory {
2372 * Makes a new instance of the plugininfo class
2374 * @param string $type the plugin type, eg. 'mod'
2375 * @param string $typerootdir full path to the location of all the plugins of this type
2376 * @param string $name the plugin name, eg. 'workshop'
2377 * @param string $namerootdir full path to the location of the plugin
2378 * @param string $typeclass the name of class that holds the info about the plugin
2379 * @return plugininfo_base the instance of $typeclass
2381 public static function make($type, $typerootdir, $name, $namerootdir, $typeclass) {
2382 $plugin = new $typeclass();
2383 $plugin->type = $type;
2384 $plugin->typerootdir = $typerootdir;
2385 $plugin->name = $name;
2386 $plugin->rootdir = $namerootdir;
2388 $plugin->init_display_name();
2389 $plugin->load_disk_version();
2390 $plugin->load_db_version();
2391 $plugin->load_required_main_version();
2392 $plugin->init_is_standard();
2394 return $plugin;
2400 * Base class providing access to the information about a plugin
2402 * @property-read string component the component name, type_name
2404 abstract class plugininfo_base {
2406 /** @var string the plugintype name, eg. mod, auth or workshopform */
2407 public $type;
2408 /** @var string full path to the location of all the plugins of this type */
2409 public $typerootdir;
2410 /** @var string the plugin name, eg. assignment, ldap */
2411 public $name;
2412 /** @var string the localized plugin name */
2413 public $displayname;
2414 /** @var string the plugin source, one of plugin_manager::PLUGIN_SOURCE_xxx constants */
2415 public $source;
2416 /** @var fullpath to the location of this plugin */
2417 public $rootdir;
2418 /** @var int|string the version of the plugin's source code */
2419 public $versiondisk;
2420 /** @var int|string the version of the installed plugin */
2421 public $versiondb;
2422 /** @var int|float|string required version of Moodle core */
2423 public $versionrequires;
2424 /** @var array other plugins that this one depends on, lazy-loaded by {@link get_other_required_plugins()} */
2425 public $dependencies;
2426 /** @var int number of instances of the plugin - not supported yet */
2427 public $instances;
2428 /** @var int order of the plugin among other plugins of the same type - not supported yet */
2429 public $sortorder;
2430 /** @var array|null array of {@link available_update_info} for this plugin */
2431 public $availableupdates;
2434 * Gathers and returns the information about all plugins of the given type
2436 * @param string $type the name of the plugintype, eg. mod, auth or workshopform
2437 * @param string $typerootdir full path to the location of the plugin dir
2438 * @param string $typeclass the name of the actually called class
2439 * @return array of plugintype classes, indexed by the plugin name
2441 public static function get_plugins($type, $typerootdir, $typeclass) {
2443 // get the information about plugins at the disk
2444 $plugins = get_plugin_list($type);
2445 $ondisk = array();
2446 foreach ($plugins as $pluginname => $pluginrootdir) {
2447 $ondisk[$pluginname] = plugininfo_default_factory::make($type, $typerootdir,
2448 $pluginname, $pluginrootdir, $typeclass);
2450 return $ondisk;
2454 * Sets {@link $displayname} property to a localized name of the plugin
2456 public function init_display_name() {
2457 if (!get_string_manager()->string_exists('pluginname', $this->component)) {
2458 $this->displayname = '[pluginname,' . $this->component . ']';
2459 } else {
2460 $this->displayname = get_string('pluginname', $this->component);
2465 * Magic method getter, redirects to read only values.
2467 * @param string $name
2468 * @return mixed
2470 public function __get($name) {
2471 switch ($name) {
2472 case 'component': return $this->type . '_' . $this->name;
2474 default:
2475 debugging('Invalid plugin property accessed! '.$name);
2476 return null;
2481 * Return the full path name of a file within the plugin.
2483 * No check is made to see if the file exists.
2485 * @param string $relativepath e.g. 'version.php'.
2486 * @return string e.g. $CFG->dirroot . '/mod/quiz/version.php'.
2488 public function full_path($relativepath) {
2489 if (empty($this->rootdir)) {
2490 return '';
2492 return $this->rootdir . '/' . $relativepath;
2496 * Load the data from version.php.
2498 * @param bool $disablecache do not attempt to obtain data from the cache
2499 * @return stdClass the object called $plugin defined in version.php
2501 protected function load_version_php($disablecache=false) {
2503 $cache = cache::make('core', 'plugininfo_base');
2505 $versionsphp = $cache->get('versions_php');
2507 if (!$disablecache and $versionsphp !== false and isset($versionsphp[$this->component])) {
2508 return $versionsphp[$this->component];
2511 $versionfile = $this->full_path('version.php');
2513 $plugin = new stdClass();
2514 if (is_readable($versionfile)) {
2515 include($versionfile);
2517 $versionsphp[$this->component] = $plugin;
2518 $cache->set('versions_php', $versionsphp);
2520 return $plugin;
2524 * Sets {@link $versiondisk} property to a numerical value representing the
2525 * version of the plugin's source code.
2527 * If the value is null after calling this method, either the plugin
2528 * does not use versioning (typically does not have any database
2529 * data) or is missing from disk.
2531 public function load_disk_version() {
2532 $plugin = $this->load_version_php();
2533 if (isset($plugin->version)) {
2534 $this->versiondisk = $plugin->version;
2539 * Sets {@link $versionrequires} property to a numerical value representing
2540 * the version of Moodle core that this plugin requires.
2542 public function load_required_main_version() {
2543 $plugin = $this->load_version_php();
2544 if (isset($plugin->requires)) {
2545 $this->versionrequires = $plugin->requires;
2550 * Initialise {@link $dependencies} to the list of other plugins (in any)
2551 * that this one requires to be installed.
2553 protected function load_other_required_plugins() {
2554 $plugin = $this->load_version_php();
2555 if (!empty($plugin->dependencies)) {
2556 $this->dependencies = $plugin->dependencies;
2557 } else {
2558 $this->dependencies = array(); // By default, no dependencies.
2563 * Get the list of other plugins that this plugin requires to be installed.
2565 * @return array with keys the frankenstyle plugin name, and values either
2566 * a version string (like '2011101700') or the constant ANY_VERSION.
2568 public function get_other_required_plugins() {
2569 if (is_null($this->dependencies)) {
2570 $this->load_other_required_plugins();
2572 return $this->dependencies;
2576 * Is this is a subplugin?
2578 * @return boolean
2580 public function is_subplugin() {
2581 return ($this->get_parent_plugin() !== false);
2585 * If I am a subplugin, return the name of my parent plugin.
2587 * @return string|bool false if not a subplugin, name of the parent otherwise
2589 public function get_parent_plugin() {
2590 return $this->get_plugin_manager()->get_parent_of_subplugin($this->type);
2594 * Sets {@link $versiondb} property to a numerical value representing the
2595 * currently installed version of the plugin.
2597 * If the value is null after calling this method, either the plugin
2598 * does not use versioning (typically does not have any database
2599 * data) or has not been installed yet.
2601 public function load_db_version() {
2602 if ($ver = self::get_version_from_config_plugins($this->component)) {
2603 $this->versiondb = $ver;
2608 * Sets {@link $source} property to one of plugin_manager::PLUGIN_SOURCE_xxx
2609 * constants.
2611 * If the property's value is null after calling this method, then
2612 * the type of the plugin has not been recognized and you should throw
2613 * an exception.
2615 public function init_is_standard() {
2617 $standard = plugin_manager::standard_plugins_list($this->type);
2619 if ($standard !== false) {
2620 $standard = array_flip($standard);
2621 if (isset($standard[$this->name])) {
2622 $this->source = plugin_manager::PLUGIN_SOURCE_STANDARD;
2623 } else if (!is_null($this->versiondb) and is_null($this->versiondisk)
2624 and plugin_manager::is_deleted_standard_plugin($this->type, $this->name)) {
2625 $this->source = plugin_manager::PLUGIN_SOURCE_STANDARD; // to be deleted
2626 } else {
2627 $this->source = plugin_manager::PLUGIN_SOURCE_EXTENSION;
2633 * Returns true if the plugin is shipped with the official distribution
2634 * of the current Moodle version, false otherwise.
2636 * @return bool
2638 public function is_standard() {
2639 return $this->source === plugin_manager::PLUGIN_SOURCE_STANDARD;
2643 * Returns true if the the given Moodle version is enough to run this plugin
2645 * @param string|int|double $moodleversion
2646 * @return bool
2648 public function is_core_dependency_satisfied($moodleversion) {
2650 if (empty($this->versionrequires)) {
2651 return true;
2653 } else {
2654 return (double)$this->versionrequires <= (double)$moodleversion;
2659 * Returns the status of the plugin
2661 * @return string one of plugin_manager::PLUGIN_STATUS_xxx constants
2663 public function get_status() {
2665 if (is_null($this->versiondb) and is_null($this->versiondisk)) {
2666 return plugin_manager::PLUGIN_STATUS_NODB;
2668 } else if (is_null($this->versiondb) and !is_null($this->versiondisk)) {
2669 return plugin_manager::PLUGIN_STATUS_NEW;
2671 } else if (!is_null($this->versiondb) and is_null($this->versiondisk)) {
2672 if (plugin_manager::is_deleted_standard_plugin($this->type, $this->name)) {
2673 return plugin_manager::PLUGIN_STATUS_DELETE;
2674 } else {
2675 return plugin_manager::PLUGIN_STATUS_MISSING;
2678 } else if ((string)$this->versiondb === (string)$this->versiondisk) {
2679 return plugin_manager::PLUGIN_STATUS_UPTODATE;
2681 } else if ($this->versiondb < $this->versiondisk) {
2682 return plugin_manager::PLUGIN_STATUS_UPGRADE;
2684 } else if ($this->versiondb > $this->versiondisk) {
2685 return plugin_manager::PLUGIN_STATUS_DOWNGRADE;
2687 } else {
2688 // $version = pi(); and similar funny jokes - hopefully Donald E. Knuth will never contribute to Moodle ;-)
2689 throw new coding_exception('Unable to determine plugin state, check the plugin versions');
2694 * Returns the information about plugin availability
2696 * True means that the plugin is enabled. False means that the plugin is
2697 * disabled. Null means that the information is not available, or the
2698 * plugin does not support configurable availability or the availability
2699 * can not be changed.
2701 * @return null|bool
2703 public function is_enabled() {
2704 return null;
2708 * Populates the property {@link $availableupdates} with the information provided by
2709 * available update checker
2711 * @param available_update_checker $provider the class providing the available update info
2713 public function check_available_updates(available_update_checker $provider) {
2714 global $CFG;
2716 if (isset($CFG->updateminmaturity)) {
2717 $minmaturity = $CFG->updateminmaturity;
2718 } else {
2719 // this can happen during the very first upgrade to 2.3
2720 $minmaturity = MATURITY_STABLE;
2723 $this->availableupdates = $provider->get_update_info($this->component,
2724 array('minmaturity' => $minmaturity));
2728 * If there are updates for this plugin available, returns them.
2730 * Returns array of {@link available_update_info} objects, if some update
2731 * is available. Returns null if there is no update available or if the update
2732 * availability is unknown.
2734 * @return array|null
2736 public function available_updates() {
2738 if (empty($this->availableupdates) or !is_array($this->availableupdates)) {
2739 return null;
2742 $updates = array();
2744 foreach ($this->availableupdates as $availableupdate) {
2745 if ($availableupdate->version > $this->versiondisk) {
2746 $updates[] = $availableupdate;
2750 if (empty($updates)) {
2751 return null;
2754 return $updates;
2758 * Returns the node name used in admin settings menu for this plugin settings (if applicable)
2760 * @return null|string node name or null if plugin does not create settings node (default)
2762 public function get_settings_section_name() {
2763 return null;
2767 * Returns the URL of the plugin settings screen
2769 * Null value means that the plugin either does not have the settings screen
2770 * or its location is not available via this library.
2772 * @return null|moodle_url
2774 public function get_settings_url() {
2775 $section = $this->get_settings_section_name();
2776 if ($section === null) {
2777 return null;
2779 $settings = admin_get_root()->locate($section);
2780 if ($settings && $settings instanceof admin_settingpage) {
2781 return new moodle_url('/admin/settings.php', array('section' => $section));
2782 } else if ($settings && $settings instanceof admin_externalpage) {
2783 return new moodle_url($settings->url);
2784 } else {
2785 return null;
2790 * Loads plugin settings to the settings tree
2792 * This function usually includes settings.php file in plugins folder.
2793 * Alternatively it can create a link to some settings page (instance of admin_externalpage)
2795 * @param part_of_admin_tree $adminroot
2796 * @param string $parentnodename
2797 * @param bool $hassiteconfig whether the current user has moodle/site:config capability
2799 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
2803 * Should there be a way to uninstall the plugin via the administration UI
2805 * By default, uninstallation is allowed for all non-standard add-ons. Subclasses
2806 * may want to override this to allow uninstallation of all plugins (simply by
2807 * returning true unconditionally). Subplugins follow their parent plugin's
2808 * decision by default.
2810 * Note that even if true is returned, the core may still prohibit the uninstallation,
2811 * e.g. in case there are other plugins that depend on this one.
2813 * @return boolean
2815 public function is_uninstall_allowed() {
2817 if ($this->is_subplugin()) {
2818 return $this->get_plugin_manager()->get_plugin_info($this->get_parent_plugin())->is_uninstall_allowed();
2821 if ($this->is_standard()) {
2822 return false;
2825 return true;
2829 * Returns the URL of the screen where this plugin can be uninstalled
2831 * Visiting that URL must be safe, that is a manual confirmation is needed
2832 * for actual uninstallation of the plugin. By default, URL to a common
2833 * uninstalling tool is returned.
2835 * @return moodle_url
2837 public function get_uninstall_url() {
2838 return $this->get_default_uninstall_url();
2842 * Returns relative directory of the plugin with heading '/'
2844 * @return string
2846 public function get_dir() {
2847 global $CFG;
2849 return substr($this->rootdir, strlen($CFG->dirroot));
2853 * Hook method to implement certain steps when uninstalling the plugin.
2855 * This hook is called by {@link plugin_manager::uninstall_plugin()} so
2856 * it is basically usable only for those plugin types that use the default
2857 * uninstall tool provided by {@link self::get_default_uninstall_url()}.
2859 * @param progress_trace $progress traces the process
2860 * @return bool true on success, false on failure
2862 public function uninstall(progress_trace $progress) {
2863 return true;
2867 * Returns URL to a script that handles common plugin uninstall procedure.
2869 * This URL is suitable for plugins that do not have their own UI
2870 * for uninstalling.
2872 * @return moodle_url
2874 protected final function get_default_uninstall_url() {
2875 return new moodle_url('/admin/plugins.php', array(
2876 'sesskey' => sesskey(),
2877 'uninstall' => $this->component,
2878 'confirm' => 0,
2883 * Provides access to plugin versions from the {config_plugins} table
2885 * @param string $plugin plugin name
2886 * @param bool $disablecache do not attempt to obtain data from the cache
2887 * @return int|bool the stored value or false if not found
2889 protected function get_version_from_config_plugins($plugin, $disablecache=false) {
2890 global $DB;
2892 $cache = cache::make('core', 'plugininfo_base');
2894 $pluginversions = $cache->get('versions_db');
2896 if ($pluginversions === false or $disablecache) {
2897 try {
2898 $pluginversions = $DB->get_records_menu('config_plugins', array('name' => 'version'), 'plugin', 'plugin,value');
2899 } catch (dml_exception $e) {
2900 // before install
2901 $pluginversions = array();
2903 $cache->set('versions_db', $pluginversions);
2906 if (isset($pluginversions[$plugin])) {
2907 return $pluginversions[$plugin];
2908 } else {
2909 return false;
2914 * Provides access to the plugin_manager singleton.
2916 * @return plugin_manmager
2918 protected function get_plugin_manager() {
2919 return plugin_manager::instance();
2925 * General class for all plugin types that do not have their own class
2927 class plugininfo_general extends plugininfo_base {
2932 * Class for page side blocks
2934 class plugininfo_block extends plugininfo_base {
2936 public static function get_plugins($type, $typerootdir, $typeclass) {
2938 // get the information about blocks at the disk
2939 $blocks = parent::get_plugins($type, $typerootdir, $typeclass);
2941 // add blocks missing from disk
2942 $blocksinfo = self::get_blocks_info();
2943 foreach ($blocksinfo as $blockname => $blockinfo) {
2944 if (isset($blocks[$blockname])) {
2945 continue;
2947 $plugin = new $typeclass();
2948 $plugin->type = $type;
2949 $plugin->typerootdir = $typerootdir;
2950 $plugin->name = $blockname;
2951 $plugin->rootdir = null;
2952 $plugin->displayname = $blockname;
2953 $plugin->versiondb = $blockinfo->version;
2954 $plugin->init_is_standard();
2956 $blocks[$blockname] = $plugin;
2959 return $blocks;
2963 * Magic method getter, redirects to read only values.
2965 * For block plugins pretends the object has 'visible' property for compatibility
2966 * with plugins developed for Moodle version below 2.4
2968 * @param string $name
2969 * @return mixed
2971 public function __get($name) {
2972 if ($name === 'visible') {
2973 debugging('This is now an instance of plugininfo_block, please use $block->is_enabled() instead of $block->visible', DEBUG_DEVELOPER);
2974 return ($this->is_enabled() !== false);
2976 return parent::__get($name);
2979 public function init_display_name() {
2981 if (get_string_manager()->string_exists('pluginname', 'block_' . $this->name)) {
2982 $this->displayname = get_string('pluginname', 'block_' . $this->name);
2984 } else if (($block = block_instance($this->name)) !== false) {
2985 $this->displayname = $block->get_title();
2987 } else {
2988 parent::init_display_name();
2992 public function load_db_version() {
2993 global $DB;
2995 $blocksinfo = self::get_blocks_info();
2996 if (isset($blocksinfo[$this->name]->version)) {
2997 $this->versiondb = $blocksinfo[$this->name]->version;
3001 public function is_enabled() {
3003 $blocksinfo = self::get_blocks_info();
3004 if (isset($blocksinfo[$this->name]->visible)) {
3005 if ($blocksinfo[$this->name]->visible) {
3006 return true;
3007 } else {
3008 return false;
3010 } else {
3011 return parent::is_enabled();
3015 public function get_settings_section_name() {
3016 return 'blocksetting' . $this->name;
3019 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3020 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3021 $ADMIN = $adminroot; // may be used in settings.php
3022 $block = $this; // also can be used inside settings.php
3023 $section = $this->get_settings_section_name();
3025 if (!$hassiteconfig || (($blockinstance = block_instance($this->name)) === false)) {
3026 return;
3029 $settings = null;
3030 if ($blockinstance->has_config()) {
3031 if (file_exists($this->full_path('settings.php'))) {
3032 $settings = new admin_settingpage($section, $this->displayname,
3033 'moodle/site:config', $this->is_enabled() === false);
3034 include($this->full_path('settings.php')); // this may also set $settings to null
3035 } else {
3036 $blocksinfo = self::get_blocks_info();
3037 $settingsurl = new moodle_url('/admin/block.php', array('block' => $blocksinfo[$this->name]->id));
3038 $settings = new admin_externalpage($section, $this->displayname,
3039 $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
3042 if ($settings) {
3043 $ADMIN->add($parentnodename, $settings);
3047 public function is_uninstall_allowed() {
3048 return true;
3051 public function get_uninstall_url() {
3052 $blocksinfo = self::get_blocks_info();
3053 return new moodle_url('/admin/blocks.php', array('delete' => $blocksinfo[$this->name]->id, 'sesskey' => sesskey()));
3057 * Provides access to the records in {block} table
3059 * @param bool $disablecache do not attempt to obtain data from the cache
3060 * @return array array of stdClasses
3062 protected static function get_blocks_info($disablecache=false) {
3063 global $DB;
3065 $cache = cache::make('core', 'plugininfo_block');
3067 $blocktypes = $cache->get('blocktypes');
3069 if ($blocktypes === false or $disablecache) {
3070 try {
3071 $blocktypes = $DB->get_records('block', null, 'name', 'name,id,version,visible');
3072 } catch (dml_exception $e) {
3073 // before install
3074 $blocktypes = array();
3076 $cache->set('blocktypes', $blocktypes);
3079 return $blocktypes;
3085 * Class for text filters
3087 class plugininfo_filter extends plugininfo_base {
3089 public static function get_plugins($type, $typerootdir, $typeclass) {
3090 global $CFG, $DB;
3092 $filters = array();
3094 // get the list of filters in /filter location
3095 $installed = filter_get_all_installed();
3097 foreach ($installed as $name => $displayname) {
3098 $plugin = new $typeclass();
3099 $plugin->type = $type;
3100 $plugin->typerootdir = $typerootdir;
3101 $plugin->name = $name;
3102 $plugin->rootdir = "$CFG->dirroot/filter/$name";
3103 $plugin->displayname = $displayname;
3105 $plugin->load_disk_version();
3106 $plugin->load_db_version();
3107 $plugin->load_required_main_version();
3108 $plugin->init_is_standard();
3110 $filters[$plugin->name] = $plugin;
3113 // Do not mess with filter registration here!
3115 $globalstates = self::get_global_states();
3117 // make sure that all registered filters are installed, just in case
3118 foreach ($globalstates as $name => $info) {
3119 if (!isset($filters[$name])) {
3120 // oops, there is a record in filter_active but the filter is not installed
3121 $plugin = new $typeclass();
3122 $plugin->type = $type;
3123 $plugin->typerootdir = $typerootdir;
3124 $plugin->name = $name;
3125 $plugin->rootdir = "$CFG->dirroot/filter/$name";
3126 $plugin->displayname = $name;
3128 $plugin->load_db_version();
3130 if (is_null($plugin->versiondb)) {
3131 // this is a hack to stimulate 'Missing from disk' error
3132 // because $plugin->versiondisk will be null !== false
3133 $plugin->versiondb = false;
3136 $filters[$plugin->name] = $plugin;
3140 return $filters;
3143 public function init_display_name() {
3144 // do nothing, the name is set in self::get_plugins()
3147 public function is_enabled() {
3149 $globalstates = self::get_global_states();
3151 foreach ($globalstates as $name => $info) {
3152 if ($name === $this->name) {
3153 if ($info->active == TEXTFILTER_DISABLED) {
3154 return false;
3155 } else {
3156 // it may be 'On' or 'Off, but available'
3157 return null;
3162 return null;
3165 public function get_settings_section_name() {
3166 return 'filtersetting' . $this->name;
3169 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3170 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3171 $ADMIN = $adminroot; // may be used in settings.php
3172 $filter = $this; // also can be used inside settings.php
3174 $settings = null;
3175 if ($hassiteconfig && file_exists($this->full_path('filtersettings.php'))) {
3176 $section = $this->get_settings_section_name();
3177 $settings = new admin_settingpage($section, $this->displayname,
3178 'moodle/site:config', $this->is_enabled() === false);
3179 include($this->full_path('filtersettings.php')); // this may also set $settings to null
3181 if ($settings) {
3182 $ADMIN->add($parentnodename, $settings);
3186 public function is_uninstall_allowed() {
3187 return true;
3190 public function get_uninstall_url() {
3191 return new moodle_url('/admin/filters.php', array('sesskey' => sesskey(), 'filterpath' => $this->name, 'action' => 'delete'));
3195 * Provides access to the results of {@link filter_get_global_states()}
3196 * but indexed by the normalized filter name
3198 * The legacy filter name is available as ->legacyname property.
3200 * @param bool $disablecache do not attempt to obtain data from the cache
3201 * @return array
3203 protected static function get_global_states($disablecache=false) {
3204 global $DB;
3206 $cache = cache::make('core', 'plugininfo_filter');
3208 $globalstates = $cache->get('globalstates');
3210 if ($globalstates === false or $disablecache) {
3212 if (!$DB->get_manager()->table_exists('filter_active')) {
3213 // Not installed yet.
3214 $cache->set('globalstates', array());
3215 return array();
3218 $globalstates = array();
3220 foreach (filter_get_global_states() as $name => $info) {
3221 if (strpos($name, '/') !== false) {
3222 // Skip existing before upgrade to new names.
3223 continue;
3226 $filterinfo = new stdClass();
3227 $filterinfo->active = $info->active;
3228 $filterinfo->sortorder = $info->sortorder;
3229 $globalstates[$name] = $filterinfo;
3232 $cache->set('globalstates', $globalstates);
3235 return $globalstates;
3241 * Class for activity modules
3243 class plugininfo_mod extends plugininfo_base {
3245 public static function get_plugins($type, $typerootdir, $typeclass) {
3247 // get the information about plugins at the disk
3248 $modules = parent::get_plugins($type, $typerootdir, $typeclass);
3250 // add modules missing from disk
3251 $modulesinfo = self::get_modules_info();
3252 foreach ($modulesinfo as $modulename => $moduleinfo) {
3253 if (isset($modules[$modulename])) {
3254 continue;
3256 $plugin = new $typeclass();
3257 $plugin->type = $type;
3258 $plugin->typerootdir = $typerootdir;
3259 $plugin->name = $modulename;
3260 $plugin->rootdir = null;
3261 $plugin->displayname = $modulename;
3262 $plugin->versiondb = $moduleinfo->version;
3263 $plugin->init_is_standard();
3265 $modules[$modulename] = $plugin;
3268 return $modules;
3272 * Magic method getter, redirects to read only values.
3274 * For module plugins we pretend the object has 'visible' property for compatibility
3275 * with plugins developed for Moodle version below 2.4
3277 * @param string $name
3278 * @return mixed
3280 public function __get($name) {
3281 if ($name === 'visible') {
3282 debugging('This is now an instance of plugininfo_mod, please use $module->is_enabled() instead of $module->visible', DEBUG_DEVELOPER);
3283 return ($this->is_enabled() !== false);
3285 return parent::__get($name);
3288 public function init_display_name() {
3289 if (get_string_manager()->string_exists('pluginname', $this->component)) {
3290 $this->displayname = get_string('pluginname', $this->component);
3291 } else {
3292 $this->displayname = get_string('modulename', $this->component);
3297 * Load the data from version.php.
3299 * @param bool $disablecache do not attempt to obtain data from the cache
3300 * @return object the data object defined in version.php.
3302 protected function load_version_php($disablecache=false) {
3304 $cache = cache::make('core', 'plugininfo_mod');
3306 $versionsphp = $cache->get('versions_php');
3308 if (!$disablecache and $versionsphp !== false and isset($versionsphp[$this->component])) {
3309 return $versionsphp[$this->component];
3312 $versionfile = $this->full_path('version.php');
3314 $module = new stdClass();
3315 $plugin = new stdClass();
3316 if (is_readable($versionfile)) {
3317 include($versionfile);
3319 if (!isset($module->version) and isset($plugin->version)) {
3320 $module = $plugin;
3322 $versionsphp[$this->component] = $module;
3323 $cache->set('versions_php', $versionsphp);
3325 return $module;
3328 public function load_db_version() {
3329 global $DB;
3331 $modulesinfo = self::get_modules_info();
3332 if (isset($modulesinfo[$this->name]->version)) {
3333 $this->versiondb = $modulesinfo[$this->name]->version;
3337 public function is_enabled() {
3339 $modulesinfo = self::get_modules_info();
3340 if (isset($modulesinfo[$this->name]->visible)) {
3341 if ($modulesinfo[$this->name]->visible) {
3342 return true;
3343 } else {
3344 return false;
3346 } else {
3347 return parent::is_enabled();
3351 public function get_settings_section_name() {
3352 return 'modsetting' . $this->name;
3355 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3356 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3357 $ADMIN = $adminroot; // may be used in settings.php
3358 $module = $this; // also can be used inside settings.php
3359 $section = $this->get_settings_section_name();
3361 $modulesinfo = self::get_modules_info();
3362 $settings = null;
3363 if ($hassiteconfig && isset($modulesinfo[$this->name]) && file_exists($this->full_path('settings.php'))) {
3364 $settings = new admin_settingpage($section, $this->displayname,
3365 'moodle/site:config', $this->is_enabled() === false);
3366 include($this->full_path('settings.php')); // this may also set $settings to null
3368 if ($settings) {
3369 $ADMIN->add($parentnodename, $settings);
3374 * Allow all activity modules but Forum to be uninstalled.
3376 * This exception for the Forum has been hard-coded in Moodle since ages,
3377 * we may want to re-think it one day.
3379 public function is_uninstall_allowed() {
3380 if ($this->name === 'forum') {
3381 return false;
3382 } else {
3383 return true;
3387 public function get_uninstall_url() {
3388 return new moodle_url('/admin/modules.php', array('delete' => $this->name, 'sesskey' => sesskey()));
3392 * Provides access to the records in {modules} table
3394 * @param bool $disablecache do not attempt to obtain data from the cache
3395 * @return array array of stdClasses
3397 protected static function get_modules_info($disablecache=false) {
3398 global $DB;
3400 $cache = cache::make('core', 'plugininfo_mod');
3402 $modulesinfo = $cache->get('modulesinfo');
3404 if ($modulesinfo === false or $disablecache) {
3405 try {
3406 $modulesinfo = $DB->get_records('modules', null, 'name', 'name,id,version,visible');
3407 } catch (dml_exception $e) {
3408 // before install
3409 $modulesinfo = array();
3411 $cache->set('modulesinfo', $modulesinfo);
3414 return $modulesinfo;
3420 * Class for question behaviours.
3422 class plugininfo_qbehaviour extends plugininfo_base {
3424 public function is_uninstall_allowed() {
3425 return true;
3428 public function get_uninstall_url() {
3429 return new moodle_url('/admin/qbehaviours.php',
3430 array('delete' => $this->name, 'sesskey' => sesskey()));
3436 * Class for question types
3438 class plugininfo_qtype extends plugininfo_base {
3440 public function is_uninstall_allowed() {
3441 return true;
3444 public function get_uninstall_url() {
3445 return new moodle_url('/admin/qtypes.php',
3446 array('delete' => $this->name, 'sesskey' => sesskey()));
3449 public function get_settings_section_name() {
3450 return 'qtypesetting' . $this->name;
3453 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3454 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3455 $ADMIN = $adminroot; // may be used in settings.php
3456 $qtype = $this; // also can be used inside settings.php
3457 $section = $this->get_settings_section_name();
3459 $settings = null;
3460 $systemcontext = context_system::instance();
3461 if (($hassiteconfig || has_capability('moodle/question:config', $systemcontext)) &&
3462 file_exists($this->full_path('settings.php'))) {
3463 $settings = new admin_settingpage($section, $this->displayname,
3464 'moodle/question:config', $this->is_enabled() === false);
3465 include($this->full_path('settings.php')); // this may also set $settings to null
3467 if ($settings) {
3468 $ADMIN->add($parentnodename, $settings);
3475 * Class for authentication plugins
3477 class plugininfo_auth extends plugininfo_base {
3479 public function is_enabled() {
3480 global $CFG;
3482 if (in_array($this->name, array('nologin', 'manual'))) {
3483 // these two are always enabled and can't be disabled
3484 return null;
3487 $enabled = array_flip(explode(',', $CFG->auth));
3489 return isset($enabled[$this->name]);
3492 public function get_settings_section_name() {
3493 return 'authsetting' . $this->name;
3496 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3497 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3498 $ADMIN = $adminroot; // may be used in settings.php
3499 $auth = $this; // also to be used inside settings.php
3500 $section = $this->get_settings_section_name();
3502 $settings = null;
3503 if ($hassiteconfig) {
3504 if (file_exists($this->full_path('settings.php'))) {
3505 // TODO: finish implementation of common settings - locking, etc.
3506 $settings = new admin_settingpage($section, $this->displayname,
3507 'moodle/site:config', $this->is_enabled() === false);
3508 include($this->full_path('settings.php')); // this may also set $settings to null
3509 } else {
3510 $settingsurl = new moodle_url('/admin/auth_config.php', array('auth' => $this->name));
3511 $settings = new admin_externalpage($section, $this->displayname,
3512 $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
3515 if ($settings) {
3516 $ADMIN->add($parentnodename, $settings);
3523 * Class for enrolment plugins
3525 class plugininfo_enrol extends plugininfo_base {
3527 public function is_enabled() {
3528 global $CFG;
3530 // We do not actually need whole enrolment classes here so we do not call
3531 // {@link enrol_get_plugins()}. Note that this may produce slightly different
3532 // results, for example if the enrolment plugin does not contain lib.php
3533 // but it is listed in $CFG->enrol_plugins_enabled
3535 $enabled = array_flip(explode(',', $CFG->enrol_plugins_enabled));
3537 return isset($enabled[$this->name]);
3540 public function get_settings_section_name() {
3541 if (file_exists($this->full_path('settings.php'))) {
3542 return 'enrolsettings' . $this->name;
3543 } else {
3544 return null;
3548 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3549 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3551 if (!$hassiteconfig or !file_exists($this->full_path('settings.php'))) {
3552 return;
3554 $section = $this->get_settings_section_name();
3556 $ADMIN = $adminroot; // may be used in settings.php
3557 $enrol = $this; // also can be used inside settings.php
3558 $settings = new admin_settingpage($section, $this->displayname,
3559 'moodle/site:config', $this->is_enabled() === false);
3561 include($this->full_path('settings.php')); // This may also set $settings to null!
3563 if ($settings) {
3564 $ADMIN->add($parentnodename, $settings);
3568 public function is_uninstall_allowed() {
3569 return true;
3572 public function get_uninstall_url() {
3573 return new moodle_url('/admin/enrol.php', array('action' => 'uninstall', 'enrol' => $this->name, 'sesskey' => sesskey()));
3579 * Class for messaging processors
3581 class plugininfo_message extends plugininfo_base {
3583 public function get_settings_section_name() {
3584 return 'messagesetting' . $this->name;
3587 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3588 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3589 $ADMIN = $adminroot; // may be used in settings.php
3590 if (!$hassiteconfig) {
3591 return;
3593 $section = $this->get_settings_section_name();
3595 $settings = null;
3596 $processors = get_message_processors();
3597 if (isset($processors[$this->name])) {
3598 $processor = $processors[$this->name];
3599 if ($processor->available && $processor->hassettings) {
3600 $settings = new admin_settingpage($section, $this->displayname,
3601 'moodle/site:config', $this->is_enabled() === false);
3602 include($this->full_path('settings.php')); // this may also set $settings to null
3605 if ($settings) {
3606 $ADMIN->add($parentnodename, $settings);
3611 * @see plugintype_interface::is_enabled()
3613 public function is_enabled() {
3614 $processors = get_message_processors();
3615 if (isset($processors[$this->name])) {
3616 return $processors[$this->name]->configured && $processors[$this->name]->enabled;
3617 } else {
3618 return parent::is_enabled();
3622 public function is_uninstall_allowed() {
3623 $processors = get_message_processors();
3624 if (isset($processors[$this->name])) {
3625 return true;
3626 } else {
3627 return false;
3632 * @see plugintype_interface::get_uninstall_url()
3634 public function get_uninstall_url() {
3635 $processors = get_message_processors();
3636 return new moodle_url('/admin/message.php', array('uninstall' => $processors[$this->name]->id, 'sesskey' => sesskey()));
3642 * Class for repositories
3644 class plugininfo_repository extends plugininfo_base {
3646 public function is_enabled() {
3648 $enabled = self::get_enabled_repositories();
3650 return isset($enabled[$this->name]);
3653 public function get_settings_section_name() {
3654 return 'repositorysettings'.$this->name;
3657 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3658 if ($hassiteconfig && $this->is_enabled()) {
3659 // completely no access to repository setting when it is not enabled
3660 $sectionname = $this->get_settings_section_name();
3661 $settingsurl = new moodle_url('/admin/repository.php',
3662 array('sesskey' => sesskey(), 'action' => 'edit', 'repos' => $this->name));
3663 $settings = new admin_externalpage($sectionname, $this->displayname,
3664 $settingsurl, 'moodle/site:config', false);
3665 $adminroot->add($parentnodename, $settings);
3670 * Provides access to the records in {repository} table
3672 * @param bool $disablecache do not attempt to obtain data from the cache
3673 * @return array array of stdClasses
3675 protected static function get_enabled_repositories($disablecache=false) {
3676 global $DB;
3678 $cache = cache::make('core', 'plugininfo_repository');
3680 $enabled = $cache->get('enabled');
3682 if ($enabled === false or $disablecache) {
3683 $enabled = $DB->get_records('repository', null, 'type', 'type,visible,sortorder');
3684 $cache->set('enabled', $enabled);
3687 return $enabled;
3693 * Class for portfolios
3695 class plugininfo_portfolio extends plugininfo_base {
3697 public function is_enabled() {
3699 $enabled = self::get_enabled_portfolios();
3701 return isset($enabled[$this->name]);
3705 * Returns list of enabled portfolio plugins
3707 * Portfolio plugin is enabled if there is at least one record in the {portfolio_instance}
3708 * table for it.
3710 * @param bool $disablecache do not attempt to obtain data from the cache
3711 * @return array array of stdClasses with properties plugin and visible indexed by plugin
3713 protected static function get_enabled_portfolios($disablecache=false) {
3714 global $DB;
3716 $cache = cache::make('core', 'plugininfo_portfolio');
3718 $enabled = $cache->get('enabled');
3720 if ($enabled === false or $disablecache) {
3721 $enabled = array();
3722 $instances = $DB->get_recordset('portfolio_instance', null, '', 'plugin,visible');
3723 foreach ($instances as $instance) {
3724 if (isset($enabled[$instance->plugin])) {
3725 if ($instance->visible) {
3726 $enabled[$instance->plugin]->visible = $instance->visible;
3728 } else {
3729 $enabled[$instance->plugin] = $instance;
3732 $instances->close();
3733 $cache->set('enabled', $enabled);
3736 return $enabled;
3742 * Class for themes
3744 class plugininfo_theme extends plugininfo_base {
3746 public function is_enabled() {
3747 global $CFG;
3749 if ((!empty($CFG->theme) and $CFG->theme === $this->name) or
3750 (!empty($CFG->themelegacy) and $CFG->themelegacy === $this->name)) {
3751 return true;
3752 } else {
3753 return parent::is_enabled();
3760 * Class representing an MNet service
3762 class plugininfo_mnetservice extends plugininfo_base {
3764 public function is_enabled() {
3765 global $CFG;
3767 if (empty($CFG->mnet_dispatcher_mode) || $CFG->mnet_dispatcher_mode !== 'strict') {
3768 return false;
3769 } else {
3770 return parent::is_enabled();
3777 * Class for admin tool plugins
3779 class plugininfo_tool extends plugininfo_base {
3781 public function is_uninstall_allowed() {
3782 return true;
3785 public function get_uninstall_url() {
3786 return new moodle_url('/admin/tools.php', array('delete' => $this->name, 'sesskey' => sesskey()));
3792 * Class for admin tool plugins
3794 class plugininfo_report extends plugininfo_base {
3796 public function is_uninstall_allowed() {
3797 return true;
3800 public function get_uninstall_url() {
3801 return new moodle_url('/admin/reports.php', array('delete' => $this->name, 'sesskey' => sesskey()));
3807 * Class for local plugins
3809 class plugininfo_local extends plugininfo_base {
3811 public function get_uninstall_url() {
3812 return new moodle_url('/admin/localplugins.php', array('delete' => $this->name, 'sesskey' => sesskey()));
3817 * Class for HTML editors
3819 class plugininfo_editor extends plugininfo_base {
3821 public function get_settings_section_name() {
3822 return 'editorsettings' . $this->name;
3825 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3826 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3827 $ADMIN = $adminroot; // may be used in settings.php
3828 $editor = $this; // also can be used inside settings.php
3829 $section = $this->get_settings_section_name();
3831 $settings = null;
3832 if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3833 $settings = new admin_settingpage($section, $this->displayname,
3834 'moodle/site:config', $this->is_enabled() === false);
3835 include($this->full_path('settings.php')); // this may also set $settings to null
3837 if ($settings) {
3838 $ADMIN->add($parentnodename, $settings);
3843 * Returns the information about plugin availability
3845 * True means that the plugin is enabled. False means that the plugin is
3846 * disabled. Null means that the information is not available, or the
3847 * plugin does not support configurable availability or the availability
3848 * can not be changed.
3850 * @return null|bool
3852 public function is_enabled() {
3853 global $CFG;
3854 if (empty($CFG->texteditors)) {
3855 $CFG->texteditors = 'tinymce,textarea';
3857 if (in_array($this->name, explode(',', $CFG->texteditors))) {
3858 return true;
3860 return false;
3865 * Class for plagiarism plugins
3867 class plugininfo_plagiarism extends plugininfo_base {
3869 public function get_settings_section_name() {
3870 return 'plagiarism'. $this->name;
3873 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3874 // plagiarism plugin just redirect to settings.php in the plugins directory
3875 if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3876 $section = $this->get_settings_section_name();
3877 $settingsurl = new moodle_url($this->get_dir().'/settings.php');
3878 $settings = new admin_externalpage($section, $this->displayname,
3879 $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
3880 $adminroot->add($parentnodename, $settings);
3886 * Class for webservice protocols
3888 class plugininfo_webservice extends plugininfo_base {
3890 public function get_settings_section_name() {
3891 return 'webservicesetting' . $this->name;
3894 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3895 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3896 $ADMIN = $adminroot; // may be used in settings.php
3897 $webservice = $this; // also can be used inside settings.php
3898 $section = $this->get_settings_section_name();
3900 $settings = null;
3901 if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3902 $settings = new admin_settingpage($section, $this->displayname,
3903 'moodle/site:config', $this->is_enabled() === false);
3904 include($this->full_path('settings.php')); // this may also set $settings to null
3906 if ($settings) {
3907 $ADMIN->add($parentnodename, $settings);
3911 public function is_enabled() {
3912 global $CFG;
3913 if (empty($CFG->enablewebservices)) {
3914 return false;
3916 $active_webservices = empty($CFG->webserviceprotocols) ? array() : explode(',', $CFG->webserviceprotocols);
3917 if (in_array($this->name, $active_webservices)) {
3918 return true;
3920 return false;
3923 public function is_uninstall_allowed() {
3924 return true;
3927 public function get_uninstall_url() {
3928 return new moodle_url('/admin/webservice/protocols.php',
3929 array('sesskey' => sesskey(), 'action' => 'uninstall', 'webservice' => $this->name));
3934 * Class for course formats
3936 class plugininfo_format extends plugininfo_base {
3939 * Gathers and returns the information about all plugins of the given type
3941 * @param string $type the name of the plugintype, eg. mod, auth or workshopform
3942 * @param string $typerootdir full path to the location of the plugin dir
3943 * @param string $typeclass the name of the actually called class
3944 * @return array of plugintype classes, indexed by the plugin name
3946 public static function get_plugins($type, $typerootdir, $typeclass) {
3947 global $CFG;
3948 $formats = parent::get_plugins($type, $typerootdir, $typeclass);
3949 require_once($CFG->dirroot.'/course/lib.php');
3950 $order = get_sorted_course_formats();
3951 $sortedformats = array();
3952 foreach ($order as $formatname) {
3953 $sortedformats[$formatname] = $formats[$formatname];
3955 return $sortedformats;
3958 public function get_settings_section_name() {
3959 return 'formatsetting' . $this->name;
3962 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3963 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3964 $ADMIN = $adminroot; // also may be used in settings.php
3965 $section = $this->get_settings_section_name();
3967 $settings = null;
3968 if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3969 $settings = new admin_settingpage($section, $this->displayname,
3970 'moodle/site:config', $this->is_enabled() === false);
3971 include($this->full_path('settings.php')); // this may also set $settings to null
3973 if ($settings) {
3974 $ADMIN->add($parentnodename, $settings);
3978 public function is_enabled() {
3979 return !get_config($this->component, 'disabled');
3982 public function is_uninstall_allowed() {
3983 if ($this->name !== get_config('moodlecourse', 'format') && $this->name !== 'site') {
3984 return true;
3985 } else {
3986 return false;
3990 public function get_uninstall_url() {
3991 return new moodle_url('/admin/courseformats.php',
3992 array('sesskey' => sesskey(), 'action' => 'uninstall', 'format' => $this->name));