3 // This file is part of Moodle - http://moodle.org/
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
19 * Defines classes used for plugins management
21 * This library provides a unified interface to various plugin types in
22 * Moodle. It is mainly used by the plugins management admin page and the
23 * plugins check page during the upgrade.
27 * @copyright 2011 David Mudrak <david@moodle.com>
28 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
31 defined('MOODLE_INTERNAL') ||
die();
34 * Singleton class providing general plugins management functionality
36 class plugin_manager
{
38 /** the plugin is shipped with standard Moodle distribution */
39 const PLUGIN_SOURCE_STANDARD
= 'std';
40 /** the plugin is added extension */
41 const PLUGIN_SOURCE_EXTENSION
= 'ext';
43 /** the plugin uses neither database nor capabilities, no versions */
44 const PLUGIN_STATUS_NODB
= 'nodb';
45 /** the plugin is up-to-date */
46 const PLUGIN_STATUS_UPTODATE
= 'uptodate';
47 /** the plugin is about to be installed */
48 const PLUGIN_STATUS_NEW
= 'new';
49 /** the plugin is about to be upgraded */
50 const PLUGIN_STATUS_UPGRADE
= 'upgrade';
51 /** the standard plugin is about to be deleted */
52 const PLUGIN_STATUS_DELETE
= 'delete';
53 /** the version at the disk is lower than the one already installed */
54 const PLUGIN_STATUS_DOWNGRADE
= 'downgrade';
55 /** the plugin is installed but missing from disk */
56 const PLUGIN_STATUS_MISSING
= 'missing';
58 /** @var plugin_manager holds the singleton instance */
59 protected static $singletoninstance;
60 /** @var array of raw plugins information */
61 protected $pluginsinfo = null;
62 /** @var array of raw subplugins information */
63 protected $subpluginsinfo = null;
66 * Direct initiation not allowed, use the factory method {@link self::instance()}
68 protected function __construct() {
72 * Sorry, this is singleton
74 protected function __clone() {
78 * Factory method for this class
80 * @return plugin_manager the singleton instance
82 public static function instance() {
83 if (is_null(self
::$singletoninstance)) {
84 self
::$singletoninstance = new self();
86 return self
::$singletoninstance;
91 * @param bool $phpunitreset
93 public static function reset_caches($phpunitreset = false) {
95 self
::$singletoninstance = null;
100 * Returns the result of {@link core_component::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(core_component
::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])) {
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) {
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 (core_component
::get_plugin_types_with_subplugins() as $type => $ignored) {
147 foreach (core_component
::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;
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)) {
203 $subplugins = $this->get_subplugins($disablecache);
205 if (!isset($subplugins[$pluginfo->component
])) {
211 foreach ($subplugins[$pluginfo->component
] as $subdata) {
212 foreach ($this->get_plugins_of_type($subdata->type
) as $subpluginfo) {
213 $list[$subpluginfo->component
] = $subpluginfo;
221 * Returns list of plugins that define their subplugins and the information
222 * about them from the db/subplugins.php file.
224 * @param bool $disablecache force reload, cache can be used otherwise
225 * @return array with keys like 'mod_quiz', and values the data from the
226 * corresponding db/subplugins.php file.
228 public function get_subplugins($disablecache=false) {
230 if ($disablecache or is_null($this->subpluginsinfo
)) {
231 $this->subpluginsinfo
= array();
232 foreach (core_component
::get_plugin_types_with_subplugins() as $type => $ignored) {
233 foreach (core_component
::get_plugin_list($type) as $component => $ownerdir) {
234 $componentsubplugins = array();
235 if (file_exists($ownerdir . '/db/subplugins.php')) {
236 $subplugins = array();
237 include($ownerdir . '/db/subplugins.php');
238 foreach ($subplugins as $subplugintype => $subplugintyperootdir) {
239 $subplugin = new stdClass();
240 $subplugin->type
= $subplugintype;
241 $subplugin->typerootdir
= $subplugintyperootdir;
242 $componentsubplugins[$subplugintype] = $subplugin;
244 $this->subpluginsinfo
[$type . '_' . $component] = $componentsubplugins;
250 return $this->subpluginsinfo
;
254 * Returns the name of the plugin that defines the given subplugin type
256 * If the given subplugin type is not actually a subplugin, returns false.
258 * @param string $subplugintype the name of subplugin type, eg. workshopform or quiz
259 * @return false|string the name of the parent plugin, eg. mod_workshop
261 public function get_parent_of_subplugin($subplugintype) {
264 foreach ($this->get_subplugins() as $pluginname => $subplugintypes) {
265 if (isset($subplugintypes[$subplugintype])) {
266 $parent = $pluginname;
275 * Returns a localized name of a given plugin
277 * @param string $component name of the plugin, eg mod_workshop or auth_ldap
280 public function plugin_name($component) {
282 $pluginfo = $this->get_plugin_info($component);
284 if (is_null($pluginfo)) {
285 throw new moodle_exception('err_unknown_plugin', 'core_plugin', '', array('plugin' => $component));
288 return $pluginfo->displayname
;
292 * Returns a localized name of a plugin typed in singular form
294 * Most plugin types define their names in core_plugin lang file. In case of subplugins,
295 * we try to ask the parent plugin for the name. In the worst case, we will return
296 * the value of the passed $type parameter.
298 * @param string $type the type of the plugin, e.g. mod or workshopform
301 public function plugintype_name($type) {
303 if (get_string_manager()->string_exists('type_' . $type, 'core_plugin')) {
304 // for most plugin types, their names are defined in core_plugin lang file
305 return get_string('type_' . $type, 'core_plugin');
307 } else if ($parent = $this->get_parent_of_subplugin($type)) {
308 // if this is a subplugin, try to ask the parent plugin for the name
309 if (get_string_manager()->string_exists('subplugintype_' . $type, $parent)) {
310 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type, $parent);
312 return $this->plugin_name($parent) . ' / ' . $type;
321 * Returns a localized name of a plugin type in plural form
323 * Most plugin types define their names in core_plugin lang file. In case of subplugins,
324 * we try to ask the parent plugin for the name. In the worst case, we will return
325 * the value of the passed $type parameter.
327 * @param string $type the type of the plugin, e.g. mod or workshopform
330 public function plugintype_name_plural($type) {
332 if (get_string_manager()->string_exists('type_' . $type . '_plural', 'core_plugin')) {
333 // for most plugin types, their names are defined in core_plugin lang file
334 return get_string('type_' . $type . '_plural', 'core_plugin');
336 } else if ($parent = $this->get_parent_of_subplugin($type)) {
337 // if this is a subplugin, try to ask the parent plugin for the name
338 if (get_string_manager()->string_exists('subplugintype_' . $type . '_plural', $parent)) {
339 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type . '_plural', $parent);
341 return $this->plugin_name($parent) . ' / ' . $type;
350 * Returns information about the known plugin, or null
352 * @param string $component frankenstyle component name.
353 * @param bool $disablecache force reload, cache can be used otherwise
354 * @return plugininfo_base|null the corresponding plugin information.
356 public function get_plugin_info($component, $disablecache=false) {
357 list($type, $name) = $this->normalize_component($component);
358 $plugins = $this->get_plugins($disablecache);
359 if (isset($plugins[$type][$name])) {
360 return $plugins[$type][$name];
367 * Check to see if the current version of the plugin seems to be a checkout of an external repository.
369 * @see available_update_deployer::plugin_external_source()
370 * @param string $component frankenstyle component name
371 * @return false|string
373 public function plugin_external_source($component) {
375 $plugininfo = $this->get_plugin_info($component);
377 if (is_null($plugininfo)) {
381 $pluginroot = $plugininfo->rootdir
;
383 if (is_dir($pluginroot.'/.git')) {
387 if (is_dir($pluginroot.'/CVS')) {
391 if (is_dir($pluginroot.'/.svn')) {
399 * Get a list of any other plugins that require this one.
400 * @param string $component frankenstyle component name.
401 * @return array of frankensyle component names that require this one.
403 public function other_plugins_that_require($component) {
405 foreach ($this->get_plugins() as $type => $plugins) {
406 foreach ($plugins as $plugin) {
407 $required = $plugin->get_other_required_plugins();
408 if (isset($required[$component])) {
409 $others[] = $plugin->component
;
417 * Check a dependencies list against the list of installed plugins.
418 * @param array $dependencies compenent name to required version or ANY_VERSION.
419 * @return bool true if all the dependencies are satisfied.
421 public function are_dependencies_satisfied($dependencies) {
422 foreach ($dependencies as $component => $requiredversion) {
423 $otherplugin = $this->get_plugin_info($component);
424 if (is_null($otherplugin)) {
428 if ($requiredversion != ANY_VERSION
and $otherplugin->versiondisk
< $requiredversion) {
437 * Checks all dependencies for all installed plugins
439 * This is used by install and upgrade. The array passed by reference as the second
440 * argument is populated with the list of plugins that have failed dependencies (note that
441 * a single plugin can appear multiple times in the $failedplugins).
443 * @param int $moodleversion the version from version.php.
444 * @param array $failedplugins to return the list of plugins with non-satisfied dependencies
445 * @return bool true if all the dependencies are satisfied for all plugins.
447 public function all_plugins_ok($moodleversion, &$failedplugins = array()) {
450 foreach ($this->get_plugins() as $type => $plugins) {
451 foreach ($plugins as $plugin) {
453 if (!$plugin->is_core_dependency_satisfied($moodleversion)) {
455 $failedplugins[] = $plugin->component
;
458 if (!$this->are_dependencies_satisfied($plugin->get_other_required_plugins())) {
460 $failedplugins[] = $plugin->component
;
469 * Is it possible to uninstall the given plugin?
471 * False is returned if the plugininfo subclass declares the uninstall should
472 * not be allowed via {@link plugininfo_base::is_uninstall_allowed()} or if the
473 * core vetoes it (e.g. becase the plugin or some of its subplugins is required
474 * by some other installed plugin).
476 * @param string $component full frankenstyle name, e.g. mod_foobar
479 public function can_uninstall_plugin($component) {
481 $pluginfo = $this->get_plugin_info($component);
483 if (is_null($pluginfo)) {
487 if (!$this->common_uninstall_check($pluginfo)) {
491 // If it has subplugins, check they can be uninstalled too.
492 $subplugins = $this->get_subplugins_of_plugin($pluginfo->component
);
493 foreach ($subplugins as $subpluginfo) {
494 if (!$this->common_uninstall_check($subpluginfo)) {
497 // Check if there are some other plugins requiring this subplugin
498 // (but the parent and siblings).
499 foreach ($this->other_plugins_that_require($subpluginfo->component
) as $requiresme) {
500 $ismyparent = ($pluginfo->component
=== $requiresme);
501 $ismysibling = in_array($requiresme, array_keys($subplugins));
502 if (!$ismyparent and !$ismysibling) {
508 // Check if there are some other plugins requiring this plugin
509 // (but its subplugins).
510 foreach ($this->other_plugins_that_require($pluginfo->component
) as $requiresme) {
511 $ismysubplugin = in_array($requiresme, array_keys($subplugins));
512 if (!$ismysubplugin) {
521 * Returns uninstall URL if exists.
523 * @param string $component
524 * @return moodle_url uninstall URL, null if uninstall not supported
526 public function get_uninstall_url($component) {
527 if (!$this->can_uninstall_plugin($component)) {
531 $pluginfo = $this->get_plugin_info($component);
533 if (is_null($pluginfo)) {
537 return $pluginfo->get_uninstall_url();
541 * Uninstall the given plugin.
543 * Automatically cleans-up all remaining configuration data, log records, events,
544 * files from the file pool etc.
546 * In the future, the functionality of {@link uninstall_plugin()} function may be moved
547 * into this method and all the code should be refactored to use it. At the moment, we
548 * mimic this future behaviour by wrapping that function call.
550 * @param string $component
551 * @param progress_trace $progress traces the process
552 * @return bool true on success, false on errors/problems
554 public function uninstall_plugin($component, progress_trace
$progress) {
556 $pluginfo = $this->get_plugin_info($component);
558 if (is_null($pluginfo)) {
562 // Give the pluginfo class a chance to execute some steps.
563 $result = $pluginfo->uninstall($progress);
568 // Call the legacy core function to uninstall the plugin.
570 uninstall_plugin($pluginfo->type
, $pluginfo->name
);
571 $progress->output(ob_get_clean());
577 * Checks if there are some plugins with a known available update
579 * @return bool true if there is at least one available update
581 public function some_plugins_updatable() {
582 foreach ($this->get_plugins() as $type => $plugins) {
583 foreach ($plugins as $plugin) {
584 if ($plugin->available_updates()) {
594 * Check to see if the given plugin folder can be removed by the web server process.
596 * @param string $component full frankenstyle component
599 public function is_plugin_folder_removable($component) {
601 $pluginfo = $this->get_plugin_info($component);
603 if (is_null($pluginfo)) {
607 // To be able to remove the plugin folder, its parent must be writable, too.
608 if (!is_writable(dirname($pluginfo->rootdir
))) {
612 // Check that the folder and all its content is writable (thence removable).
613 return $this->is_directory_removable($pluginfo->rootdir
);
617 * Defines a list of all plugins that were originally shipped in the standard Moodle distribution,
618 * but are not anymore and are deleted during upgrades.
620 * The main purpose of this list is to hide missing plugins during upgrade.
622 * @param string $type plugin type
623 * @param string $name plugin name
626 public static function is_deleted_standard_plugin($type, $name) {
628 // Example of the array structure:
630 // 'block' => array('admin', 'admin_tree'),
631 // 'mod' => array('assignment'),
633 // Do not include plugins that were removed during upgrades to versions that are
634 // not supported as source versions for upgrade any more. For example, at MOODLE_23_STABLE
635 // branch, listed should be no plugins that were removed at 1.9.x - 2.1.x versions as
636 // Moodle 2.3 supports upgrades from 2.2.x only.
638 'qformat' => array('blackboard'),
639 'enrol' => array('authorize'),
642 if (!isset($plugins[$type])) {
645 return in_array($name, $plugins[$type]);
649 * Defines a white list of all plugins shipped in the standard Moodle distribution
651 * @param string $type
652 * @return false|array array of standard plugins or false if the type is unknown
654 public static function standard_plugins_list($type) {
656 $standard_plugins = array(
658 'assignment' => array(
659 'offline', 'online', 'upload', 'uploadsingle'
662 'assignsubmission' => array(
663 'comments', 'file', 'onlinetext'
666 'assignfeedback' => array(
667 'comments', 'file', 'offline'
671 'bold', 'clear', 'html', 'image', 'indent', 'italic', 'link',
672 'media', 'orderedlist', 'outdent', 'strike', 'title',
673 'underline', 'unlink', 'unorderedlist'
677 'cas', 'db', 'email', 'fc', 'imap', 'ldap', 'manual', 'mnet',
678 'nntp', 'nologin', 'none', 'pam', 'pop3', 'radius',
679 'shibboleth', 'webservice'
683 'activity_modules', 'admin_bookmarks', 'badges', 'blog_menu',
684 'blog_recent', 'blog_tags', 'calendar_month',
685 'calendar_upcoming', 'comments', 'community',
686 'completionstatus', 'course_list', 'course_overview',
687 'course_summary', 'feedback', 'glossary_random', 'html',
688 'login', 'mentees', 'messages', 'mnet_hosts', 'myprofile',
689 'navigation', 'news_items', 'online_users', 'participants',
690 'private_files', 'quiz_results', 'recent_activity',
691 'rss_client', 'search_forums', 'section_links',
692 'selfcompletion', 'settings', 'site_main_menu',
693 'social_activities', 'tag_flickr', 'tag_youtube', 'tags'
697 'exportimscp', 'importhtml', 'print'
700 'cachelock' => array(
704 'cachestore' => array(
705 'file', 'memcache', 'memcached', 'mongodb', 'session', 'static'
708 'calendartype' => array(
712 'coursereport' => array(
716 'datafield' => array(
717 'checkbox', 'date', 'file', 'latlong', 'menu', 'multimenu',
718 'number', 'picture', 'radiobutton', 'text', 'textarea', 'url'
721 'datapreset' => array(
726 'textarea', 'tinymce', 'atto'
730 'category', 'cohort', 'database', 'flatfile',
731 'guest', 'imsenterprise', 'ldap', 'manual', 'meta', 'mnet',
736 'activitynames', 'algebra', 'censor', 'emailprotect',
737 'emoticon', 'mediaplugin', 'multilang', 'tex', 'tidy',
738 'urltolink', 'data', 'glossary'
742 'singleactivity', 'social', 'topics', 'weeks'
745 'gradeexport' => array(
746 'ods', 'txt', 'xls', 'xml'
749 'gradeimport' => array(
753 'gradereport' => array(
754 'grader', 'outcomes', 'overview', 'user'
757 'gradingform' => array(
765 'email', 'jabber', 'popup'
768 'mnetservice' => array(
773 'assign', 'assignment', 'book', 'chat', 'choice', 'data', 'feedback', 'folder',
774 'forum', 'glossary', 'imscp', 'label', 'lesson', 'lti', 'page',
775 'quiz', 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop'
778 'plagiarism' => array(
781 'portfolio' => array(
782 'boxnet', 'download', 'flickr', 'googledocs', 'mahara', 'picasa'
785 'profilefield' => array(
786 'checkbox', 'datetime', 'menu', 'text', 'textarea'
789 'qbehaviour' => array(
790 'adaptive', 'adaptivenopenalty', 'deferredcbm',
791 'deferredfeedback', 'immediatecbm', 'immediatefeedback',
792 'informationitem', 'interactive', 'interactivecountback',
793 'manualgraded', 'missing'
797 'aiken', 'blackboard_six', 'examview', 'gift',
798 'learnwise', 'missingword', 'multianswer', 'webct',
803 'calculated', 'calculatedmulti', 'calculatedsimple',
804 'description', 'essay', 'match', 'missingtype', 'multianswer',
805 'multichoice', 'numerical', 'random', 'randomsamatch',
806 'shortanswer', 'truefalse'
810 'grading', 'overview', 'responses', 'statistics'
813 'quizaccess' => array(
814 'delaybetweenattempts', 'ipaddress', 'numattempts', 'openclosedate',
815 'password', 'safebrowser', 'securewindow', 'timelimit'
819 'backups', 'completion', 'configlog', 'courseoverview',
820 'log', 'loglive', 'outline', 'participation', 'progress', 'questioninstances', 'security', 'stats', 'performance'
823 'repository' => array(
824 'alfresco', 'areafiles', 'boxnet', 'coursefiles', 'dropbox', 'equella', 'filesystem',
825 'flickr', 'flickr_public', 'googledocs', 'local', 'merlot',
826 'picasa', 'recent', 'skydrive', 's3', 'upload', 'url', 'user', 'webdav',
827 'wikimedia', 'youtube'
830 'scormreport' => array(
838 'ctrlhelp', 'dragmath', 'managefiles', 'moodleemoticon', 'moodleimage',
839 'moodlemedia', 'moodlenolink', 'pdw', 'spellchecker', 'wrap'
843 'afterburner', 'anomaly', 'arialist', 'base', 'binarius', 'bootstrapbase',
844 'boxxie', 'brick', 'canvas', 'clean', 'formal_white', 'formfactor',
845 'fusion', 'leatherbound', 'magazine', 'mymobile', 'nimble',
846 'nonzero', 'overlay', 'serenity', 'sky_high', 'splash',
847 'standard', 'standardold'
851 'assignmentupgrade', 'behat', 'capability', 'customlang',
852 'dbtransfer', 'generator', 'health', 'innodb', 'installaddon',
853 'langimport', 'multilangupgrade', 'phpunit', 'profiling',
854 'qeupgradehelper', 'replace', 'spamcleaner', 'timezoneimport',
855 'unittest', 'uploadcourse', 'uploaduser', 'unsuproles', 'xmldb'
858 'webservice' => array(
859 'amf', 'rest', 'soap', 'xmlrpc'
862 'workshopallocation' => array(
863 'manual', 'random', 'scheduled'
866 'workshopeval' => array(
870 'workshopform' => array(
871 'accumulative', 'comments', 'numerrors', 'rubric'
875 if (isset($standard_plugins[$type])) {
876 return $standard_plugins[$type];
883 * Wrapper for the core function {@link core_component::normalize_component()}.
885 * This is here just to make it possible to mock it in unit tests.
887 * @param string $component
890 protected function normalize_component($component) {
891 return core_component
::normalize_component($component);
895 * Reorders plugin types into a sequence to be displayed
897 * For technical reasons, plugin types returned by {@link core_component::get_plugin_types()} are
898 * in a certain order that does not need to fit the expected order for the display.
899 * Particularly, activity modules should be displayed first as they represent the
900 * real heart of Moodle. They should be followed by other plugin types that are
901 * used to build the courses (as that is what one expects from LMS). After that,
902 * other supportive plugin types follow.
904 * @param array $types associative array
905 * @return array same array with altered order of items
907 protected function reorder_plugin_types(array $types) {
909 'mod' => $types['mod'],
910 'block' => $types['block'],
911 'qtype' => $types['qtype'],
912 'qbehaviour' => $types['qbehaviour'],
913 'qformat' => $types['qformat'],
914 'filter' => $types['filter'],
915 'enrol' => $types['enrol'],
917 foreach ($types as $type => $path) {
918 if (!isset($fix[$type])) {
926 * Check if the given directory can be removed by the web server process.
928 * This recursively checks that the given directory and all its contents
931 * @param string $fullpath
934 protected function is_directory_removable($fullpath) {
936 if (!is_writable($fullpath)) {
940 if (is_dir($fullpath)) {
941 $handle = opendir($fullpath);
948 while ($filename = readdir($handle)) {
950 if ($filename === '.' or $filename === '..') {
954 $subfilepath = $fullpath.'/'.$filename;
956 if (is_dir($subfilepath)) {
957 $result = $result && $this->is_directory_removable($subfilepath);
960 $result = $result && is_writable($subfilepath);
970 * Helper method that implements common uninstall prerequisities
972 * @param plugininfo_base $pluginfo
975 protected function common_uninstall_check(plugininfo_base
$pluginfo) {
977 if (!$pluginfo->is_uninstall_allowed()) {
978 // The plugin's plugininfo class declares it should not be uninstalled.
982 if ($pluginfo->get_status() === plugin_manager
::PLUGIN_STATUS_NEW
) {
983 // The plugin is not installed. It should be either installed or removed from the disk.
984 // Relying on this temporary state may be tricky.
988 if (is_null($pluginfo->get_uninstall_url())) {
989 // Backwards compatibility.
990 debugging('plugininfo_base subclasses should use is_uninstall_allowed() instead of returning null in get_uninstall_url()',
1001 * General exception thrown by the {@link available_update_checker} class
1003 class available_update_checker_exception
extends moodle_exception
{
1006 * @param string $errorcode exception description identifier
1007 * @param mixed $debuginfo debugging data to display
1009 public function __construct($errorcode, $debuginfo=null) {
1010 parent
::__construct($errorcode, 'core_plugin', '', null, print_r($debuginfo, true));
1016 * Singleton class that handles checking for available updates
1018 class available_update_checker
{
1020 /** @var available_update_checker holds the singleton instance */
1021 protected static $singletoninstance;
1022 /** @var null|int the timestamp of when the most recent response was fetched */
1023 protected $recentfetch = null;
1024 /** @var null|array the recent response from the update notification provider */
1025 protected $recentresponse = null;
1026 /** @var null|string the numerical version of the local Moodle code */
1027 protected $currentversion = null;
1028 /** @var null|string the release info of the local Moodle code */
1029 protected $currentrelease = null;
1030 /** @var null|string branch of the local Moodle code */
1031 protected $currentbranch = null;
1032 /** @var array of (string)frankestyle => (string)version list of additional plugins deployed at this site */
1033 protected $currentplugins = array();
1036 * Direct initiation not allowed, use the factory method {@link self::instance()}
1038 protected function __construct() {
1042 * Sorry, this is singleton
1044 protected function __clone() {
1048 * Factory method for this class
1050 * @return available_update_checker the singleton instance
1052 public static function instance() {
1053 if (is_null(self
::$singletoninstance)) {
1054 self
::$singletoninstance = new self();
1056 return self
::$singletoninstance;
1061 * @param bool $phpunitreset
1063 public static function reset_caches($phpunitreset = false) {
1064 if ($phpunitreset) {
1065 self
::$singletoninstance = null;
1070 * Returns the timestamp of the last execution of {@link fetch()}
1072 * @return int|null null if it has never been executed or we don't known
1074 public function get_last_timefetched() {
1076 $this->restore_response();
1078 if (!empty($this->recentfetch
)) {
1079 return $this->recentfetch
;
1087 * Fetches the available update status from the remote site
1089 * @throws available_update_checker_exception
1091 public function fetch() {
1092 $response = $this->get_response();
1093 $this->validate_response($response);
1094 $this->store_response($response);
1098 * Returns the available update information for the given component
1100 * This method returns null if the most recent response does not contain any information
1101 * about it. The returned structure is an array of available updates for the given
1102 * component. Each update info is an object with at least one property called
1103 * 'version'. Other possible properties are 'release', 'maturity', 'url' and 'downloadurl'.
1105 * For the 'core' component, the method returns real updates only (those with higher version).
1106 * For all other components, the list of all known remote updates is returned and the caller
1107 * (usually the {@link plugin_manager}) is supposed to make the actual comparison of versions.
1109 * @param string $component frankenstyle
1110 * @param array $options with supported keys 'minmaturity' and/or 'notifybuilds'
1111 * @return null|array null or array of available_update_info objects
1113 public function get_update_info($component, array $options = array()) {
1115 if (!isset($options['minmaturity'])) {
1116 $options['minmaturity'] = 0;
1119 if (!isset($options['notifybuilds'])) {
1120 $options['notifybuilds'] = false;
1123 if ($component == 'core') {
1124 $this->load_current_environment();
1127 $this->restore_response();
1129 if (empty($this->recentresponse
['updates'][$component])) {
1134 foreach ($this->recentresponse
['updates'][$component] as $info) {
1135 $update = new available_update_info($component, $info);
1136 if (isset($update->maturity
) and ($update->maturity
< $options['minmaturity'])) {
1139 if ($component == 'core') {
1140 if ($update->version
<= $this->currentversion
) {
1143 if (empty($options['notifybuilds']) and $this->is_same_release($update->release
)) {
1147 $updates[] = $update;
1150 if (empty($updates)) {
1158 * The method being run via cron.php
1160 public function cron() {
1163 if (!$this->cron_autocheck_enabled()) {
1164 $this->cron_mtrace('Automatic check for available updates not enabled, skipping.');
1168 $now = $this->cron_current_timestamp();
1170 if ($this->cron_has_fresh_fetch($now)) {
1171 $this->cron_mtrace('Recently fetched info about available updates is still fresh enough, skipping.');
1175 if ($this->cron_has_outdated_fetch($now)) {
1176 $this->cron_mtrace('Outdated or missing info about available updates, forced fetching ... ', '');
1177 $this->cron_execute();
1181 $offset = $this->cron_execution_offset();
1182 $start = mktime(1, 0, 0, date('n', $now), date('j', $now), date('Y', $now)); // 01:00 AM today local time
1183 if ($now > $start +
$offset) {
1184 $this->cron_mtrace('Regular daily check for available updates ... ', '');
1185 $this->cron_execute();
1190 /// end of public API //////////////////////////////////////////////////////
1193 * Makes cURL request to get data from the remote site
1195 * @return string raw request result
1196 * @throws available_update_checker_exception
1198 protected function get_response() {
1200 require_once($CFG->libdir
.'/filelib.php');
1202 $curl = new curl(array('proxy' => true));
1203 $response = $curl->post($this->prepare_request_url(), $this->prepare_request_params(), $this->prepare_request_options());
1204 $curlerrno = $curl->get_errno();
1205 if (!empty($curlerrno)) {
1206 throw new available_update_checker_exception('err_response_curl', 'cURL error '.$curlerrno.': '.$curl->error
);
1208 $curlinfo = $curl->get_info();
1209 if ($curlinfo['http_code'] != 200) {
1210 throw new available_update_checker_exception('err_response_http_code', $curlinfo['http_code']);
1216 * Makes sure the response is valid, has correct API format etc.
1218 * @param string $response raw response as returned by the {@link self::get_response()}
1219 * @throws available_update_checker_exception
1221 protected function validate_response($response) {
1223 $response = $this->decode_response($response);
1225 if (empty($response)) {
1226 throw new available_update_checker_exception('err_response_empty');
1229 if (empty($response['status']) or $response['status'] !== 'OK') {
1230 throw new available_update_checker_exception('err_response_status', $response['status']);
1233 if (empty($response['apiver']) or $response['apiver'] !== '1.2') {
1234 throw new available_update_checker_exception('err_response_format_version', $response['apiver']);
1237 if (empty($response['forbranch']) or $response['forbranch'] !== moodle_major_version(true)) {
1238 throw new available_update_checker_exception('err_response_target_version', $response['forbranch']);
1243 * Decodes the raw string response from the update notifications provider
1245 * @param string $response as returned by {@link self::get_response()}
1246 * @return array decoded response structure
1248 protected function decode_response($response) {
1249 return json_decode($response, true);
1253 * Stores the valid fetched response for later usage
1255 * This implementation uses the config_plugins table as the permanent storage.
1257 * @param string $response raw valid data returned by {@link self::get_response()}
1259 protected function store_response($response) {
1261 set_config('recentfetch', time(), 'core_plugin');
1262 set_config('recentresponse', $response, 'core_plugin');
1264 $this->restore_response(true);
1268 * Loads the most recent raw response record we have fetched
1270 * After this method is called, $this->recentresponse is set to an array. If the
1271 * array is empty, then either no data have been fetched yet or the fetched data
1272 * do not have expected format (and thence they are ignored and a debugging
1273 * message is displayed).
1275 * This implementation uses the config_plugins table as the permanent storage.
1277 * @param bool $forcereload reload even if it was already loaded
1279 protected function restore_response($forcereload = false) {
1281 if (!$forcereload and !is_null($this->recentresponse
)) {
1282 // we already have it, nothing to do
1286 $config = get_config('core_plugin');
1288 if (!empty($config->recentresponse
) and !empty($config->recentfetch
)) {
1290 $this->validate_response($config->recentresponse
);
1291 $this->recentfetch
= $config->recentfetch
;
1292 $this->recentresponse
= $this->decode_response($config->recentresponse
);
1293 } catch (available_update_checker_exception
$e) {
1294 // The server response is not valid. Behave as if no data were fetched yet.
1295 // This may happen when the most recent update info (cached locally) has been
1296 // fetched with the previous branch of Moodle (like during an upgrade from 2.x
1297 // to 2.y) or when the API of the response has changed.
1298 $this->recentresponse
= array();
1302 $this->recentresponse
= array();
1307 * Compares two raw {@link $recentresponse} records and returns the list of changed updates
1309 * This method is used to populate potential update info to be sent to site admins.
1313 * @throws available_update_checker_exception
1314 * @return array parts of $new['updates'] that have changed
1316 protected function compare_responses(array $old, array $new) {
1322 if (!array_key_exists('updates', $new)) {
1323 throw new available_update_checker_exception('err_response_format');
1327 return $new['updates'];
1330 if (!array_key_exists('updates', $old)) {
1331 throw new available_update_checker_exception('err_response_format');
1336 foreach ($new['updates'] as $newcomponent => $newcomponentupdates) {
1337 if (empty($old['updates'][$newcomponent])) {
1338 $changes[$newcomponent] = $newcomponentupdates;
1341 foreach ($newcomponentupdates as $newcomponentupdate) {
1343 foreach ($old['updates'][$newcomponent] as $oldcomponentupdate) {
1344 if ($newcomponentupdate['version'] == $oldcomponentupdate['version']) {
1349 if (!isset($changes[$newcomponent])) {
1350 $changes[$newcomponent] = array();
1352 $changes[$newcomponent][] = $newcomponentupdate;
1361 * Returns the URL to send update requests to
1363 * During the development or testing, you can set $CFG->alternativeupdateproviderurl
1364 * to a custom URL that will be used. Otherwise the standard URL will be returned.
1366 * @return string URL
1368 protected function prepare_request_url() {
1371 if (!empty($CFG->config_php_settings
['alternativeupdateproviderurl'])) {
1372 return $CFG->config_php_settings
['alternativeupdateproviderurl'];
1374 return 'https://download.moodle.org/api/1.2/updates.php';
1379 * Sets the properties currentversion, currentrelease, currentbranch and currentplugins
1381 * @param bool $forcereload
1383 protected function load_current_environment($forcereload=false) {
1386 if (!is_null($this->currentversion
) and !$forcereload) {
1394 require($CFG->dirroot
.'/version.php');
1395 $this->currentversion
= $version;
1396 $this->currentrelease
= $release;
1397 $this->currentbranch
= moodle_major_version(true);
1399 $pluginman = plugin_manager
::instance();
1400 foreach ($pluginman->get_plugins() as $type => $plugins) {
1401 foreach ($plugins as $plugin) {
1402 if (!$plugin->is_standard()) {
1403 $this->currentplugins
[$plugin->component
] = $plugin->versiondisk
;
1410 * Returns the list of HTTP params to be sent to the updates provider URL
1412 * @return array of (string)param => (string)value
1414 protected function prepare_request_params() {
1417 $this->load_current_environment();
1418 $this->restore_response();
1421 $params['format'] = 'json';
1423 if (isset($this->recentresponse
['ticket'])) {
1424 $params['ticket'] = $this->recentresponse
['ticket'];
1427 if (isset($this->currentversion
)) {
1428 $params['version'] = $this->currentversion
;
1430 throw new coding_exception('Main Moodle version must be already known here');
1433 if (isset($this->currentbranch
)) {
1434 $params['branch'] = $this->currentbranch
;
1436 throw new coding_exception('Moodle release must be already known here');
1440 foreach ($this->currentplugins
as $plugin => $version) {
1441 $plugins[] = $plugin.'@'.$version;
1443 if (!empty($plugins)) {
1444 $params['plugins'] = implode(',', $plugins);
1451 * Returns the list of cURL options to use when fetching available updates data
1453 * @return array of (string)param => (string)value
1455 protected function prepare_request_options() {
1459 'CURLOPT_SSL_VERIFYHOST' => 2, // this is the default in {@link curl} class but just in case
1460 'CURLOPT_SSL_VERIFYPEER' => true,
1467 * Returns the current timestamp
1469 * @return int the timestamp
1471 protected function cron_current_timestamp() {
1476 * Output cron debugging info
1479 * @param string $msg output message
1480 * @param string $eol end of line
1482 protected function cron_mtrace($msg, $eol = PHP_EOL
) {
1487 * Decide if the autocheck feature is disabled in the server setting
1489 * @return bool true if autocheck enabled, false if disabled
1491 protected function cron_autocheck_enabled() {
1494 if (empty($CFG->updateautocheck
)) {
1502 * Decide if the recently fetched data are still fresh enough
1504 * @param int $now current timestamp
1505 * @return bool true if no need to re-fetch, false otherwise
1507 protected function cron_has_fresh_fetch($now) {
1508 $recent = $this->get_last_timefetched();
1510 if (empty($recent)) {
1514 if ($now < $recent) {
1515 $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1519 if ($now - $recent > 24 * HOURSECS
) {
1527 * Decide if the fetch is outadated or even missing
1529 * @param int $now current timestamp
1530 * @return bool false if no need to re-fetch, true otherwise
1532 protected function cron_has_outdated_fetch($now) {
1533 $recent = $this->get_last_timefetched();
1535 if (empty($recent)) {
1539 if ($now < $recent) {
1540 $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1544 if ($now - $recent > 48 * HOURSECS
) {
1552 * Returns the cron execution offset for this site
1554 * The main {@link self::cron()} is supposed to run every night in some random time
1555 * between 01:00 and 06:00 AM (local time). The exact moment is defined by so called
1556 * execution offset, that is the amount of time after 01:00 AM. The offset value is
1557 * initially generated randomly and then used consistently at the site. This way, the
1558 * regular checks against the download.moodle.org server are spread in time.
1560 * @return int the offset number of seconds from range 1 sec to 5 hours
1562 protected function cron_execution_offset() {
1565 if (empty($CFG->updatecronoffset
)) {
1566 set_config('updatecronoffset', rand(1, 5 * HOURSECS
));
1569 return $CFG->updatecronoffset
;
1573 * Fetch available updates info and eventually send notification to site admins
1575 protected function cron_execute() {
1578 $this->restore_response();
1579 $previous = $this->recentresponse
;
1581 $this->restore_response(true);
1582 $current = $this->recentresponse
;
1583 $changes = $this->compare_responses($previous, $current);
1584 $notifications = $this->cron_notifications($changes);
1585 $this->cron_notify($notifications);
1586 $this->cron_mtrace('done');
1587 } catch (available_update_checker_exception
$e) {
1588 $this->cron_mtrace('FAILED!');
1593 * Given the list of changes in available updates, pick those to send to site admins
1595 * @param array $changes as returned by {@link self::compare_responses()}
1596 * @return array of available_update_info objects to send to site admins
1598 protected function cron_notifications(array $changes) {
1601 $notifications = array();
1602 $pluginman = plugin_manager
::instance();
1603 $plugins = $pluginman->get_plugins(true);
1605 foreach ($changes as $component => $componentchanges) {
1606 if (empty($componentchanges)) {
1609 $componentupdates = $this->get_update_info($component,
1610 array('minmaturity' => $CFG->updateminmaturity
, 'notifybuilds' => $CFG->updatenotifybuilds
));
1611 if (empty($componentupdates)) {
1614 // notify only about those $componentchanges that are present in $componentupdates
1615 // to respect the preferences
1616 foreach ($componentchanges as $componentchange) {
1617 foreach ($componentupdates as $componentupdate) {
1618 if ($componentupdate->version
== $componentchange['version']) {
1619 if ($component == 'core') {
1620 // In case of 'core', we already know that the $componentupdate
1621 // is a real update with higher version ({@see self::get_update_info()}).
1622 // We just perform additional check for the release property as there
1623 // can be two Moodle releases having the same version (e.g. 2.4.0 and 2.5dev shortly
1624 // after the release). We can do that because we have the release info
1625 // always available for the core.
1626 if ((string)$componentupdate->release
=== (string)$componentchange['release']) {
1627 $notifications[] = $componentupdate;
1630 // Use the plugin_manager to check if the detected $componentchange
1631 // is a real update with higher version. That is, the $componentchange
1632 // is present in the array of {@link available_update_info} objects
1633 // returned by the plugin's available_updates() method.
1634 list($plugintype, $pluginname) = core_component
::normalize_component($component);
1635 if (!empty($plugins[$plugintype][$pluginname])) {
1636 $availableupdates = $plugins[$plugintype][$pluginname]->available_updates();
1637 if (!empty($availableupdates)) {
1638 foreach ($availableupdates as $availableupdate) {
1639 if ($availableupdate->version
== $componentchange['version']) {
1640 $notifications[] = $componentupdate;
1651 return $notifications;
1655 * Sends the given notifications to site admins via messaging API
1657 * @param array $notifications array of available_update_info objects to send
1659 protected function cron_notify(array $notifications) {
1662 if (empty($notifications)) {
1666 $admins = get_admins();
1668 if (empty($admins)) {
1672 $this->cron_mtrace('sending notifications ... ', '');
1674 $text = get_string('updatenotifications', 'core_admin') . PHP_EOL
;
1675 $html = html_writer
::tag('h1', get_string('updatenotifications', 'core_admin')) . PHP_EOL
;
1677 $coreupdates = array();
1678 $pluginupdates = array();
1680 foreach ($notifications as $notification) {
1681 if ($notification->component
== 'core') {
1682 $coreupdates[] = $notification;
1684 $pluginupdates[] = $notification;
1688 if (!empty($coreupdates)) {
1689 $text .= PHP_EOL
. get_string('updateavailable', 'core_admin') . PHP_EOL
;
1690 $html .= html_writer
::tag('h2', get_string('updateavailable', 'core_admin')) . PHP_EOL
;
1691 $html .= html_writer
::start_tag('ul') . PHP_EOL
;
1692 foreach ($coreupdates as $coreupdate) {
1693 $html .= html_writer
::start_tag('li');
1694 if (isset($coreupdate->release
)) {
1695 $text .= get_string('updateavailable_release', 'core_admin', $coreupdate->release
);
1696 $html .= html_writer
::tag('strong', get_string('updateavailable_release', 'core_admin', $coreupdate->release
));
1698 if (isset($coreupdate->version
)) {
1699 $text .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version
);
1700 $html .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version
);
1702 if (isset($coreupdate->maturity
)) {
1703 $text .= ' ('.get_string('maturity'.$coreupdate->maturity
, 'core_admin').')';
1704 $html .= ' ('.get_string('maturity'.$coreupdate->maturity
, 'core_admin').')';
1707 $html .= html_writer
::end_tag('li') . PHP_EOL
;
1710 $html .= html_writer
::end_tag('ul') . PHP_EOL
;
1712 $a = array('url' => $CFG->wwwroot
.'/'.$CFG->admin
.'/index.php');
1713 $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL
;
1714 $a = array('url' => html_writer
::link($CFG->wwwroot
.'/'.$CFG->admin
.'/index.php', $CFG->wwwroot
.'/'.$CFG->admin
.'/index.php'));
1715 $html .= html_writer
::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL
;
1718 if (!empty($pluginupdates)) {
1719 $text .= PHP_EOL
. get_string('updateavailableforplugin', 'core_admin') . PHP_EOL
;
1720 $html .= html_writer
::tag('h2', get_string('updateavailableforplugin', 'core_admin')) . PHP_EOL
;
1722 $html .= html_writer
::start_tag('ul') . PHP_EOL
;
1723 foreach ($pluginupdates as $pluginupdate) {
1724 $html .= html_writer
::start_tag('li');
1725 $text .= get_string('pluginname', $pluginupdate->component
);
1726 $html .= html_writer
::tag('strong', get_string('pluginname', $pluginupdate->component
));
1728 $text .= ' ('.$pluginupdate->component
.')';
1729 $html .= ' ('.$pluginupdate->component
.')';
1731 $text .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version
);
1732 $html .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version
);
1735 $html .= html_writer
::end_tag('li') . PHP_EOL
;
1738 $html .= html_writer
::end_tag('ul') . PHP_EOL
;
1740 $a = array('url' => $CFG->wwwroot
.'/'.$CFG->admin
.'/plugins.php');
1741 $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL
;
1742 $a = array('url' => html_writer
::link($CFG->wwwroot
.'/'.$CFG->admin
.'/plugins.php', $CFG->wwwroot
.'/'.$CFG->admin
.'/plugins.php'));
1743 $html .= html_writer
::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL
;
1746 $a = array('siteurl' => $CFG->wwwroot
);
1747 $text .= get_string('updatenotificationfooter', 'core_admin', $a) . PHP_EOL
;
1748 $a = array('siteurl' => html_writer
::link($CFG->wwwroot
, $CFG->wwwroot
));
1749 $html .= html_writer
::tag('footer', html_writer
::tag('p', get_string('updatenotificationfooter', 'core_admin', $a),
1750 array('style' => 'font-size:smaller; color:#333;')));
1752 foreach ($admins as $admin) {
1753 $message = new stdClass();
1754 $message->component
= 'moodle';
1755 $message->name
= 'availableupdate';
1756 $message->userfrom
= get_admin();
1757 $message->userto
= $admin;
1758 $message->subject
= get_string('updatenotificationsubject', 'core_admin', array('siteurl' => $CFG->wwwroot
));
1759 $message->fullmessage
= $text;
1760 $message->fullmessageformat
= FORMAT_PLAIN
;
1761 $message->fullmessagehtml
= $html;
1762 $message->smallmessage
= get_string('updatenotifications', 'core_admin');
1763 $message->notification
= 1;
1764 message_send($message);
1769 * Compare two release labels and decide if they are the same
1771 * @param string $remote release info of the available update
1772 * @param null|string $local release info of the local code, defaults to $release defined in version.php
1773 * @return boolean true if the releases declare the same minor+major version
1775 protected function is_same_release($remote, $local=null) {
1777 if (is_null($local)) {
1778 $this->load_current_environment();
1779 $local = $this->currentrelease
;
1782 $pattern = '/^([0-9\.\+]+)([^(]*)/';
1784 preg_match($pattern, $remote, $remotematches);
1785 preg_match($pattern, $local, $localmatches);
1787 $remotematches[1] = str_replace('+', '', $remotematches[1]);
1788 $localmatches[1] = str_replace('+', '', $localmatches[1]);
1790 if ($remotematches[1] === $localmatches[1] and rtrim($remotematches[2]) === rtrim($localmatches[2])) {
1800 * Defines the structure of objects returned by {@link available_update_checker::get_update_info()}
1802 class available_update_info
{
1804 /** @var string frankenstyle component name */
1806 /** @var int the available version of the component */
1808 /** @var string|null optional release name */
1809 public $release = null;
1810 /** @var int|null optional maturity info, eg {@link MATURITY_STABLE} */
1811 public $maturity = null;
1812 /** @var string|null optional URL of a page with more info about the update */
1814 /** @var string|null optional URL of a ZIP package that can be downloaded and installed */
1815 public $download = null;
1816 /** @var string|null of self::download is set, then this must be the MD5 hash of the ZIP */
1817 public $downloadmd5 = null;
1820 * Creates new instance of the class
1822 * The $info array must provide at least the 'version' value and optionally all other
1823 * values to populate the object's properties.
1825 * @param string $name the frankenstyle component name
1826 * @param array $info associative array with other properties
1828 public function __construct($name, array $info) {
1829 $this->component
= $name;
1830 foreach ($info as $k => $v) {
1831 if (property_exists('available_update_info', $k) and $k != 'component') {
1840 * Implements a communication bridge to the mdeploy.php utility
1842 class available_update_deployer
{
1844 const HTTP_PARAM_PREFIX
= 'updteautodpldata_'; // Hey, even Google has not heard of such a prefix! So it MUST be safe :-p
1845 const HTTP_PARAM_CHECKER
= 'datapackagesize'; // Name of the parameter that holds the number of items in the received data items
1847 /** @var available_update_deployer holds the singleton instance */
1848 protected static $singletoninstance;
1849 /** @var moodle_url URL of a page that includes the deployer UI */
1850 protected $callerurl;
1851 /** @var moodle_url URL to return after the deployment */
1852 protected $returnurl;
1855 * Direct instantiation not allowed, use the factory method {@link self::instance()}
1857 protected function __construct() {
1861 * Sorry, this is singleton
1863 protected function __clone() {
1867 * Factory method for this class
1869 * @return available_update_deployer the singleton instance
1871 public static function instance() {
1872 if (is_null(self
::$singletoninstance)) {
1873 self
::$singletoninstance = new self();
1875 return self
::$singletoninstance;
1879 * Reset caches used by this script
1881 * @param bool $phpunitreset is this called as a part of PHPUnit reset?
1883 public static function reset_caches($phpunitreset = false) {
1884 if ($phpunitreset) {
1885 self
::$singletoninstance = null;
1890 * Is automatic deployment enabled?
1894 public function enabled() {
1897 if (!empty($CFG->disableupdateautodeploy
)) {
1898 // The feature is prohibited via config.php
1902 return get_config('updateautodeploy');
1906 * Sets some base properties of the class to make it usable.
1908 * @param moodle_url $callerurl the base URL of a script that will handle the class'es form data
1909 * @param moodle_url $returnurl the final URL to return to when the deployment is finished
1911 public function initialize(moodle_url
$callerurl, moodle_url
$returnurl) {
1913 if (!$this->enabled()) {
1914 throw new coding_exception('Unable to initialize the deployer, the feature is not enabled.');
1917 $this->callerurl
= $callerurl;
1918 $this->returnurl
= $returnurl;
1922 * Has the deployer been initialized?
1924 * Initialized deployer means that the following properties were set:
1925 * callerurl, returnurl
1929 public function initialized() {
1931 if (!$this->enabled()) {
1935 if (empty($this->callerurl
)) {
1939 if (empty($this->returnurl
)) {
1947 * Returns a list of reasons why the deployment can not happen
1949 * If the returned array is empty, the deployment seems to be possible. The returned
1950 * structure is an associative array with keys representing individual impediments.
1951 * Possible keys are: missingdownloadurl, missingdownloadmd5, notwritable.
1953 * @param available_update_info $info
1956 public function deployment_impediments(available_update_info
$info) {
1958 $impediments = array();
1960 if (empty($info->download
)) {
1961 $impediments['missingdownloadurl'] = true;
1964 if (empty($info->downloadmd5
)) {
1965 $impediments['missingdownloadmd5'] = true;
1968 if (!empty($info->download
) and !$this->update_downloadable($info->download
)) {
1969 $impediments['notdownloadable'] = true;
1972 if (!$this->component_writable($info->component
)) {
1973 $impediments['notwritable'] = true;
1976 return $impediments;
1980 * Check to see if the current version of the plugin seems to be a checkout of an external repository.
1982 * @see plugin_manager::plugin_external_source()
1983 * @param available_update_info $info
1984 * @return false|string
1986 public function plugin_external_source(available_update_info
$info) {
1988 $paths = core_component
::get_plugin_types();
1989 list($plugintype, $pluginname) = core_component
::normalize_component($info->component
);
1990 $pluginroot = $paths[$plugintype].'/'.$pluginname;
1992 if (is_dir($pluginroot.'/.git')) {
1996 if (is_dir($pluginroot.'/CVS')) {
2000 if (is_dir($pluginroot.'/.svn')) {
2008 * Prepares a renderable widget to confirm installation of an available update.
2010 * @param available_update_info $info component version to deploy
2011 * @return renderable
2013 public function make_confirm_widget(available_update_info
$info) {
2015 if (!$this->initialized()) {
2016 throw new coding_exception('Illegal method call - deployer not initialized.');
2019 $params = $this->data_to_params(array(
2020 'updateinfo' => (array)$info, // see http://www.php.net/manual/en/language.types.array.php#language.types.array.casting
2023 $widget = new single_button(
2024 new moodle_url($this->callerurl
, $params),
2025 get_string('updateavailableinstall', 'core_admin'),
2033 * Prepares a renderable widget to execute installation of an available update.
2035 * @param available_update_info $info component version to deploy
2036 * @param moodle_url $returnurl URL to return after the installation execution
2037 * @return renderable
2039 public function make_execution_widget(available_update_info
$info, moodle_url
$returnurl = null) {
2042 if (!$this->initialized()) {
2043 throw new coding_exception('Illegal method call - deployer not initialized.');
2046 $pluginrootpaths = core_component
::get_plugin_types();
2048 list($plugintype, $pluginname) = core_component
::normalize_component($info->component
);
2050 if (empty($pluginrootpaths[$plugintype])) {
2051 throw new coding_exception('Unknown plugin type root location', $plugintype);
2054 list($passfile, $password) = $this->prepare_authorization();
2056 if (is_null($returnurl)) {
2057 $returnurl = new moodle_url('/admin');
2059 $returnurl = $returnurl;
2064 'type' => $plugintype,
2065 'name' => $pluginname,
2066 'typeroot' => $pluginrootpaths[$plugintype],
2067 'package' => $info->download
,
2068 'md5' => $info->downloadmd5
,
2069 'dataroot' => $CFG->dataroot
,
2070 'dirroot' => $CFG->dirroot
,
2071 'passfile' => $passfile,
2072 'password' => $password,
2073 'returnurl' => $returnurl->out(false),
2076 if (!empty($CFG->proxyhost
)) {
2077 // MDL-36973 - Beware - we should call just !is_proxybypass() here. But currently, our
2078 // cURL wrapper class does not do it. So, to have consistent behaviour, we pass proxy
2079 // setting regardless the $CFG->proxybypass setting. Once the {@link curl} class is
2080 // fixed, the condition should be amended.
2081 if (true or !is_proxybypass($info->download
)) {
2082 if (empty($CFG->proxyport
)) {
2083 $params['proxy'] = $CFG->proxyhost
;
2085 $params['proxy'] = $CFG->proxyhost
.':'.$CFG->proxyport
;
2088 if (!empty($CFG->proxyuser
) and !empty($CFG->proxypassword
)) {
2089 $params['proxyuserpwd'] = $CFG->proxyuser
.':'.$CFG->proxypassword
;
2092 if (!empty($CFG->proxytype
)) {
2093 $params['proxytype'] = $CFG->proxytype
;
2098 $widget = new single_button(
2099 new moodle_url('/mdeploy.php', $params),
2100 get_string('updateavailableinstall', 'core_admin'),
2108 * Returns array of data objects passed to this tool.
2112 public function submitted_data() {
2114 $data = $this->params_to_data($_POST);
2116 if (empty($data) or empty($data[self
::HTTP_PARAM_CHECKER
])) {
2120 if (!empty($data['updateinfo']) and is_object($data['updateinfo'])) {
2121 $updateinfo = $data['updateinfo'];
2122 if (!empty($updateinfo->component
) and !empty($updateinfo->version
)) {
2123 $data['updateinfo'] = new available_update_info($updateinfo->component
, (array)$updateinfo);
2127 if (!empty($data['callerurl'])) {
2128 $data['callerurl'] = new moodle_url($data['callerurl']);
2131 if (!empty($data['returnurl'])) {
2132 $data['returnurl'] = new moodle_url($data['returnurl']);
2139 * Handles magic getters and setters for protected properties.
2141 * @param string $name method name, e.g. set_returnurl()
2142 * @param array $arguments arguments to be passed to the array
2144 public function __call($name, array $arguments = array()) {
2146 if (substr($name, 0, 4) === 'set_') {
2147 $property = substr($name, 4);
2148 if (empty($property)) {
2149 throw new coding_exception('Invalid property name (empty)');
2151 if (empty($arguments)) {
2152 $arguments = array(true); // Default value for flag-like properties.
2154 // Make sure it is a protected property.
2155 $isprotected = false;
2156 $reflection = new ReflectionObject($this);
2157 foreach ($reflection->getProperties(ReflectionProperty
::IS_PROTECTED
) as $reflectionproperty) {
2158 if ($reflectionproperty->getName() === $property) {
2159 $isprotected = true;
2163 if (!$isprotected) {
2164 throw new coding_exception('Unable to set property - it does not exist or it is not protected');
2166 $value = reset($arguments);
2167 $this->$property = $value;
2171 if (substr($name, 0, 4) === 'get_') {
2172 $property = substr($name, 4);
2173 if (empty($property)) {
2174 throw new coding_exception('Invalid property name (empty)');
2176 if (!empty($arguments)) {
2177 throw new coding_exception('No parameter expected');
2179 // Make sure it is a protected property.
2180 $isprotected = false;
2181 $reflection = new ReflectionObject($this);
2182 foreach ($reflection->getProperties(ReflectionProperty
::IS_PROTECTED
) as $reflectionproperty) {
2183 if ($reflectionproperty->getName() === $property) {
2184 $isprotected = true;
2188 if (!$isprotected) {
2189 throw new coding_exception('Unable to get property - it does not exist or it is not protected');
2191 return $this->$property;
2196 * Generates a random token and stores it in a file in moodledata directory.
2198 * @return array of the (string)filename and (string)password in this order
2200 public function prepare_authorization() {
2203 make_upload_directory('mdeploy/auth/');
2208 while (!$success and $attempts < 5) {
2211 $passfile = $this->generate_passfile();
2212 $password = $this->generate_password();
2215 $filepath = $CFG->dataroot
.'/mdeploy/auth/'.$passfile;
2217 if (!file_exists($filepath)) {
2218 $success = file_put_contents($filepath, $password . PHP_EOL
. $now . PHP_EOL
, LOCK_EX
);
2219 chmod($filepath, $CFG->filepermissions
);
2224 return array($passfile, $password);
2227 throw new moodle_exception('unable_prepare_authorization', 'core_plugin');
2231 // End of external API
2234 * Prepares an array of HTTP parameters that can be passed to another page.
2236 * @param array|object $data associative array or an object holding the data, data JSON-able
2237 * @return array suitable as a param for moodle_url
2239 protected function data_to_params($data) {
2241 // Append some our own data
2242 if (!empty($this->callerurl
)) {
2243 $data['callerurl'] = $this->callerurl
->out(false);
2245 if (!empty($this->returnurl
)) {
2246 $data['returnurl'] = $this->returnurl
->out(false);
2249 // Finally append the count of items in the package.
2250 $data[self
::HTTP_PARAM_CHECKER
] = count($data);
2254 foreach ($data as $name => $value) {
2255 $transname = self
::HTTP_PARAM_PREFIX
.$name;
2256 $transvalue = json_encode($value);
2257 $params[$transname] = $transvalue;
2264 * Converts HTTP parameters passed to the script into native PHP data
2266 * @param array $params such as $_REQUEST or $_POST
2267 * @return array data passed for this class
2269 protected function params_to_data(array $params) {
2271 if (empty($params)) {
2276 foreach ($params as $name => $value) {
2277 if (strpos($name, self
::HTTP_PARAM_PREFIX
) === 0) {
2278 $realname = substr($name, strlen(self
::HTTP_PARAM_PREFIX
));
2279 $realvalue = json_decode($value);
2280 $data[$realname] = $realvalue;
2288 * Returns a random string to be used as a filename of the password storage.
2292 protected function generate_passfile() {
2293 return clean_param(uniqid('mdeploy_', true), PARAM_FILE
);
2297 * Returns a random string to be used as the authorization token
2301 protected function generate_password() {
2302 return complex_random_string();
2306 * Checks if the given component's directory is writable
2308 * For the purpose of the deployment, the web server process has to have
2309 * write access to all files in the component's directory (recursively) and for the
2312 * @see worker::move_directory_source_precheck()
2313 * @param string $component normalized component name
2316 protected function component_writable($component) {
2318 list($plugintype, $pluginname) = core_component
::normalize_component($component);
2320 $directory = core_component
::get_plugin_directory($plugintype, $pluginname);
2322 if (is_null($directory)) {
2323 throw new coding_exception('Unknown component location', $component);
2326 return $this->directory_writable($directory);
2330 * Checks if the mdeploy.php will be able to fetch the ZIP from the given URL
2332 * This is mainly supposed to check if the transmission over HTTPS would
2333 * work. That is, if the CA certificates are present at the server.
2335 * @param string $downloadurl the URL of the ZIP package to download
2338 protected function update_downloadable($downloadurl) {
2341 $curloptions = array(
2342 'CURLOPT_SSL_VERIFYHOST' => 2, // this is the default in {@link curl} class but just in case
2343 'CURLOPT_SSL_VERIFYPEER' => true,
2346 $curl = new curl(array('proxy' => true));
2347 $result = $curl->head($downloadurl, $curloptions);
2348 $errno = $curl->get_errno();
2349 if (empty($errno)) {
2357 * Checks if the directory and all its contents (recursively) is writable
2359 * @param string $path full path to a directory
2362 private function directory_writable($path) {
2364 if (!is_writable($path)) {
2368 if (is_dir($path)) {
2369 $handle = opendir($path);
2376 while ($filename = readdir($handle)) {
2377 $filepath = $path.'/'.$filename;
2379 if ($filename === '.' or $filename === '..') {
2383 if (is_dir($filepath)) {
2384 $result = $result && $this->directory_writable($filepath);
2387 $result = $result && is_writable($filepath);
2399 * Factory class producing required subclasses of {@link plugininfo_base}
2401 class plugininfo_default_factory
{
2404 * Makes a new instance of the plugininfo class
2406 * @param string $type the plugin type, eg. 'mod'
2407 * @param string $typerootdir full path to the location of all the plugins of this type
2408 * @param string $name the plugin name, eg. 'workshop'
2409 * @param string $namerootdir full path to the location of the plugin
2410 * @param string $typeclass the name of class that holds the info about the plugin
2411 * @return plugininfo_base the instance of $typeclass
2413 public static function make($type, $typerootdir, $name, $namerootdir, $typeclass) {
2414 $plugin = new $typeclass();
2415 $plugin->type
= $type;
2416 $plugin->typerootdir
= $typerootdir;
2417 $plugin->name
= $name;
2418 $plugin->rootdir
= $namerootdir;
2420 $plugin->init_display_name();
2421 $plugin->load_disk_version();
2422 $plugin->load_db_version();
2423 $plugin->load_required_main_version();
2424 $plugin->init_is_standard();
2432 * Base class providing access to the information about a plugin
2434 * @property-read string component the component name, type_name
2436 abstract class plugininfo_base
{
2438 /** @var string the plugintype name, eg. mod, auth or workshopform */
2440 /** @var string full path to the location of all the plugins of this type */
2441 public $typerootdir;
2442 /** @var string the plugin name, eg. assignment, ldap */
2444 /** @var string the localized plugin name */
2445 public $displayname;
2446 /** @var string the plugin source, one of plugin_manager::PLUGIN_SOURCE_xxx constants */
2448 /** @var fullpath to the location of this plugin */
2450 /** @var int|string the version of the plugin's source code */
2451 public $versiondisk;
2452 /** @var int|string the version of the installed plugin */
2454 /** @var int|float|string required version of Moodle core */
2455 public $versionrequires;
2456 /** @var array other plugins that this one depends on, lazy-loaded by {@link get_other_required_plugins()} */
2457 public $dependencies;
2458 /** @var int number of instances of the plugin - not supported yet */
2460 /** @var int order of the plugin among other plugins of the same type - not supported yet */
2462 /** @var array|null array of {@link available_update_info} for this plugin */
2463 public $availableupdates;
2466 * Gathers and returns the information about all plugins of the given type
2468 * @param string $type the name of the plugintype, eg. mod, auth or workshopform
2469 * @param string $typerootdir full path to the location of the plugin dir
2470 * @param string $typeclass the name of the actually called class
2471 * @return array of plugintype classes, indexed by the plugin name
2473 public static function get_plugins($type, $typerootdir, $typeclass) {
2475 // get the information about plugins at the disk
2476 $plugins = core_component
::get_plugin_list($type);
2478 foreach ($plugins as $pluginname => $pluginrootdir) {
2479 $ondisk[$pluginname] = plugininfo_default_factory
::make($type, $typerootdir,
2480 $pluginname, $pluginrootdir, $typeclass);
2486 * Sets {@link $displayname} property to a localized name of the plugin
2488 public function init_display_name() {
2489 if (!get_string_manager()->string_exists('pluginname', $this->component
)) {
2490 $this->displayname
= '[pluginname,' . $this->component
. ']';
2492 $this->displayname
= get_string('pluginname', $this->component
);
2497 * Magic method getter, redirects to read only values.
2499 * @param string $name
2502 public function __get($name) {
2504 case 'component': return $this->type
. '_' . $this->name
;
2507 debugging('Invalid plugin property accessed! '.$name);
2513 * Return the full path name of a file within the plugin.
2515 * No check is made to see if the file exists.
2517 * @param string $relativepath e.g. 'version.php'.
2518 * @return string e.g. $CFG->dirroot . '/mod/quiz/version.php'.
2520 public function full_path($relativepath) {
2521 if (empty($this->rootdir
)) {
2524 return $this->rootdir
. '/' . $relativepath;
2528 * Load the data from version.php.
2530 * @param bool $disablecache do not attempt to obtain data from the cache
2531 * @return stdClass the object called $plugin defined in version.php
2533 protected function load_version_php($disablecache=false) {
2535 $cache = cache
::make('core', 'plugininfo_base');
2537 $versionsphp = $cache->get('versions_php');
2539 if (!$disablecache and $versionsphp !== false and isset($versionsphp[$this->component
])) {
2540 return $versionsphp[$this->component
];
2543 $versionfile = $this->full_path('version.php');
2545 $plugin = new stdClass();
2546 if (is_readable($versionfile)) {
2547 include($versionfile);
2549 $versionsphp[$this->component
] = $plugin;
2550 $cache->set('versions_php', $versionsphp);
2556 * Sets {@link $versiondisk} property to a numerical value representing the
2557 * version of the plugin's source code.
2559 * If the value is null after calling this method, either the plugin
2560 * does not use versioning (typically does not have any database
2561 * data) or is missing from disk.
2563 public function load_disk_version() {
2564 $plugin = $this->load_version_php();
2565 if (isset($plugin->version
)) {
2566 $this->versiondisk
= $plugin->version
;
2571 * Sets {@link $versionrequires} property to a numerical value representing
2572 * the version of Moodle core that this plugin requires.
2574 public function load_required_main_version() {
2575 $plugin = $this->load_version_php();
2576 if (isset($plugin->requires
)) {
2577 $this->versionrequires
= $plugin->requires
;
2582 * Initialise {@link $dependencies} to the list of other plugins (in any)
2583 * that this one requires to be installed.
2585 protected function load_other_required_plugins() {
2586 $plugin = $this->load_version_php();
2587 if (!empty($plugin->dependencies
)) {
2588 $this->dependencies
= $plugin->dependencies
;
2590 $this->dependencies
= array(); // By default, no dependencies.
2595 * Get the list of other plugins that this plugin requires to be installed.
2597 * @return array with keys the frankenstyle plugin name, and values either
2598 * a version string (like '2011101700') or the constant ANY_VERSION.
2600 public function get_other_required_plugins() {
2601 if (is_null($this->dependencies
)) {
2602 $this->load_other_required_plugins();
2604 return $this->dependencies
;
2608 * Is this is a subplugin?
2612 public function is_subplugin() {
2613 return ($this->get_parent_plugin() !== false);
2617 * If I am a subplugin, return the name of my parent plugin.
2619 * @return string|bool false if not a subplugin, name of the parent otherwise
2621 public function get_parent_plugin() {
2622 return $this->get_plugin_manager()->get_parent_of_subplugin($this->type
);
2626 * Sets {@link $versiondb} property to a numerical value representing the
2627 * currently installed version of the plugin.
2629 * If the value is null after calling this method, either the plugin
2630 * does not use versioning (typically does not have any database
2631 * data) or has not been installed yet.
2633 public function load_db_version() {
2634 if ($ver = self
::get_version_from_config_plugins($this->component
)) {
2635 $this->versiondb
= $ver;
2640 * Sets {@link $source} property to one of plugin_manager::PLUGIN_SOURCE_xxx
2643 * If the property's value is null after calling this method, then
2644 * the type of the plugin has not been recognized and you should throw
2647 public function init_is_standard() {
2649 $standard = plugin_manager
::standard_plugins_list($this->type
);
2651 if ($standard !== false) {
2652 $standard = array_flip($standard);
2653 if (isset($standard[$this->name
])) {
2654 $this->source
= plugin_manager
::PLUGIN_SOURCE_STANDARD
;
2655 } else if (!is_null($this->versiondb
) and is_null($this->versiondisk
)
2656 and plugin_manager
::is_deleted_standard_plugin($this->type
, $this->name
)) {
2657 $this->source
= plugin_manager
::PLUGIN_SOURCE_STANDARD
; // to be deleted
2659 $this->source
= plugin_manager
::PLUGIN_SOURCE_EXTENSION
;
2665 * Returns true if the plugin is shipped with the official distribution
2666 * of the current Moodle version, false otherwise.
2670 public function is_standard() {
2671 return $this->source
=== plugin_manager
::PLUGIN_SOURCE_STANDARD
;
2675 * Returns true if the the given Moodle version is enough to run this plugin
2677 * @param string|int|double $moodleversion
2680 public function is_core_dependency_satisfied($moodleversion) {
2682 if (empty($this->versionrequires
)) {
2686 return (double)$this->versionrequires
<= (double)$moodleversion;
2691 * Returns the status of the plugin
2693 * @return string one of plugin_manager::PLUGIN_STATUS_xxx constants
2695 public function get_status() {
2697 if (is_null($this->versiondb
) and is_null($this->versiondisk
)) {
2698 return plugin_manager
::PLUGIN_STATUS_NODB
;
2700 } else if (is_null($this->versiondb
) and !is_null($this->versiondisk
)) {
2701 return plugin_manager
::PLUGIN_STATUS_NEW
;
2703 } else if (!is_null($this->versiondb
) and is_null($this->versiondisk
)) {
2704 if (plugin_manager
::is_deleted_standard_plugin($this->type
, $this->name
)) {
2705 return plugin_manager
::PLUGIN_STATUS_DELETE
;
2707 return plugin_manager
::PLUGIN_STATUS_MISSING
;
2710 } else if ((string)$this->versiondb
=== (string)$this->versiondisk
) {
2711 return plugin_manager
::PLUGIN_STATUS_UPTODATE
;
2713 } else if ($this->versiondb
< $this->versiondisk
) {
2714 return plugin_manager
::PLUGIN_STATUS_UPGRADE
;
2716 } else if ($this->versiondb
> $this->versiondisk
) {
2717 return plugin_manager
::PLUGIN_STATUS_DOWNGRADE
;
2720 // $version = pi(); and similar funny jokes - hopefully Donald E. Knuth will never contribute to Moodle ;-)
2721 throw new coding_exception('Unable to determine plugin state, check the plugin versions');
2726 * Returns the information about plugin availability
2728 * True means that the plugin is enabled. False means that the plugin is
2729 * disabled. Null means that the information is not available, or the
2730 * plugin does not support configurable availability or the availability
2731 * can not be changed.
2735 public function is_enabled() {
2740 * Populates the property {@link $availableupdates} with the information provided by
2741 * available update checker
2743 * @param available_update_checker $provider the class providing the available update info
2745 public function check_available_updates(available_update_checker
$provider) {
2748 if (isset($CFG->updateminmaturity
)) {
2749 $minmaturity = $CFG->updateminmaturity
;
2751 // this can happen during the very first upgrade to 2.3
2752 $minmaturity = MATURITY_STABLE
;
2755 $this->availableupdates
= $provider->get_update_info($this->component
,
2756 array('minmaturity' => $minmaturity));
2760 * If there are updates for this plugin available, returns them.
2762 * Returns array of {@link available_update_info} objects, if some update
2763 * is available. Returns null if there is no update available or if the update
2764 * availability is unknown.
2766 * @return array|null
2768 public function available_updates() {
2770 if (empty($this->availableupdates
) or !is_array($this->availableupdates
)) {
2776 foreach ($this->availableupdates
as $availableupdate) {
2777 if ($availableupdate->version
> $this->versiondisk
) {
2778 $updates[] = $availableupdate;
2782 if (empty($updates)) {
2790 * Returns the node name used in admin settings menu for this plugin settings (if applicable)
2792 * @return null|string node name or null if plugin does not create settings node (default)
2794 public function get_settings_section_name() {
2799 * Returns the URL of the plugin settings screen
2801 * Null value means that the plugin either does not have the settings screen
2802 * or its location is not available via this library.
2804 * @return null|moodle_url
2806 public function get_settings_url() {
2807 $section = $this->get_settings_section_name();
2808 if ($section === null) {
2811 $settings = admin_get_root()->locate($section);
2812 if ($settings && $settings instanceof admin_settingpage
) {
2813 return new moodle_url('/admin/settings.php', array('section' => $section));
2814 } else if ($settings && $settings instanceof admin_externalpage
) {
2815 return new moodle_url($settings->url
);
2822 * Loads plugin settings to the settings tree
2824 * This function usually includes settings.php file in plugins folder.
2825 * Alternatively it can create a link to some settings page (instance of admin_externalpage)
2827 * @param part_of_admin_tree $adminroot
2828 * @param string $parentnodename
2829 * @param bool $hassiteconfig whether the current user has moodle/site:config capability
2831 public function load_settings(part_of_admin_tree
$adminroot, $parentnodename, $hassiteconfig) {
2835 * Should there be a way to uninstall the plugin via the administration UI
2837 * By default, uninstallation is allowed for all non-standard add-ons. Subclasses
2838 * may want to override this to allow uninstallation of all plugins (simply by
2839 * returning true unconditionally). Subplugins follow their parent plugin's
2840 * decision by default.
2842 * Note that even if true is returned, the core may still prohibit the uninstallation,
2843 * e.g. in case there are other plugins that depend on this one.
2847 public function is_uninstall_allowed() {
2849 if ($this->is_subplugin()) {
2850 return $this->get_plugin_manager()->get_plugin_info($this->get_parent_plugin())->is_uninstall_allowed();
2853 if ($this->is_standard()) {
2861 * Optional extra warning before uninstallation, for example number of uses in courses.
2865 public function get_uninstall_extra_warning() {
2870 * Returns the URL of the screen where this plugin can be uninstalled
2872 * Visiting that URL must be safe, that is a manual confirmation is needed
2873 * for actual uninstallation of the plugin. By default, URL to a common
2874 * uninstalling tool is returned.
2876 * @return moodle_url
2878 public function get_uninstall_url() {
2879 return $this->get_default_uninstall_url();
2883 * Returns relative directory of the plugin with heading '/'
2887 public function get_dir() {
2890 return substr($this->rootdir
, strlen($CFG->dirroot
));
2894 * Hook method to implement certain steps when uninstalling the plugin.
2896 * This hook is called by {@link plugin_manager::uninstall_plugin()} so
2897 * it is basically usable only for those plugin types that use the default
2898 * uninstall tool provided by {@link self::get_default_uninstall_url()}.
2900 * @param progress_trace $progress traces the process
2901 * @return bool true on success, false on failure
2903 public function uninstall(progress_trace
$progress) {
2908 * Returns URL to a script that handles common plugin uninstall procedure.
2910 * This URL is suitable for plugins that do not have their own UI
2913 * @return moodle_url
2915 protected final function get_default_uninstall_url() {
2916 return new moodle_url('/admin/plugins.php', array(
2917 'sesskey' => sesskey(),
2918 'uninstall' => $this->component
,
2924 * Provides access to plugin versions from the {config_plugins} table
2926 * @param string $plugin plugin name
2927 * @param bool $disablecache do not attempt to obtain data from the cache
2928 * @return int|bool the stored value or false if not found
2930 protected function get_version_from_config_plugins($plugin, $disablecache=false) {
2933 $cache = cache
::make('core', 'plugininfo_base');
2935 $pluginversions = $cache->get('versions_db');
2937 if ($pluginversions === false or $disablecache) {
2939 $pluginversions = $DB->get_records_menu('config_plugins', array('name' => 'version'), 'plugin', 'plugin,value');
2940 } catch (dml_exception
$e) {
2942 $pluginversions = array();
2944 $cache->set('versions_db', $pluginversions);
2947 if (isset($pluginversions[$plugin])) {
2948 return $pluginversions[$plugin];
2955 * Provides access to the plugin_manager singleton.
2957 * @return plugin_manmager
2959 protected function get_plugin_manager() {
2960 return plugin_manager
::instance();
2966 * General class for all plugin types that do not have their own class
2968 class plugininfo_general
extends plugininfo_base
{
2973 * Class for page side blocks
2975 class plugininfo_block
extends plugininfo_base
{
2977 public static function get_plugins($type, $typerootdir, $typeclass) {
2979 // get the information about blocks at the disk
2980 $blocks = parent
::get_plugins($type, $typerootdir, $typeclass);
2982 // add blocks missing from disk
2983 $blocksinfo = self
::get_blocks_info();
2984 foreach ($blocksinfo as $blockname => $blockinfo) {
2985 if (isset($blocks[$blockname])) {
2988 $plugin = new $typeclass();
2989 $plugin->type
= $type;
2990 $plugin->typerootdir
= $typerootdir;
2991 $plugin->name
= $blockname;
2992 $plugin->rootdir
= null;
2993 $plugin->displayname
= $blockname;
2994 $plugin->versiondb
= $blockinfo->version
;
2995 $plugin->init_is_standard();
2997 $blocks[$blockname] = $plugin;
3004 * Magic method getter, redirects to read only values.
3006 * For block plugins pretends the object has 'visible' property for compatibility
3007 * with plugins developed for Moodle version below 2.4
3009 * @param string $name
3012 public function __get($name) {
3013 if ($name === 'visible') {
3014 debugging('This is now an instance of plugininfo_block, please use $block->is_enabled() instead of $block->visible', DEBUG_DEVELOPER
);
3015 return ($this->is_enabled() !== false);
3017 return parent
::__get($name);
3020 public function init_display_name() {
3022 if (get_string_manager()->string_exists('pluginname', 'block_' . $this->name
)) {
3023 $this->displayname
= get_string('pluginname', 'block_' . $this->name
);
3025 } else if (($block = block_instance($this->name
)) !== false) {
3026 $this->displayname
= $block->get_title();
3029 parent
::init_display_name();
3033 public function load_db_version() {
3036 $blocksinfo = self
::get_blocks_info();
3037 if (isset($blocksinfo[$this->name
]->version
)) {
3038 $this->versiondb
= $blocksinfo[$this->name
]->version
;
3042 public function is_enabled() {
3044 $blocksinfo = self
::get_blocks_info();
3045 if (isset($blocksinfo[$this->name
]->visible
)) {
3046 if ($blocksinfo[$this->name
]->visible
) {
3052 return parent
::is_enabled();
3056 public function get_settings_section_name() {
3057 return 'blocksetting' . $this->name
;
3060 public function load_settings(part_of_admin_tree
$adminroot, $parentnodename, $hassiteconfig) {
3061 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3062 $ADMIN = $adminroot; // may be used in settings.php
3063 $block = $this; // also can be used inside settings.php
3064 $section = $this->get_settings_section_name();
3066 if (!$hassiteconfig ||
(($blockinstance = block_instance($this->name
)) === false)) {
3071 if ($blockinstance->has_config()) {
3072 if (file_exists($this->full_path('settings.php'))) {
3073 $settings = new admin_settingpage($section, $this->displayname
,
3074 'moodle/site:config', $this->is_enabled() === false);
3075 include($this->full_path('settings.php')); // this may also set $settings to null
3077 $blocksinfo = self
::get_blocks_info();
3078 $settingsurl = new moodle_url('/admin/block.php', array('block' => $blocksinfo[$this->name
]->id
));
3079 $settings = new admin_externalpage($section, $this->displayname
,
3080 $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
3084 $ADMIN->add($parentnodename, $settings);
3088 public function is_uninstall_allowed() {
3093 * Warnign with number of block instances.
3097 public function get_uninstall_extra_warning() {
3100 if (!$count = $DB->count_records('block_instances', array('blockname'=>$this->name
))) {
3104 return '<p>'.get_string('uninstallextraconfirmblock', 'core_plugin', array('instances'=>$count)).'</p>';
3108 * Provides access to the records in {block} table
3110 * @param bool $disablecache do not attempt to obtain data from the cache
3111 * @return array array of stdClasses
3113 protected static function get_blocks_info($disablecache=false) {
3116 $cache = cache
::make('core', 'plugininfo_block');
3118 $blocktypes = $cache->get('blocktypes');
3120 if ($blocktypes === false or $disablecache) {
3122 $blocktypes = $DB->get_records('block', null, 'name', 'name,id,version,visible');
3123 } catch (dml_exception
$e) {
3125 $blocktypes = array();
3127 $cache->set('blocktypes', $blocktypes);
3136 * Class for text filters
3138 class plugininfo_filter
extends plugininfo_base
{
3140 public static function get_plugins($type, $typerootdir, $typeclass) {
3145 // get the list of filters in /filter location
3146 $installed = filter_get_all_installed();
3148 foreach ($installed as $name => $displayname) {
3149 $plugin = new $typeclass();
3150 $plugin->type
= $type;
3151 $plugin->typerootdir
= $typerootdir;
3152 $plugin->name
= $name;
3153 $plugin->rootdir
= "$CFG->dirroot/filter/$name";
3154 $plugin->displayname
= $displayname;
3156 $plugin->load_disk_version();
3157 $plugin->load_db_version();
3158 $plugin->load_required_main_version();
3159 $plugin->init_is_standard();
3161 $filters[$plugin->name
] = $plugin;
3164 // Do not mess with filter registration here!
3166 $globalstates = self
::get_global_states();
3168 // make sure that all registered filters are installed, just in case
3169 foreach ($globalstates as $name => $info) {
3170 if (!isset($filters[$name])) {
3171 // oops, there is a record in filter_active but the filter is not installed
3172 $plugin = new $typeclass();
3173 $plugin->type
= $type;
3174 $plugin->typerootdir
= $typerootdir;
3175 $plugin->name
= $name;
3176 $plugin->rootdir
= "$CFG->dirroot/filter/$name";
3177 $plugin->displayname
= $name;
3179 $plugin->load_db_version();
3181 if (is_null($plugin->versiondb
)) {
3182 // this is a hack to stimulate 'Missing from disk' error
3183 // because $plugin->versiondisk will be null !== false
3184 $plugin->versiondb
= false;
3187 $filters[$plugin->name
] = $plugin;
3194 public function init_display_name() {
3195 // do nothing, the name is set in self::get_plugins()
3198 public function is_enabled() {
3200 $globalstates = self
::get_global_states();
3202 foreach ($globalstates as $name => $info) {
3203 if ($name === $this->name
) {
3204 if ($info->active
== TEXTFILTER_DISABLED
) {
3207 // it may be 'On' or 'Off, but available'
3216 public function get_settings_section_name() {
3217 return 'filtersetting' . $this->name
;
3220 public function load_settings(part_of_admin_tree
$adminroot, $parentnodename, $hassiteconfig) {
3221 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3222 $ADMIN = $adminroot; // may be used in settings.php
3223 $filter = $this; // also can be used inside settings.php
3226 if ($hassiteconfig && file_exists($this->full_path('filtersettings.php'))) {
3227 $section = $this->get_settings_section_name();
3228 $settings = new admin_settingpage($section, $this->displayname
,
3229 'moodle/site:config', $this->is_enabled() === false);
3230 include($this->full_path('filtersettings.php')); // this may also set $settings to null
3233 $ADMIN->add($parentnodename, $settings);
3237 public function is_uninstall_allowed() {
3241 public function get_uninstall_url() {
3242 return new moodle_url('/admin/filters.php', array('sesskey' => sesskey(), 'filterpath' => $this->name
, 'action' => 'delete'));
3246 * Provides access to the results of {@link filter_get_global_states()}
3247 * but indexed by the normalized filter name
3249 * The legacy filter name is available as ->legacyname property.
3251 * @param bool $disablecache do not attempt to obtain data from the cache
3254 protected static function get_global_states($disablecache=false) {
3257 $cache = cache
::make('core', 'plugininfo_filter');
3259 $globalstates = $cache->get('globalstates');
3261 if ($globalstates === false or $disablecache) {
3263 if (!$DB->get_manager()->table_exists('filter_active')) {
3264 // Not installed yet.
3265 $cache->set('globalstates', array());
3269 $globalstates = array();
3271 foreach (filter_get_global_states() as $name => $info) {
3272 if (strpos($name, '/') !== false) {
3273 // Skip existing before upgrade to new names.
3277 $filterinfo = new stdClass();
3278 $filterinfo->active
= $info->active
;
3279 $filterinfo->sortorder
= $info->sortorder
;
3280 $globalstates[$name] = $filterinfo;
3283 $cache->set('globalstates', $globalstates);
3286 return $globalstates;
3292 * Class for activity modules
3294 class plugininfo_mod
extends plugininfo_base
{
3296 public static function get_plugins($type, $typerootdir, $typeclass) {
3298 // get the information about plugins at the disk
3299 $modules = parent
::get_plugins($type, $typerootdir, $typeclass);
3301 // add modules missing from disk
3302 $modulesinfo = self
::get_modules_info();
3303 foreach ($modulesinfo as $modulename => $moduleinfo) {
3304 if (isset($modules[$modulename])) {
3307 $plugin = new $typeclass();
3308 $plugin->type
= $type;
3309 $plugin->typerootdir
= $typerootdir;
3310 $plugin->name
= $modulename;
3311 $plugin->rootdir
= null;
3312 $plugin->displayname
= $modulename;
3313 $plugin->versiondb
= $moduleinfo->version
;
3314 $plugin->init_is_standard();
3316 $modules[$modulename] = $plugin;
3323 * Magic method getter, redirects to read only values.
3325 * For module plugins we pretend the object has 'visible' property for compatibility
3326 * with plugins developed for Moodle version below 2.4
3328 * @param string $name
3331 public function __get($name) {
3332 if ($name === 'visible') {
3333 debugging('This is now an instance of plugininfo_mod, please use $module->is_enabled() instead of $module->visible', DEBUG_DEVELOPER
);
3334 return ($this->is_enabled() !== false);
3336 return parent
::__get($name);
3339 public function init_display_name() {
3340 if (get_string_manager()->string_exists('pluginname', $this->component
)) {
3341 $this->displayname
= get_string('pluginname', $this->component
);
3343 $this->displayname
= get_string('modulename', $this->component
);
3348 * Load the data from version.php.
3350 * @param bool $disablecache do not attempt to obtain data from the cache
3351 * @return object the data object defined in version.php.
3353 protected function load_version_php($disablecache=false) {
3355 $cache = cache
::make('core', 'plugininfo_mod');
3357 $versionsphp = $cache->get('versions_php');
3359 if (!$disablecache and $versionsphp !== false and isset($versionsphp[$this->component
])) {
3360 return $versionsphp[$this->component
];
3363 $versionfile = $this->full_path('version.php');
3365 $module = new stdClass();
3366 $plugin = new stdClass();
3367 if (is_readable($versionfile)) {
3368 include($versionfile);
3370 if (!isset($module->version
) and isset($plugin->version
)) {
3373 $versionsphp[$this->component
] = $module;
3374 $cache->set('versions_php', $versionsphp);
3379 public function load_db_version() {
3382 $modulesinfo = self
::get_modules_info();
3383 if (isset($modulesinfo[$this->name
]->version
)) {
3384 $this->versiondb
= $modulesinfo[$this->name
]->version
;
3388 public function is_enabled() {
3390 $modulesinfo = self
::get_modules_info();
3391 if (isset($modulesinfo[$this->name
]->visible
)) {
3392 if ($modulesinfo[$this->name
]->visible
) {
3398 return parent
::is_enabled();
3402 public function get_settings_section_name() {
3403 return 'modsetting' . $this->name
;
3406 public function load_settings(part_of_admin_tree
$adminroot, $parentnodename, $hassiteconfig) {
3407 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3408 $ADMIN = $adminroot; // may be used in settings.php
3409 $module = $this; // also can be used inside settings.php
3410 $section = $this->get_settings_section_name();
3412 $modulesinfo = self
::get_modules_info();
3414 if ($hassiteconfig && isset($modulesinfo[$this->name
]) && file_exists($this->full_path('settings.php'))) {
3415 $settings = new admin_settingpage($section, $this->displayname
,
3416 'moodle/site:config', $this->is_enabled() === false);
3417 include($this->full_path('settings.php')); // this may also set $settings to null
3420 $ADMIN->add($parentnodename, $settings);
3425 * Allow all activity modules but Forum to be uninstalled.
3427 * This exception for the Forum has been hard-coded in Moodle since ages,
3428 * we may want to re-think it one day.
3430 public function is_uninstall_allowed() {
3431 if ($this->name
=== 'forum') {
3439 * Return warning with number of activities and number of affected courses.
3443 public function get_uninstall_extra_warning() {
3446 if (!$module = $DB->get_record('modules', array('name'=>$this->name
))) {
3450 if (!$count = $DB->count_records('course_modules', array('module'=>$module->id
))) {
3454 $sql = "SELECT COUNT('x')
3457 FROM {course_modules}
3461 $courses = $DB->count_records_sql($sql, array('mid'=>$module->id
));
3463 return '<p>'.get_string('uninstallextraconfirmmod', 'core_plugin', array('instances'=>$count, 'courses'=>$courses)).'</p>';
3467 * Provides access to the records in {modules} table
3469 * @param bool $disablecache do not attempt to obtain data from the cache
3470 * @return array array of stdClasses
3472 protected static function get_modules_info($disablecache=false) {
3475 $cache = cache
::make('core', 'plugininfo_mod');
3477 $modulesinfo = $cache->get('modulesinfo');
3479 if ($modulesinfo === false or $disablecache) {
3481 $modulesinfo = $DB->get_records('modules', null, 'name', 'name,id,version,visible');
3482 } catch (dml_exception
$e) {
3484 $modulesinfo = array();
3486 $cache->set('modulesinfo', $modulesinfo);
3489 return $modulesinfo;
3495 * Class for question behaviours.
3497 class plugininfo_qbehaviour
extends plugininfo_base
{
3499 public function is_uninstall_allowed() {
3503 public function get_uninstall_url() {
3504 return new moodle_url('/admin/qbehaviours.php',
3505 array('delete' => $this->name
, 'sesskey' => sesskey()));
3511 * Class for question types
3513 class plugininfo_qtype
extends plugininfo_base
{
3515 public function is_uninstall_allowed() {
3519 public function get_uninstall_url() {
3520 return new moodle_url('/admin/qtypes.php',
3521 array('delete' => $this->name
, 'sesskey' => sesskey()));
3524 public function get_settings_section_name() {
3525 return 'qtypesetting' . $this->name
;
3528 public function load_settings(part_of_admin_tree
$adminroot, $parentnodename, $hassiteconfig) {
3529 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3530 $ADMIN = $adminroot; // may be used in settings.php
3531 $qtype = $this; // also can be used inside settings.php
3532 $section = $this->get_settings_section_name();
3535 $systemcontext = context_system
::instance();
3536 if (($hassiteconfig ||
has_capability('moodle/question:config', $systemcontext)) &&
3537 file_exists($this->full_path('settings.php'))) {
3538 $settings = new admin_settingpage($section, $this->displayname
,
3539 'moodle/question:config', $this->is_enabled() === false);
3540 include($this->full_path('settings.php')); // this may also set $settings to null
3543 $ADMIN->add($parentnodename, $settings);
3550 * Class for authentication plugins
3552 class plugininfo_auth
extends plugininfo_base
{
3554 public function is_enabled() {
3557 if (in_array($this->name
, array('nologin', 'manual'))) {
3558 // these two are always enabled and can't be disabled
3562 $enabled = array_flip(explode(',', $CFG->auth
));
3564 return isset($enabled[$this->name
]);
3567 public function get_settings_section_name() {
3568 return 'authsetting' . $this->name
;
3571 public function load_settings(part_of_admin_tree
$adminroot, $parentnodename, $hassiteconfig) {
3572 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3573 $ADMIN = $adminroot; // may be used in settings.php
3574 $auth = $this; // also to be used inside settings.php
3575 $section = $this->get_settings_section_name();
3578 if ($hassiteconfig) {
3579 if (file_exists($this->full_path('settings.php'))) {
3580 // TODO: finish implementation of common settings - locking, etc.
3581 $settings = new admin_settingpage($section, $this->displayname
,
3582 'moodle/site:config', $this->is_enabled() === false);
3583 include($this->full_path('settings.php')); // this may also set $settings to null
3585 $settingsurl = new moodle_url('/admin/auth_config.php', array('auth' => $this->name
));
3586 $settings = new admin_externalpage($section, $this->displayname
,
3587 $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
3591 $ADMIN->add($parentnodename, $settings);
3598 * Class for enrolment plugins
3600 class plugininfo_enrol
extends plugininfo_base
{
3602 public function is_enabled() {
3605 // We do not actually need whole enrolment classes here so we do not call
3606 // {@link enrol_get_plugins()}. Note that this may produce slightly different
3607 // results, for example if the enrolment plugin does not contain lib.php
3608 // but it is listed in $CFG->enrol_plugins_enabled
3610 $enabled = array_flip(explode(',', $CFG->enrol_plugins_enabled
));
3612 return isset($enabled[$this->name
]);
3615 public function get_settings_section_name() {
3616 if (file_exists($this->full_path('settings.php'))) {
3617 return 'enrolsettings' . $this->name
;
3623 public function load_settings(part_of_admin_tree
$adminroot, $parentnodename, $hassiteconfig) {
3624 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3626 if (!$hassiteconfig or !file_exists($this->full_path('settings.php'))) {
3629 $section = $this->get_settings_section_name();
3631 $ADMIN = $adminroot; // may be used in settings.php
3632 $enrol = $this; // also can be used inside settings.php
3633 $settings = new admin_settingpage($section, $this->displayname
,
3634 'moodle/site:config', $this->is_enabled() === false);
3636 include($this->full_path('settings.php')); // This may also set $settings to null!
3639 $ADMIN->add($parentnodename, $settings);
3643 public function is_uninstall_allowed() {
3644 if ($this->name
=== 'manual') {
3651 * Return warning with number of activities and number of affected courses.
3655 public function get_uninstall_extra_warning() {
3656 global $DB, $OUTPUT;
3658 $sql = "SELECT COUNT('x')
3659 FROM {user_enrolments} ue
3660 JOIN {enrol} e ON e.id = ue.enrolid
3661 WHERE e.enrol = :plugin";
3662 $count = $DB->count_records_sql($sql, array('plugin'=>$this->name
));
3668 $migrateurl = new moodle_url('/admin/enrol.php', array('action'=>'migrate', 'enrol'=>$this->name
, 'sesskey'=>sesskey()));
3669 $migrate = new single_button($migrateurl, get_string('migratetomanual', 'core_enrol'));
3670 $button = $OUTPUT->render($migrate);
3672 $result = '<p>'.get_string('uninstallextraconfirmenrol', 'core_plugin', array('enrolments'=>$count)).'</p>';
3681 * Class for messaging processors
3683 class plugininfo_message
extends plugininfo_base
{
3685 public function get_settings_section_name() {
3686 return 'messagesetting' . $this->name
;
3689 public function load_settings(part_of_admin_tree
$adminroot, $parentnodename, $hassiteconfig) {
3690 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3691 $ADMIN = $adminroot; // may be used in settings.php
3692 if (!$hassiteconfig) {
3695 $section = $this->get_settings_section_name();
3698 $processors = get_message_processors();
3699 if (isset($processors[$this->name
])) {
3700 $processor = $processors[$this->name
];
3701 if ($processor->available
&& $processor->hassettings
) {
3702 $settings = new admin_settingpage($section, $this->displayname
,
3703 'moodle/site:config', $this->is_enabled() === false);
3704 include($this->full_path('settings.php')); // this may also set $settings to null
3708 $ADMIN->add($parentnodename, $settings);
3713 * @see plugintype_interface::is_enabled()
3715 public function is_enabled() {
3716 $processors = get_message_processors();
3717 if (isset($processors[$this->name
])) {
3718 return $processors[$this->name
]->configured
&& $processors[$this->name
]->enabled
;
3720 return parent
::is_enabled();
3724 public function is_uninstall_allowed() {
3725 $processors = get_message_processors();
3726 if (isset($processors[$this->name
])) {
3734 * @see plugintype_interface::get_uninstall_url()
3736 public function get_uninstall_url() {
3737 $processors = get_message_processors();
3738 return new moodle_url('/admin/message.php', array('uninstall' => $processors[$this->name
]->id
, 'sesskey' => sesskey()));
3744 * Class for repositories
3746 class plugininfo_repository
extends plugininfo_base
{
3748 public function is_enabled() {
3750 $enabled = self
::get_enabled_repositories();
3752 return isset($enabled[$this->name
]);
3755 public function get_settings_section_name() {
3756 return 'repositorysettings'.$this->name
;
3759 public function load_settings(part_of_admin_tree
$adminroot, $parentnodename, $hassiteconfig) {
3760 if ($hassiteconfig && $this->is_enabled()) {
3761 // completely no access to repository setting when it is not enabled
3762 $sectionname = $this->get_settings_section_name();
3763 $settingsurl = new moodle_url('/admin/repository.php',
3764 array('sesskey' => sesskey(), 'action' => 'edit', 'repos' => $this->name
));
3765 $settings = new admin_externalpage($sectionname, $this->displayname
,
3766 $settingsurl, 'moodle/site:config', false);
3767 $adminroot->add($parentnodename, $settings);
3772 * Provides access to the records in {repository} table
3774 * @param bool $disablecache do not attempt to obtain data from the cache
3775 * @return array array of stdClasses
3777 protected static function get_enabled_repositories($disablecache=false) {
3780 $cache = cache
::make('core', 'plugininfo_repository');
3782 $enabled = $cache->get('enabled');
3784 if ($enabled === false or $disablecache) {
3785 $enabled = $DB->get_records('repository', null, 'type', 'type,visible,sortorder');
3786 $cache->set('enabled', $enabled);
3795 * Class for portfolios
3797 class plugininfo_portfolio
extends plugininfo_base
{
3799 public function is_enabled() {
3801 $enabled = self
::get_enabled_portfolios();
3803 return isset($enabled[$this->name
]);
3807 * Returns list of enabled portfolio plugins
3809 * Portfolio plugin is enabled if there is at least one record in the {portfolio_instance}
3812 * @param bool $disablecache do not attempt to obtain data from the cache
3813 * @return array array of stdClasses with properties plugin and visible indexed by plugin
3815 protected static function get_enabled_portfolios($disablecache=false) {
3818 $cache = cache
::make('core', 'plugininfo_portfolio');
3820 $enabled = $cache->get('enabled');
3822 if ($enabled === false or $disablecache) {
3824 $instances = $DB->get_recordset('portfolio_instance', null, '', 'plugin,visible');
3825 foreach ($instances as $instance) {
3826 if (isset($enabled[$instance->plugin
])) {
3827 if ($instance->visible
) {
3828 $enabled[$instance->plugin
]->visible
= $instance->visible
;
3831 $enabled[$instance->plugin
] = $instance;
3834 $instances->close();
3835 $cache->set('enabled', $enabled);
3846 class plugininfo_theme
extends plugininfo_base
{
3848 public function is_enabled() {
3851 if ((!empty($CFG->theme
) and $CFG->theme
=== $this->name
) or
3852 (!empty($CFG->themelegacy
) and $CFG->themelegacy
=== $this->name
)) {
3855 return parent
::is_enabled();
3862 * Class representing an MNet service
3864 class plugininfo_mnetservice
extends plugininfo_base
{
3866 public function is_enabled() {
3869 if (empty($CFG->mnet_dispatcher_mode
) ||
$CFG->mnet_dispatcher_mode
!== 'strict') {
3872 return parent
::is_enabled();
3879 * Class for admin tool plugins
3881 class plugininfo_tool
extends plugininfo_base
{
3883 public function is_uninstall_allowed() {
3890 * Class for admin tool plugins
3892 class plugininfo_report
extends plugininfo_base
{
3894 public function is_uninstall_allowed() {
3901 * Class for local plugins
3903 class plugininfo_local
extends plugininfo_base
{
3905 public function is_uninstall_allowed() {
3911 * Class for HTML editors
3913 class plugininfo_editor
extends plugininfo_base
{
3915 public function get_settings_section_name() {
3916 return 'editorsettings' . $this->name
;
3919 public function load_settings(part_of_admin_tree
$adminroot, $parentnodename, $hassiteconfig) {
3920 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3921 $ADMIN = $adminroot; // may be used in settings.php
3922 $editor = $this; // also can be used inside settings.php
3923 $section = $this->get_settings_section_name();
3926 if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3927 $settings = new admin_settingpage($section, $this->displayname
,
3928 'moodle/site:config', $this->is_enabled() === false);
3929 include($this->full_path('settings.php')); // this may also set $settings to null
3932 $ADMIN->add($parentnodename, $settings);
3937 * Basic textarea editor can not be uninstalled.
3939 public function is_uninstall_allowed() {
3940 if ($this->name
=== 'textarea') {
3948 * Returns the information about plugin availability
3950 * True means that the plugin is enabled. False means that the plugin is
3951 * disabled. Null means that the information is not available, or the
3952 * plugin does not support configurable availability or the availability
3953 * can not be changed.
3957 public function is_enabled() {
3959 if (empty($CFG->texteditors
)) {
3960 $CFG->texteditors
= 'tinymce,textarea';
3962 if (in_array($this->name
, explode(',', $CFG->texteditors
))) {
3970 * Class for plagiarism plugins
3972 class plugininfo_plagiarism
extends plugininfo_base
{
3974 public function get_settings_section_name() {
3975 return 'plagiarism'. $this->name
;
3978 public function load_settings(part_of_admin_tree
$adminroot, $parentnodename, $hassiteconfig) {
3979 // plagiarism plugin just redirect to settings.php in the plugins directory
3980 if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3981 $section = $this->get_settings_section_name();
3982 $settingsurl = new moodle_url($this->get_dir().'/settings.php');
3983 $settings = new admin_externalpage($section, $this->displayname
,
3984 $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
3985 $adminroot->add($parentnodename, $settings);
3989 public function is_uninstall_allowed() {
3995 * Class for webservice protocols
3997 class plugininfo_webservice
extends plugininfo_base
{
3999 public function get_settings_section_name() {
4000 return 'webservicesetting' . $this->name
;
4003 public function load_settings(part_of_admin_tree
$adminroot, $parentnodename, $hassiteconfig) {
4004 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
4005 $ADMIN = $adminroot; // may be used in settings.php
4006 $webservice = $this; // also can be used inside settings.php
4007 $section = $this->get_settings_section_name();
4010 if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
4011 $settings = new admin_settingpage($section, $this->displayname
,
4012 'moodle/site:config', $this->is_enabled() === false);
4013 include($this->full_path('settings.php')); // this may also set $settings to null
4016 $ADMIN->add($parentnodename, $settings);
4020 public function is_enabled() {
4022 if (empty($CFG->enablewebservices
)) {
4025 $active_webservices = empty($CFG->webserviceprotocols
) ?
array() : explode(',', $CFG->webserviceprotocols
);
4026 if (in_array($this->name
, $active_webservices)) {
4032 public function is_uninstall_allowed() {
4038 * Class for course formats
4040 class plugininfo_format
extends plugininfo_base
{
4043 * Gathers and returns the information about all plugins of the given type
4045 * @param string $type the name of the plugintype, eg. mod, auth or workshopform
4046 * @param string $typerootdir full path to the location of the plugin dir
4047 * @param string $typeclass the name of the actually called class
4048 * @return array of plugintype classes, indexed by the plugin name
4050 public static function get_plugins($type, $typerootdir, $typeclass) {
4052 $formats = parent
::get_plugins($type, $typerootdir, $typeclass);
4053 require_once($CFG->dirroot
.'/course/lib.php');
4054 $order = get_sorted_course_formats();
4055 $sortedformats = array();
4056 foreach ($order as $formatname) {
4057 $sortedformats[$formatname] = $formats[$formatname];
4059 return $sortedformats;
4062 public function get_settings_section_name() {
4063 return 'formatsetting' . $this->name
;
4066 public function load_settings(part_of_admin_tree
$adminroot, $parentnodename, $hassiteconfig) {
4067 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
4068 $ADMIN = $adminroot; // also may be used in settings.php
4069 $section = $this->get_settings_section_name();
4072 if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
4073 $settings = new admin_settingpage($section, $this->displayname
,
4074 'moodle/site:config', $this->is_enabled() === false);
4075 include($this->full_path('settings.php')); // this may also set $settings to null
4078 $ADMIN->add($parentnodename, $settings);
4082 public function is_enabled() {
4083 return !get_config($this->component
, 'disabled');
4086 public function is_uninstall_allowed() {
4087 if ($this->name
!== get_config('moodlecourse', 'format') && $this->name
!== 'site') {
4094 public function get_uninstall_extra_warning() {
4097 $coursecount = $DB->count_records('course', array('format' => $this->name
));
4099 if (!$coursecount) {
4103 $defaultformat = $this->get_plugin_manager()->plugin_name('format_'.get_config('moodlecourse', 'format'));
4104 $message = get_string(
4105 'formatuninstallwithcourses', 'core_admin',
4106 (object)array('count' => $coursecount, 'format' => $this->displayname
,
4107 'defaultformat' => $defaultformat));