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