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