Merge branch 'MDL-13114-master' of git://github.com/FMCorz/moodle
[moodle.git] / lib / pluginlib.php
blob2f2b72d4e93dd6de155d0c3aaa0e21b09228b127
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
18 /**
19 * Defines classes used for plugins management
21 * This library provides a unified interface to various plugin types in
22 * Moodle. It is mainly used by the plugins management admin page and the
23 * plugins check page during the upgrade.
25 * @package core
26 * @subpackage admin
27 * @copyright 2011 David Mudrak <david@moodle.com>
28 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
31 defined('MOODLE_INTERNAL') || die();
33 /**
34 * Singleton class providing general plugins management functionality
36 class plugin_manager {
38 /** the plugin is shipped with standard Moodle distribution */
39 const PLUGIN_SOURCE_STANDARD = 'std';
40 /** the plugin is added extension */
41 const PLUGIN_SOURCE_EXTENSION = 'ext';
43 /** the plugin uses neither database nor capabilities, no versions */
44 const PLUGIN_STATUS_NODB = 'nodb';
45 /** the plugin is up-to-date */
46 const PLUGIN_STATUS_UPTODATE = 'uptodate';
47 /** the plugin is about to be installed */
48 const PLUGIN_STATUS_NEW = 'new';
49 /** the plugin is about to be upgraded */
50 const PLUGIN_STATUS_UPGRADE = 'upgrade';
51 /** the standard plugin is about to be deleted */
52 const PLUGIN_STATUS_DELETE = 'delete';
53 /** the version at the disk is lower than the one already installed */
54 const PLUGIN_STATUS_DOWNGRADE = 'downgrade';
55 /** the plugin is installed but missing from disk */
56 const PLUGIN_STATUS_MISSING = 'missing';
58 /** @var plugin_manager holds the singleton instance */
59 protected static $singletoninstance;
60 /** @var array of raw plugins information */
61 protected $pluginsinfo = null;
62 /** @var array of raw subplugins information */
63 protected $subpluginsinfo = null;
65 /**
66 * Direct initiation not allowed, use the factory method {@link self::instance()}
68 protected function __construct() {
71 /**
72 * Sorry, this is singleton
74 protected function __clone() {
77 /**
78 * Factory method for this class
80 * @return plugin_manager the singleton instance
82 public static function instance() {
83 if (is_null(self::$singletoninstance)) {
84 self::$singletoninstance = new self();
86 return self::$singletoninstance;
89 /**
90 * Reset any caches
91 * @param bool $phpunitreset
93 public static function reset_caches($phpunitreset = false) {
94 if ($phpunitreset) {
95 self::$singletoninstance = null;
99 /**
100 * Returns the result of {@link get_plugin_types()} ordered for humans
102 * @see self::reorder_plugin_types()
103 * @param bool $fullpaths false means relative paths from dirroot
104 * @return array (string)name => (string)location
106 public function get_plugin_types($fullpaths = true) {
107 return $this->reorder_plugin_types(get_plugin_types($fullpaths));
111 * Returns list of known plugins of the given type
113 * This method returns the subset of the tree returned by {@link self::get_plugins()}.
114 * If the given type is not known, empty array is returned.
116 * @param string $type plugin type, e.g. 'mod' or 'workshopallocation'
117 * @param bool $disablecache force reload, cache can be used otherwise
118 * @return array (string)plugin name (e.g. 'workshop') => corresponding subclass of {@link plugininfo_base}
120 public function get_plugins_of_type($type, $disablecache=false) {
122 $plugins = $this->get_plugins($disablecache);
124 if (!isset($plugins[$type])) {
125 return array();
128 return $plugins[$type];
132 * Returns a tree of known plugins and information about them
134 * @param bool $disablecache force reload, cache can be used otherwise
135 * @return array 2D array. The first keys are plugin type names (e.g. qtype);
136 * the second keys are the plugin local name (e.g. multichoice); and
137 * the values are the corresponding objects extending {@link plugininfo_base}
139 public function get_plugins($disablecache=false) {
140 global $CFG;
142 if ($disablecache or is_null($this->pluginsinfo)) {
143 // Hack: include mod and editor subplugin management classes first,
144 // the adminlib.php is supposed to contain extra admin settings too.
145 require_once($CFG->libdir.'/adminlib.php');
146 foreach (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) {
655 $standard_plugins = array(
657 'assignment' => array(
658 'offline', 'online', 'upload', 'uploadsingle'
661 'assignsubmission' => array(
662 'comments', 'file', 'onlinetext'
665 'assignfeedback' => array(
666 'comments', 'file', 'offline'
669 'auth' => array(
670 'cas', 'db', 'email', 'fc', 'imap', 'ldap', 'manual', 'mnet',
671 'nntp', 'nologin', 'none', 'pam', 'pop3', 'radius',
672 'shibboleth', 'webservice'
675 'block' => array(
676 'activity_modules', 'admin_bookmarks', 'badges', 'blog_menu',
677 'blog_recent', 'blog_tags', 'calendar_month',
678 'calendar_upcoming', 'comments', 'community',
679 'completionstatus', 'course_list', 'course_overview',
680 'course_summary', 'feedback', 'glossary_random', 'html',
681 'login', 'mentees', 'messages', 'mnet_hosts', 'myprofile',
682 'navigation', 'news_items', 'online_users', 'participants',
683 'private_files', 'quiz_results', 'recent_activity',
684 'rss_client', 'search_forums', 'section_links',
685 'selfcompletion', 'settings', 'site_main_menu',
686 'social_activities', 'tag_flickr', 'tag_youtube', 'tags'
689 'booktool' => array(
690 'exportimscp', 'importhtml', 'print'
693 'cachelock' => array(
694 'file'
697 'cachestore' => array(
698 'file', 'memcache', 'memcached', 'mongodb', 'session', 'static'
701 'coursereport' => array(
702 //deprecated!
705 'datafield' => array(
706 'checkbox', 'date', 'file', 'latlong', 'menu', 'multimenu',
707 'number', 'picture', 'radiobutton', 'text', 'textarea', 'url'
710 'datapreset' => array(
711 'imagegallery'
714 'editor' => array(
715 'textarea', 'tinymce'
718 'enrol' => array(
719 'category', 'cohort', 'database', 'flatfile',
720 'guest', 'imsenterprise', 'ldap', 'manual', 'meta', 'mnet',
721 'paypal', 'self'
724 'filter' => array(
725 'activitynames', 'algebra', 'censor', 'emailprotect',
726 'emoticon', 'mediaplugin', 'multilang', 'tex', 'tidy',
727 'urltolink', 'data', 'glossary'
730 'format' => array(
731 'scorm', 'social', 'topics', 'weeks'
734 'gradeexport' => array(
735 'ods', 'txt', 'xls', 'xml'
738 'gradeimport' => array(
739 'csv', 'xml'
742 'gradereport' => array(
743 'grader', 'outcomes', 'overview', 'user'
746 'gradingform' => array(
747 'rubric', 'guide'
750 'local' => array(
753 'message' => array(
754 'email', 'jabber', 'popup'
757 'mnetservice' => array(
758 'enrol'
761 'mod' => array(
762 'assign', 'assignment', 'book', 'chat', 'choice', 'data', 'feedback', 'folder',
763 'forum', 'glossary', 'imscp', 'label', 'lesson', 'lti', 'page',
764 'quiz', 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop'
767 'plagiarism' => array(
770 'portfolio' => array(
771 'boxnet', 'download', 'flickr', 'googledocs', 'mahara', 'picasa'
774 'profilefield' => array(
775 'checkbox', 'datetime', 'menu', 'text', 'textarea'
778 'qbehaviour' => array(
779 'adaptive', 'adaptivenopenalty', 'deferredcbm',
780 'deferredfeedback', 'immediatecbm', 'immediatefeedback',
781 'informationitem', 'interactive', 'interactivecountback',
782 'manualgraded', 'missing'
785 'qformat' => array(
786 'aiken', 'blackboard_six', 'examview', 'gift',
787 'learnwise', 'missingword', 'multianswer', 'webct',
788 'xhtml', 'xml'
791 'qtype' => array(
792 'calculated', 'calculatedmulti', 'calculatedsimple',
793 'description', 'essay', 'match', 'missingtype', 'multianswer',
794 'multichoice', 'numerical', 'random', 'randomsamatch',
795 'shortanswer', 'truefalse'
798 'quiz' => array(
799 'grading', 'overview', 'responses', 'statistics'
802 'quizaccess' => array(
803 'delaybetweenattempts', 'ipaddress', 'numattempts', 'openclosedate',
804 'password', 'safebrowser', 'securewindow', 'timelimit'
807 'report' => array(
808 'backups', 'completion', 'configlog', 'courseoverview',
809 'log', 'loglive', 'outline', 'participation', 'progress', 'questioninstances', 'security', 'stats', 'performance'
812 'repository' => array(
813 'alfresco', 'boxnet', 'coursefiles', 'dropbox', 'equella', 'filesystem',
814 'flickr', 'flickr_public', 'googledocs', 'local', 'merlot',
815 'picasa', 'recent', 'skydrive', 's3', 'upload', 'url', 'user', 'webdav',
816 'wikimedia', 'youtube'
819 'scormreport' => array(
820 'basic',
821 'interactions',
822 'graphs'
825 'tinymce' => array(
826 'ctrlhelp', 'dragmath', 'moodleemoticon', 'moodleimage', 'moodlemedia', 'moodlenolink', 'spellchecker',
827 'pdw', 'wrap'
830 'theme' => array(
831 'afterburner', 'anomaly', 'arialist', 'base', 'binarius', 'bootstrapbase',
832 'boxxie', 'brick', 'canvas', 'clean', 'formal_white', 'formfactor',
833 'fusion', 'leatherbound', 'magazine', 'mymobile', 'nimble',
834 'nonzero', 'overlay', 'serenity', 'sky_high', 'splash',
835 'standard', 'standardold'
838 'tool' => array(
839 'assignmentupgrade', 'behat', 'capability', 'customlang',
840 'dbtransfer', 'generator', 'health', 'innodb', 'installaddon',
841 'langimport', 'multilangupgrade', 'phpunit', 'profiling',
842 'qeupgradehelper', 'replace', 'spamcleaner', 'timezoneimport',
843 'unittest', 'uploadcourse', 'uploaduser', 'unsuproles', 'xmldb'
846 'webservice' => array(
847 'amf', 'rest', 'soap', 'xmlrpc'
850 'workshopallocation' => array(
851 'manual', 'random', 'scheduled'
854 'workshopeval' => array(
855 'best'
858 'workshopform' => array(
859 'accumulative', 'comments', 'numerrors', 'rubric'
863 if (isset($standard_plugins[$type])) {
864 return $standard_plugins[$type];
865 } else {
866 return false;
871 * Wrapper for the core function {@link normalize_component()}.
873 * This is here just to make it possible to mock it in unit tests.
875 * @param string $component
876 * @return array
878 protected function normalize_component($component) {
879 return normalize_component($component);
883 * Reorders plugin types into a sequence to be displayed
885 * For technical reasons, plugin types returned by {@link get_plugin_types()} are
886 * in a certain order that does not need to fit the expected order for the display.
887 * Particularly, activity modules should be displayed first as they represent the
888 * real heart of Moodle. They should be followed by other plugin types that are
889 * used to build the courses (as that is what one expects from LMS). After that,
890 * other supportive plugin types follow.
892 * @param array $types associative array
893 * @return array same array with altered order of items
895 protected function reorder_plugin_types(array $types) {
896 $fix = array(
897 'mod' => $types['mod'],
898 'block' => $types['block'],
899 'qtype' => $types['qtype'],
900 'qbehaviour' => $types['qbehaviour'],
901 'qformat' => $types['qformat'],
902 'filter' => $types['filter'],
903 'enrol' => $types['enrol'],
905 foreach ($types as $type => $path) {
906 if (!isset($fix[$type])) {
907 $fix[$type] = $path;
910 return $fix;
914 * Check if the given directory can be removed by the web server process.
916 * This recursively checks that the given directory and all its contents
917 * it writable.
919 * @param string $fullpath
920 * @return boolean
922 protected function is_directory_removable($fullpath) {
924 if (!is_writable($fullpath)) {
925 return false;
928 if (is_dir($fullpath)) {
929 $handle = opendir($fullpath);
930 } else {
931 return false;
934 $result = true;
936 while ($filename = readdir($handle)) {
938 if ($filename === '.' or $filename === '..') {
939 continue;
942 $subfilepath = $fullpath.'/'.$filename;
944 if (is_dir($subfilepath)) {
945 $result = $result && $this->is_directory_removable($subfilepath);
947 } else {
948 $result = $result && is_writable($subfilepath);
952 closedir($handle);
954 return $result;
958 * Helper method that implements common uninstall prerequisities
960 * @param plugininfo_base $pluginfo
961 * @return bool
963 protected function common_uninstall_check(plugininfo_base $pluginfo) {
965 if (!$pluginfo->is_uninstall_allowed()) {
966 // The plugin's plugininfo class declares it should not be uninstalled.
967 return false;
970 if ($pluginfo->get_status() === plugin_manager::PLUGIN_STATUS_NEW) {
971 // The plugin is not installed. It should be either installed or removed from the disk.
972 // Relying on this temporary state may be tricky.
973 return false;
976 if (is_null($pluginfo->get_uninstall_url())) {
977 // Backwards compatibility.
978 debugging('plugininfo_base subclasses should use is_uninstall_allowed() instead of returning null in get_uninstall_url()',
979 DEBUG_DEVELOPER);
980 return false;
983 return true;
989 * General exception thrown by the {@link available_update_checker} class
991 class available_update_checker_exception extends moodle_exception {
994 * @param string $errorcode exception description identifier
995 * @param mixed $debuginfo debugging data to display
997 public function __construct($errorcode, $debuginfo=null) {
998 parent::__construct($errorcode, 'core_plugin', '', null, print_r($debuginfo, true));
1004 * Singleton class that handles checking for available updates
1006 class available_update_checker {
1008 /** @var available_update_checker holds the singleton instance */
1009 protected static $singletoninstance;
1010 /** @var null|int the timestamp of when the most recent response was fetched */
1011 protected $recentfetch = null;
1012 /** @var null|array the recent response from the update notification provider */
1013 protected $recentresponse = null;
1014 /** @var null|string the numerical version of the local Moodle code */
1015 protected $currentversion = null;
1016 /** @var null|string the release info of the local Moodle code */
1017 protected $currentrelease = null;
1018 /** @var null|string branch of the local Moodle code */
1019 protected $currentbranch = null;
1020 /** @var array of (string)frankestyle => (string)version list of additional plugins deployed at this site */
1021 protected $currentplugins = array();
1024 * Direct initiation not allowed, use the factory method {@link self::instance()}
1026 protected function __construct() {
1030 * Sorry, this is singleton
1032 protected function __clone() {
1036 * Factory method for this class
1038 * @return available_update_checker the singleton instance
1040 public static function instance() {
1041 if (is_null(self::$singletoninstance)) {
1042 self::$singletoninstance = new self();
1044 return self::$singletoninstance;
1048 * Reset any caches
1049 * @param bool $phpunitreset
1051 public static function reset_caches($phpunitreset = false) {
1052 if ($phpunitreset) {
1053 self::$singletoninstance = null;
1058 * Returns the timestamp of the last execution of {@link fetch()}
1060 * @return int|null null if it has never been executed or we don't known
1062 public function get_last_timefetched() {
1064 $this->restore_response();
1066 if (!empty($this->recentfetch)) {
1067 return $this->recentfetch;
1069 } else {
1070 return null;
1075 * Fetches the available update status from the remote site
1077 * @throws available_update_checker_exception
1079 public function fetch() {
1080 $response = $this->get_response();
1081 $this->validate_response($response);
1082 $this->store_response($response);
1086 * Returns the available update information for the given component
1088 * This method returns null if the most recent response does not contain any information
1089 * about it. The returned structure is an array of available updates for the given
1090 * component. Each update info is an object with at least one property called
1091 * 'version'. Other possible properties are 'release', 'maturity', 'url' and 'downloadurl'.
1093 * For the 'core' component, the method returns real updates only (those with higher version).
1094 * For all other components, the list of all known remote updates is returned and the caller
1095 * (usually the {@link plugin_manager}) is supposed to make the actual comparison of versions.
1097 * @param string $component frankenstyle
1098 * @param array $options with supported keys 'minmaturity' and/or 'notifybuilds'
1099 * @return null|array null or array of available_update_info objects
1101 public function get_update_info($component, array $options = array()) {
1103 if (!isset($options['minmaturity'])) {
1104 $options['minmaturity'] = 0;
1107 if (!isset($options['notifybuilds'])) {
1108 $options['notifybuilds'] = false;
1111 if ($component == 'core') {
1112 $this->load_current_environment();
1115 $this->restore_response();
1117 if (empty($this->recentresponse['updates'][$component])) {
1118 return null;
1121 $updates = array();
1122 foreach ($this->recentresponse['updates'][$component] as $info) {
1123 $update = new available_update_info($component, $info);
1124 if (isset($update->maturity) and ($update->maturity < $options['minmaturity'])) {
1125 continue;
1127 if ($component == 'core') {
1128 if ($update->version <= $this->currentversion) {
1129 continue;
1131 if (empty($options['notifybuilds']) and $this->is_same_release($update->release)) {
1132 continue;
1135 $updates[] = $update;
1138 if (empty($updates)) {
1139 return null;
1142 return $updates;
1146 * The method being run via cron.php
1148 public function cron() {
1149 global $CFG;
1151 if (!$this->cron_autocheck_enabled()) {
1152 $this->cron_mtrace('Automatic check for available updates not enabled, skipping.');
1153 return;
1156 $now = $this->cron_current_timestamp();
1158 if ($this->cron_has_fresh_fetch($now)) {
1159 $this->cron_mtrace('Recently fetched info about available updates is still fresh enough, skipping.');
1160 return;
1163 if ($this->cron_has_outdated_fetch($now)) {
1164 $this->cron_mtrace('Outdated or missing info about available updates, forced fetching ... ', '');
1165 $this->cron_execute();
1166 return;
1169 $offset = $this->cron_execution_offset();
1170 $start = mktime(1, 0, 0, date('n', $now), date('j', $now), date('Y', $now)); // 01:00 AM today local time
1171 if ($now > $start + $offset) {
1172 $this->cron_mtrace('Regular daily check for available updates ... ', '');
1173 $this->cron_execute();
1174 return;
1178 /// end of public API //////////////////////////////////////////////////////
1181 * Makes cURL request to get data from the remote site
1183 * @return string raw request result
1184 * @throws available_update_checker_exception
1186 protected function get_response() {
1187 global $CFG;
1188 require_once($CFG->libdir.'/filelib.php');
1190 $curl = new curl(array('proxy' => true));
1191 $response = $curl->post($this->prepare_request_url(), $this->prepare_request_params(), $this->prepare_request_options());
1192 $curlerrno = $curl->get_errno();
1193 if (!empty($curlerrno)) {
1194 throw new available_update_checker_exception('err_response_curl', 'cURL error '.$curlerrno.': '.$curl->error);
1196 $curlinfo = $curl->get_info();
1197 if ($curlinfo['http_code'] != 200) {
1198 throw new available_update_checker_exception('err_response_http_code', $curlinfo['http_code']);
1200 return $response;
1204 * Makes sure the response is valid, has correct API format etc.
1206 * @param string $response raw response as returned by the {@link self::get_response()}
1207 * @throws available_update_checker_exception
1209 protected function validate_response($response) {
1211 $response = $this->decode_response($response);
1213 if (empty($response)) {
1214 throw new available_update_checker_exception('err_response_empty');
1217 if (empty($response['status']) or $response['status'] !== 'OK') {
1218 throw new available_update_checker_exception('err_response_status', $response['status']);
1221 if (empty($response['apiver']) or $response['apiver'] !== '1.2') {
1222 throw new available_update_checker_exception('err_response_format_version', $response['apiver']);
1225 if (empty($response['forbranch']) or $response['forbranch'] !== moodle_major_version(true)) {
1226 throw new available_update_checker_exception('err_response_target_version', $response['forbranch']);
1231 * Decodes the raw string response from the update notifications provider
1233 * @param string $response as returned by {@link self::get_response()}
1234 * @return array decoded response structure
1236 protected function decode_response($response) {
1237 return json_decode($response, true);
1241 * Stores the valid fetched response for later usage
1243 * This implementation uses the config_plugins table as the permanent storage.
1245 * @param string $response raw valid data returned by {@link self::get_response()}
1247 protected function store_response($response) {
1249 set_config('recentfetch', time(), 'core_plugin');
1250 set_config('recentresponse', $response, 'core_plugin');
1252 $this->restore_response(true);
1256 * Loads the most recent raw response record we have fetched
1258 * After this method is called, $this->recentresponse is set to an array. If the
1259 * array is empty, then either no data have been fetched yet or the fetched data
1260 * do not have expected format (and thence they are ignored and a debugging
1261 * message is displayed).
1263 * This implementation uses the config_plugins table as the permanent storage.
1265 * @param bool $forcereload reload even if it was already loaded
1267 protected function restore_response($forcereload = false) {
1269 if (!$forcereload and !is_null($this->recentresponse)) {
1270 // we already have it, nothing to do
1271 return;
1274 $config = get_config('core_plugin');
1276 if (!empty($config->recentresponse) and !empty($config->recentfetch)) {
1277 try {
1278 $this->validate_response($config->recentresponse);
1279 $this->recentfetch = $config->recentfetch;
1280 $this->recentresponse = $this->decode_response($config->recentresponse);
1281 } catch (available_update_checker_exception $e) {
1282 // The server response is not valid. Behave as if no data were fetched yet.
1283 // This may happen when the most recent update info (cached locally) has been
1284 // fetched with the previous branch of Moodle (like during an upgrade from 2.x
1285 // to 2.y) or when the API of the response has changed.
1286 $this->recentresponse = array();
1289 } else {
1290 $this->recentresponse = array();
1295 * Compares two raw {@link $recentresponse} records and returns the list of changed updates
1297 * This method is used to populate potential update info to be sent to site admins.
1299 * @param array $old
1300 * @param array $new
1301 * @throws available_update_checker_exception
1302 * @return array parts of $new['updates'] that have changed
1304 protected function compare_responses(array $old, array $new) {
1306 if (empty($new)) {
1307 return array();
1310 if (!array_key_exists('updates', $new)) {
1311 throw new available_update_checker_exception('err_response_format');
1314 if (empty($old)) {
1315 return $new['updates'];
1318 if (!array_key_exists('updates', $old)) {
1319 throw new available_update_checker_exception('err_response_format');
1322 $changes = array();
1324 foreach ($new['updates'] as $newcomponent => $newcomponentupdates) {
1325 if (empty($old['updates'][$newcomponent])) {
1326 $changes[$newcomponent] = $newcomponentupdates;
1327 continue;
1329 foreach ($newcomponentupdates as $newcomponentupdate) {
1330 $inold = false;
1331 foreach ($old['updates'][$newcomponent] as $oldcomponentupdate) {
1332 if ($newcomponentupdate['version'] == $oldcomponentupdate['version']) {
1333 $inold = true;
1336 if (!$inold) {
1337 if (!isset($changes[$newcomponent])) {
1338 $changes[$newcomponent] = array();
1340 $changes[$newcomponent][] = $newcomponentupdate;
1345 return $changes;
1349 * Returns the URL to send update requests to
1351 * During the development or testing, you can set $CFG->alternativeupdateproviderurl
1352 * to a custom URL that will be used. Otherwise the standard URL will be returned.
1354 * @return string URL
1356 protected function prepare_request_url() {
1357 global $CFG;
1359 if (!empty($CFG->config_php_settings['alternativeupdateproviderurl'])) {
1360 return $CFG->config_php_settings['alternativeupdateproviderurl'];
1361 } else {
1362 return 'https://download.moodle.org/api/1.2/updates.php';
1367 * Sets the properties currentversion, currentrelease, currentbranch and currentplugins
1369 * @param bool $forcereload
1371 protected function load_current_environment($forcereload=false) {
1372 global $CFG;
1374 if (!is_null($this->currentversion) and !$forcereload) {
1375 // nothing to do
1376 return;
1379 $version = null;
1380 $release = null;
1382 require($CFG->dirroot.'/version.php');
1383 $this->currentversion = $version;
1384 $this->currentrelease = $release;
1385 $this->currentbranch = moodle_major_version(true);
1387 $pluginman = plugin_manager::instance();
1388 foreach ($pluginman->get_plugins() as $type => $plugins) {
1389 foreach ($plugins as $plugin) {
1390 if (!$plugin->is_standard()) {
1391 $this->currentplugins[$plugin->component] = $plugin->versiondisk;
1398 * Returns the list of HTTP params to be sent to the updates provider URL
1400 * @return array of (string)param => (string)value
1402 protected function prepare_request_params() {
1403 global $CFG;
1405 $this->load_current_environment();
1406 $this->restore_response();
1408 $params = array();
1409 $params['format'] = 'json';
1411 if (isset($this->recentresponse['ticket'])) {
1412 $params['ticket'] = $this->recentresponse['ticket'];
1415 if (isset($this->currentversion)) {
1416 $params['version'] = $this->currentversion;
1417 } else {
1418 throw new coding_exception('Main Moodle version must be already known here');
1421 if (isset($this->currentbranch)) {
1422 $params['branch'] = $this->currentbranch;
1423 } else {
1424 throw new coding_exception('Moodle release must be already known here');
1427 $plugins = array();
1428 foreach ($this->currentplugins as $plugin => $version) {
1429 $plugins[] = $plugin.'@'.$version;
1431 if (!empty($plugins)) {
1432 $params['plugins'] = implode(',', $plugins);
1435 return $params;
1439 * Returns the list of cURL options to use when fetching available updates data
1441 * @return array of (string)param => (string)value
1443 protected function prepare_request_options() {
1444 global $CFG;
1446 $options = array(
1447 'CURLOPT_SSL_VERIFYHOST' => 2, // this is the default in {@link curl} class but just in case
1448 'CURLOPT_SSL_VERIFYPEER' => true,
1451 return $options;
1455 * Returns the current timestamp
1457 * @return int the timestamp
1459 protected function cron_current_timestamp() {
1460 return time();
1464 * Output cron debugging info
1466 * @see mtrace()
1467 * @param string $msg output message
1468 * @param string $eol end of line
1470 protected function cron_mtrace($msg, $eol = PHP_EOL) {
1471 mtrace($msg, $eol);
1475 * Decide if the autocheck feature is disabled in the server setting
1477 * @return bool true if autocheck enabled, false if disabled
1479 protected function cron_autocheck_enabled() {
1480 global $CFG;
1482 if (empty($CFG->updateautocheck)) {
1483 return false;
1484 } else {
1485 return true;
1490 * Decide if the recently fetched data are still fresh enough
1492 * @param int $now current timestamp
1493 * @return bool true if no need to re-fetch, false otherwise
1495 protected function cron_has_fresh_fetch($now) {
1496 $recent = $this->get_last_timefetched();
1498 if (empty($recent)) {
1499 return false;
1502 if ($now < $recent) {
1503 $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1504 return true;
1507 if ($now - $recent > 24 * HOURSECS) {
1508 return false;
1511 return true;
1515 * Decide if the fetch is outadated or even missing
1517 * @param int $now current timestamp
1518 * @return bool false if no need to re-fetch, true otherwise
1520 protected function cron_has_outdated_fetch($now) {
1521 $recent = $this->get_last_timefetched();
1523 if (empty($recent)) {
1524 return true;
1527 if ($now < $recent) {
1528 $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1529 return false;
1532 if ($now - $recent > 48 * HOURSECS) {
1533 return true;
1536 return false;
1540 * Returns the cron execution offset for this site
1542 * The main {@link self::cron()} is supposed to run every night in some random time
1543 * between 01:00 and 06:00 AM (local time). The exact moment is defined by so called
1544 * execution offset, that is the amount of time after 01:00 AM. The offset value is
1545 * initially generated randomly and then used consistently at the site. This way, the
1546 * regular checks against the download.moodle.org server are spread in time.
1548 * @return int the offset number of seconds from range 1 sec to 5 hours
1550 protected function cron_execution_offset() {
1551 global $CFG;
1553 if (empty($CFG->updatecronoffset)) {
1554 set_config('updatecronoffset', rand(1, 5 * HOURSECS));
1557 return $CFG->updatecronoffset;
1561 * Fetch available updates info and eventually send notification to site admins
1563 protected function cron_execute() {
1565 try {
1566 $this->restore_response();
1567 $previous = $this->recentresponse;
1568 $this->fetch();
1569 $this->restore_response(true);
1570 $current = $this->recentresponse;
1571 $changes = $this->compare_responses($previous, $current);
1572 $notifications = $this->cron_notifications($changes);
1573 $this->cron_notify($notifications);
1574 $this->cron_mtrace('done');
1575 } catch (available_update_checker_exception $e) {
1576 $this->cron_mtrace('FAILED!');
1581 * Given the list of changes in available updates, pick those to send to site admins
1583 * @param array $changes as returned by {@link self::compare_responses()}
1584 * @return array of available_update_info objects to send to site admins
1586 protected function cron_notifications(array $changes) {
1587 global $CFG;
1589 $notifications = array();
1590 $pluginman = plugin_manager::instance();
1591 $plugins = $pluginman->get_plugins(true);
1593 foreach ($changes as $component => $componentchanges) {
1594 if (empty($componentchanges)) {
1595 continue;
1597 $componentupdates = $this->get_update_info($component,
1598 array('minmaturity' => $CFG->updateminmaturity, 'notifybuilds' => $CFG->updatenotifybuilds));
1599 if (empty($componentupdates)) {
1600 continue;
1602 // notify only about those $componentchanges that are present in $componentupdates
1603 // to respect the preferences
1604 foreach ($componentchanges as $componentchange) {
1605 foreach ($componentupdates as $componentupdate) {
1606 if ($componentupdate->version == $componentchange['version']) {
1607 if ($component == 'core') {
1608 // In case of 'core', we already know that the $componentupdate
1609 // is a real update with higher version ({@see self::get_update_info()}).
1610 // We just perform additional check for the release property as there
1611 // can be two Moodle releases having the same version (e.g. 2.4.0 and 2.5dev shortly
1612 // after the release). We can do that because we have the release info
1613 // always available for the core.
1614 if ((string)$componentupdate->release === (string)$componentchange['release']) {
1615 $notifications[] = $componentupdate;
1617 } else {
1618 // Use the plugin_manager to check if the detected $componentchange
1619 // is a real update with higher version. That is, the $componentchange
1620 // is present in the array of {@link available_update_info} objects
1621 // returned by the plugin's available_updates() method.
1622 list($plugintype, $pluginname) = normalize_component($component);
1623 if (!empty($plugins[$plugintype][$pluginname])) {
1624 $availableupdates = $plugins[$plugintype][$pluginname]->available_updates();
1625 if (!empty($availableupdates)) {
1626 foreach ($availableupdates as $availableupdate) {
1627 if ($availableupdate->version == $componentchange['version']) {
1628 $notifications[] = $componentupdate;
1639 return $notifications;
1643 * Sends the given notifications to site admins via messaging API
1645 * @param array $notifications array of available_update_info objects to send
1647 protected function cron_notify(array $notifications) {
1648 global $CFG;
1650 if (empty($notifications)) {
1651 return;
1654 $admins = get_admins();
1656 if (empty($admins)) {
1657 return;
1660 $this->cron_mtrace('sending notifications ... ', '');
1662 $text = get_string('updatenotifications', 'core_admin') . PHP_EOL;
1663 $html = html_writer::tag('h1', get_string('updatenotifications', 'core_admin')) . PHP_EOL;
1665 $coreupdates = array();
1666 $pluginupdates = array();
1668 foreach ($notifications as $notification) {
1669 if ($notification->component == 'core') {
1670 $coreupdates[] = $notification;
1671 } else {
1672 $pluginupdates[] = $notification;
1676 if (!empty($coreupdates)) {
1677 $text .= PHP_EOL . get_string('updateavailable', 'core_admin') . PHP_EOL;
1678 $html .= html_writer::tag('h2', get_string('updateavailable', 'core_admin')) . PHP_EOL;
1679 $html .= html_writer::start_tag('ul') . PHP_EOL;
1680 foreach ($coreupdates as $coreupdate) {
1681 $html .= html_writer::start_tag('li');
1682 if (isset($coreupdate->release)) {
1683 $text .= get_string('updateavailable_release', 'core_admin', $coreupdate->release);
1684 $html .= html_writer::tag('strong', get_string('updateavailable_release', 'core_admin', $coreupdate->release));
1686 if (isset($coreupdate->version)) {
1687 $text .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
1688 $html .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
1690 if (isset($coreupdate->maturity)) {
1691 $text .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
1692 $html .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
1694 $text .= PHP_EOL;
1695 $html .= html_writer::end_tag('li') . PHP_EOL;
1697 $text .= PHP_EOL;
1698 $html .= html_writer::end_tag('ul') . PHP_EOL;
1700 $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/index.php');
1701 $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
1702 $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/index.php', $CFG->wwwroot.'/'.$CFG->admin.'/index.php'));
1703 $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
1706 if (!empty($pluginupdates)) {
1707 $text .= PHP_EOL . get_string('updateavailableforplugin', 'core_admin') . PHP_EOL;
1708 $html .= html_writer::tag('h2', get_string('updateavailableforplugin', 'core_admin')) . PHP_EOL;
1710 $html .= html_writer::start_tag('ul') . PHP_EOL;
1711 foreach ($pluginupdates as $pluginupdate) {
1712 $html .= html_writer::start_tag('li');
1713 $text .= get_string('pluginname', $pluginupdate->component);
1714 $html .= html_writer::tag('strong', get_string('pluginname', $pluginupdate->component));
1716 $text .= ' ('.$pluginupdate->component.')';
1717 $html .= ' ('.$pluginupdate->component.')';
1719 $text .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
1720 $html .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
1722 $text .= PHP_EOL;
1723 $html .= html_writer::end_tag('li') . PHP_EOL;
1725 $text .= PHP_EOL;
1726 $html .= html_writer::end_tag('ul') . PHP_EOL;
1728 $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php');
1729 $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
1730 $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/plugins.php', $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php'));
1731 $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
1734 $a = array('siteurl' => $CFG->wwwroot);
1735 $text .= get_string('updatenotificationfooter', 'core_admin', $a) . PHP_EOL;
1736 $a = array('siteurl' => html_writer::link($CFG->wwwroot, $CFG->wwwroot));
1737 $html .= html_writer::tag('footer', html_writer::tag('p', get_string('updatenotificationfooter', 'core_admin', $a),
1738 array('style' => 'font-size:smaller; color:#333;')));
1740 foreach ($admins as $admin) {
1741 $message = new stdClass();
1742 $message->component = 'moodle';
1743 $message->name = 'availableupdate';
1744 $message->userfrom = get_admin();
1745 $message->userto = $admin;
1746 $message->subject = get_string('updatenotificationsubject', 'core_admin', array('siteurl' => $CFG->wwwroot));
1747 $message->fullmessage = $text;
1748 $message->fullmessageformat = FORMAT_PLAIN;
1749 $message->fullmessagehtml = $html;
1750 $message->smallmessage = get_string('updatenotifications', 'core_admin');
1751 $message->notification = 1;
1752 message_send($message);
1757 * Compare two release labels and decide if they are the same
1759 * @param string $remote release info of the available update
1760 * @param null|string $local release info of the local code, defaults to $release defined in version.php
1761 * @return boolean true if the releases declare the same minor+major version
1763 protected function is_same_release($remote, $local=null) {
1765 if (is_null($local)) {
1766 $this->load_current_environment();
1767 $local = $this->currentrelease;
1770 $pattern = '/^([0-9\.\+]+)([^(]*)/';
1772 preg_match($pattern, $remote, $remotematches);
1773 preg_match($pattern, $local, $localmatches);
1775 $remotematches[1] = str_replace('+', '', $remotematches[1]);
1776 $localmatches[1] = str_replace('+', '', $localmatches[1]);
1778 if ($remotematches[1] === $localmatches[1] and rtrim($remotematches[2]) === rtrim($localmatches[2])) {
1779 return true;
1780 } else {
1781 return false;
1788 * Defines the structure of objects returned by {@link available_update_checker::get_update_info()}
1790 class available_update_info {
1792 /** @var string frankenstyle component name */
1793 public $component;
1794 /** @var int the available version of the component */
1795 public $version;
1796 /** @var string|null optional release name */
1797 public $release = null;
1798 /** @var int|null optional maturity info, eg {@link MATURITY_STABLE} */
1799 public $maturity = null;
1800 /** @var string|null optional URL of a page with more info about the update */
1801 public $url = null;
1802 /** @var string|null optional URL of a ZIP package that can be downloaded and installed */
1803 public $download = null;
1804 /** @var string|null of self::download is set, then this must be the MD5 hash of the ZIP */
1805 public $downloadmd5 = null;
1808 * Creates new instance of the class
1810 * The $info array must provide at least the 'version' value and optionally all other
1811 * values to populate the object's properties.
1813 * @param string $name the frankenstyle component name
1814 * @param array $info associative array with other properties
1816 public function __construct($name, array $info) {
1817 $this->component = $name;
1818 foreach ($info as $k => $v) {
1819 if (property_exists('available_update_info', $k) and $k != 'component') {
1820 $this->$k = $v;
1828 * Implements a communication bridge to the mdeploy.php utility
1830 class available_update_deployer {
1832 const HTTP_PARAM_PREFIX = 'updteautodpldata_'; // Hey, even Google has not heard of such a prefix! So it MUST be safe :-p
1833 const HTTP_PARAM_CHECKER = 'datapackagesize'; // Name of the parameter that holds the number of items in the received data items
1835 /** @var available_update_deployer holds the singleton instance */
1836 protected static $singletoninstance;
1837 /** @var moodle_url URL of a page that includes the deployer UI */
1838 protected $callerurl;
1839 /** @var moodle_url URL to return after the deployment */
1840 protected $returnurl;
1843 * Direct instantiation not allowed, use the factory method {@link self::instance()}
1845 protected function __construct() {
1849 * Sorry, this is singleton
1851 protected function __clone() {
1855 * Factory method for this class
1857 * @return available_update_deployer the singleton instance
1859 public static function instance() {
1860 if (is_null(self::$singletoninstance)) {
1861 self::$singletoninstance = new self();
1863 return self::$singletoninstance;
1867 * Reset caches used by this script
1869 * @param bool $phpunitreset is this called as a part of PHPUnit reset?
1871 public static function reset_caches($phpunitreset = false) {
1872 if ($phpunitreset) {
1873 self::$singletoninstance = null;
1878 * Is automatic deployment enabled?
1880 * @return bool
1882 public function enabled() {
1883 global $CFG;
1885 if (!empty($CFG->disableupdateautodeploy)) {
1886 // The feature is prohibited via config.php
1887 return false;
1890 return get_config('updateautodeploy');
1894 * Sets some base properties of the class to make it usable.
1896 * @param moodle_url $callerurl the base URL of a script that will handle the class'es form data
1897 * @param moodle_url $returnurl the final URL to return to when the deployment is finished
1899 public function initialize(moodle_url $callerurl, moodle_url $returnurl) {
1901 if (!$this->enabled()) {
1902 throw new coding_exception('Unable to initialize the deployer, the feature is not enabled.');
1905 $this->callerurl = $callerurl;
1906 $this->returnurl = $returnurl;
1910 * Has the deployer been initialized?
1912 * Initialized deployer means that the following properties were set:
1913 * callerurl, returnurl
1915 * @return bool
1917 public function initialized() {
1919 if (!$this->enabled()) {
1920 return false;
1923 if (empty($this->callerurl)) {
1924 return false;
1927 if (empty($this->returnurl)) {
1928 return false;
1931 return true;
1935 * Returns a list of reasons why the deployment can not happen
1937 * If the returned array is empty, the deployment seems to be possible. The returned
1938 * structure is an associative array with keys representing individual impediments.
1939 * Possible keys are: missingdownloadurl, missingdownloadmd5, notwritable.
1941 * @param available_update_info $info
1942 * @return array
1944 public function deployment_impediments(available_update_info $info) {
1946 $impediments = array();
1948 if (empty($info->download)) {
1949 $impediments['missingdownloadurl'] = true;
1952 if (empty($info->downloadmd5)) {
1953 $impediments['missingdownloadmd5'] = true;
1956 if (!empty($info->download) and !$this->update_downloadable($info->download)) {
1957 $impediments['notdownloadable'] = true;
1960 if (!$this->component_writable($info->component)) {
1961 $impediments['notwritable'] = true;
1964 return $impediments;
1968 * Check to see if the current version of the plugin seems to be a checkout of an external repository.
1970 * @see plugin_manager::plugin_external_source()
1971 * @param available_update_info $info
1972 * @return false|string
1974 public function plugin_external_source(available_update_info $info) {
1976 $paths = get_plugin_types(true);
1977 list($plugintype, $pluginname) = normalize_component($info->component);
1978 $pluginroot = $paths[$plugintype].'/'.$pluginname;
1980 if (is_dir($pluginroot.'/.git')) {
1981 return 'git';
1984 if (is_dir($pluginroot.'/CVS')) {
1985 return 'cvs';
1988 if (is_dir($pluginroot.'/.svn')) {
1989 return 'svn';
1992 return false;
1996 * Prepares a renderable widget to confirm installation of an available update.
1998 * @param available_update_info $info component version to deploy
1999 * @return renderable
2001 public function make_confirm_widget(available_update_info $info) {
2003 if (!$this->initialized()) {
2004 throw new coding_exception('Illegal method call - deployer not initialized.');
2007 $params = $this->data_to_params(array(
2008 'updateinfo' => (array)$info, // see http://www.php.net/manual/en/language.types.array.php#language.types.array.casting
2011 $widget = new single_button(
2012 new moodle_url($this->callerurl, $params),
2013 get_string('updateavailableinstall', 'core_admin'),
2014 'post'
2017 return $widget;
2021 * Prepares a renderable widget to execute installation of an available update.
2023 * @param available_update_info $info component version to deploy
2024 * @param moodle_url $returnurl URL to return after the installation execution
2025 * @return renderable
2027 public function make_execution_widget(available_update_info $info, moodle_url $returnurl = null) {
2028 global $CFG;
2030 if (!$this->initialized()) {
2031 throw new coding_exception('Illegal method call - deployer not initialized.');
2034 $pluginrootpaths = get_plugin_types(true);
2036 list($plugintype, $pluginname) = normalize_component($info->component);
2038 if (empty($pluginrootpaths[$plugintype])) {
2039 throw new coding_exception('Unknown plugin type root location', $plugintype);
2042 list($passfile, $password) = $this->prepare_authorization();
2044 if (is_null($returnurl)) {
2045 $returnurl = new moodle_url('/admin');
2046 } else {
2047 $returnurl = $returnurl;
2050 $params = array(
2051 'upgrade' => true,
2052 'type' => $plugintype,
2053 'name' => $pluginname,
2054 'typeroot' => $pluginrootpaths[$plugintype],
2055 'package' => $info->download,
2056 'md5' => $info->downloadmd5,
2057 'dataroot' => $CFG->dataroot,
2058 'dirroot' => $CFG->dirroot,
2059 'passfile' => $passfile,
2060 'password' => $password,
2061 'returnurl' => $returnurl->out(false),
2064 if (!empty($CFG->proxyhost)) {
2065 // MDL-36973 - Beware - we should call just !is_proxybypass() here. But currently, our
2066 // cURL wrapper class does not do it. So, to have consistent behaviour, we pass proxy
2067 // setting regardless the $CFG->proxybypass setting. Once the {@link curl} class is
2068 // fixed, the condition should be amended.
2069 if (true or !is_proxybypass($info->download)) {
2070 if (empty($CFG->proxyport)) {
2071 $params['proxy'] = $CFG->proxyhost;
2072 } else {
2073 $params['proxy'] = $CFG->proxyhost.':'.$CFG->proxyport;
2076 if (!empty($CFG->proxyuser) and !empty($CFG->proxypassword)) {
2077 $params['proxyuserpwd'] = $CFG->proxyuser.':'.$CFG->proxypassword;
2080 if (!empty($CFG->proxytype)) {
2081 $params['proxytype'] = $CFG->proxytype;
2086 $widget = new single_button(
2087 new moodle_url('/mdeploy.php', $params),
2088 get_string('updateavailableinstall', 'core_admin'),
2089 'post'
2092 return $widget;
2096 * Returns array of data objects passed to this tool.
2098 * @return array
2100 public function submitted_data() {
2102 $data = $this->params_to_data($_POST);
2104 if (empty($data) or empty($data[self::HTTP_PARAM_CHECKER])) {
2105 return false;
2108 if (!empty($data['updateinfo']) and is_object($data['updateinfo'])) {
2109 $updateinfo = $data['updateinfo'];
2110 if (!empty($updateinfo->component) and !empty($updateinfo->version)) {
2111 $data['updateinfo'] = new available_update_info($updateinfo->component, (array)$updateinfo);
2115 if (!empty($data['callerurl'])) {
2116 $data['callerurl'] = new moodle_url($data['callerurl']);
2119 if (!empty($data['returnurl'])) {
2120 $data['returnurl'] = new moodle_url($data['returnurl']);
2123 return $data;
2127 * Handles magic getters and setters for protected properties.
2129 * @param string $name method name, e.g. set_returnurl()
2130 * @param array $arguments arguments to be passed to the array
2132 public function __call($name, array $arguments = array()) {
2134 if (substr($name, 0, 4) === 'set_') {
2135 $property = substr($name, 4);
2136 if (empty($property)) {
2137 throw new coding_exception('Invalid property name (empty)');
2139 if (empty($arguments)) {
2140 $arguments = array(true); // Default value for flag-like properties.
2142 // Make sure it is a protected property.
2143 $isprotected = false;
2144 $reflection = new ReflectionObject($this);
2145 foreach ($reflection->getProperties(ReflectionProperty::IS_PROTECTED) as $reflectionproperty) {
2146 if ($reflectionproperty->getName() === $property) {
2147 $isprotected = true;
2148 break;
2151 if (!$isprotected) {
2152 throw new coding_exception('Unable to set property - it does not exist or it is not protected');
2154 $value = reset($arguments);
2155 $this->$property = $value;
2156 return;
2159 if (substr($name, 0, 4) === 'get_') {
2160 $property = substr($name, 4);
2161 if (empty($property)) {
2162 throw new coding_exception('Invalid property name (empty)');
2164 if (!empty($arguments)) {
2165 throw new coding_exception('No parameter expected');
2167 // Make sure it is a protected property.
2168 $isprotected = false;
2169 $reflection = new ReflectionObject($this);
2170 foreach ($reflection->getProperties(ReflectionProperty::IS_PROTECTED) as $reflectionproperty) {
2171 if ($reflectionproperty->getName() === $property) {
2172 $isprotected = true;
2173 break;
2176 if (!$isprotected) {
2177 throw new coding_exception('Unable to get property - it does not exist or it is not protected');
2179 return $this->$property;
2184 * Generates a random token and stores it in a file in moodledata directory.
2186 * @return array of the (string)filename and (string)password in this order
2188 public function prepare_authorization() {
2189 global $CFG;
2191 make_upload_directory('mdeploy/auth/');
2193 $attempts = 0;
2194 $success = false;
2196 while (!$success and $attempts < 5) {
2197 $attempts++;
2199 $passfile = $this->generate_passfile();
2200 $password = $this->generate_password();
2201 $now = time();
2203 $filepath = $CFG->dataroot.'/mdeploy/auth/'.$passfile;
2205 if (!file_exists($filepath)) {
2206 $success = file_put_contents($filepath, $password . PHP_EOL . $now . PHP_EOL, LOCK_EX);
2210 if ($success) {
2211 return array($passfile, $password);
2213 } else {
2214 throw new moodle_exception('unable_prepare_authorization', 'core_plugin');
2218 // End of external API
2221 * Prepares an array of HTTP parameters that can be passed to another page.
2223 * @param array|object $data associative array or an object holding the data, data JSON-able
2224 * @return array suitable as a param for moodle_url
2226 protected function data_to_params($data) {
2228 // Append some our own data
2229 if (!empty($this->callerurl)) {
2230 $data['callerurl'] = $this->callerurl->out(false);
2232 if (!empty($this->returnurl)) {
2233 $data['returnurl'] = $this->returnurl->out(false);
2236 // Finally append the count of items in the package.
2237 $data[self::HTTP_PARAM_CHECKER] = count($data);
2239 // Generate params
2240 $params = array();
2241 foreach ($data as $name => $value) {
2242 $transname = self::HTTP_PARAM_PREFIX.$name;
2243 $transvalue = json_encode($value);
2244 $params[$transname] = $transvalue;
2247 return $params;
2251 * Converts HTTP parameters passed to the script into native PHP data
2253 * @param array $params such as $_REQUEST or $_POST
2254 * @return array data passed for this class
2256 protected function params_to_data(array $params) {
2258 if (empty($params)) {
2259 return array();
2262 $data = array();
2263 foreach ($params as $name => $value) {
2264 if (strpos($name, self::HTTP_PARAM_PREFIX) === 0) {
2265 $realname = substr($name, strlen(self::HTTP_PARAM_PREFIX));
2266 $realvalue = json_decode($value);
2267 $data[$realname] = $realvalue;
2271 return $data;
2275 * Returns a random string to be used as a filename of the password storage.
2277 * @return string
2279 protected function generate_passfile() {
2280 return clean_param(uniqid('mdeploy_', true), PARAM_FILE);
2284 * Returns a random string to be used as the authorization token
2286 * @return string
2288 protected function generate_password() {
2289 return complex_random_string();
2293 * Checks if the given component's directory is writable
2295 * For the purpose of the deployment, the web server process has to have
2296 * write access to all files in the component's directory (recursively) and for the
2297 * directory itself.
2299 * @see worker::move_directory_source_precheck()
2300 * @param string $component normalized component name
2301 * @return boolean
2303 protected function component_writable($component) {
2305 list($plugintype, $pluginname) = normalize_component($component);
2307 $directory = get_plugin_directory($plugintype, $pluginname);
2309 if (is_null($directory)) {
2310 throw new coding_exception('Unknown component location', $component);
2313 return $this->directory_writable($directory);
2317 * Checks if the mdeploy.php will be able to fetch the ZIP from the given URL
2319 * This is mainly supposed to check if the transmission over HTTPS would
2320 * work. That is, if the CA certificates are present at the server.
2322 * @param string $downloadurl the URL of the ZIP package to download
2323 * @return bool
2325 protected function update_downloadable($downloadurl) {
2326 global $CFG;
2328 $curloptions = array(
2329 'CURLOPT_SSL_VERIFYHOST' => 2, // this is the default in {@link curl} class but just in case
2330 'CURLOPT_SSL_VERIFYPEER' => true,
2333 $curl = new curl(array('proxy' => true));
2334 $result = $curl->head($downloadurl, $curloptions);
2335 $errno = $curl->get_errno();
2336 if (empty($errno)) {
2337 return true;
2338 } else {
2339 return false;
2344 * Checks if the directory and all its contents (recursively) is writable
2346 * @param string $path full path to a directory
2347 * @return boolean
2349 private function directory_writable($path) {
2351 if (!is_writable($path)) {
2352 return false;
2355 if (is_dir($path)) {
2356 $handle = opendir($path);
2357 } else {
2358 return false;
2361 $result = true;
2363 while ($filename = readdir($handle)) {
2364 $filepath = $path.'/'.$filename;
2366 if ($filename === '.' or $filename === '..') {
2367 continue;
2370 if (is_dir($filepath)) {
2371 $result = $result && $this->directory_writable($filepath);
2373 } else {
2374 $result = $result && is_writable($filepath);
2378 closedir($handle);
2380 return $result;
2386 * Factory class producing required subclasses of {@link plugininfo_base}
2388 class plugininfo_default_factory {
2391 * Makes a new instance of the plugininfo class
2393 * @param string $type the plugin type, eg. 'mod'
2394 * @param string $typerootdir full path to the location of all the plugins of this type
2395 * @param string $name the plugin name, eg. 'workshop'
2396 * @param string $namerootdir full path to the location of the plugin
2397 * @param string $typeclass the name of class that holds the info about the plugin
2398 * @return plugininfo_base the instance of $typeclass
2400 public static function make($type, $typerootdir, $name, $namerootdir, $typeclass) {
2401 $plugin = new $typeclass();
2402 $plugin->type = $type;
2403 $plugin->typerootdir = $typerootdir;
2404 $plugin->name = $name;
2405 $plugin->rootdir = $namerootdir;
2407 $plugin->init_display_name();
2408 $plugin->load_disk_version();
2409 $plugin->load_db_version();
2410 $plugin->load_required_main_version();
2411 $plugin->init_is_standard();
2413 return $plugin;
2419 * Base class providing access to the information about a plugin
2421 * @property-read string component the component name, type_name
2423 abstract class plugininfo_base {
2425 /** @var string the plugintype name, eg. mod, auth or workshopform */
2426 public $type;
2427 /** @var string full path to the location of all the plugins of this type */
2428 public $typerootdir;
2429 /** @var string the plugin name, eg. assignment, ldap */
2430 public $name;
2431 /** @var string the localized plugin name */
2432 public $displayname;
2433 /** @var string the plugin source, one of plugin_manager::PLUGIN_SOURCE_xxx constants */
2434 public $source;
2435 /** @var fullpath to the location of this plugin */
2436 public $rootdir;
2437 /** @var int|string the version of the plugin's source code */
2438 public $versiondisk;
2439 /** @var int|string the version of the installed plugin */
2440 public $versiondb;
2441 /** @var int|float|string required version of Moodle core */
2442 public $versionrequires;
2443 /** @var array other plugins that this one depends on, lazy-loaded by {@link get_other_required_plugins()} */
2444 public $dependencies;
2445 /** @var int number of instances of the plugin - not supported yet */
2446 public $instances;
2447 /** @var int order of the plugin among other plugins of the same type - not supported yet */
2448 public $sortorder;
2449 /** @var array|null array of {@link available_update_info} for this plugin */
2450 public $availableupdates;
2453 * Gathers and returns the information about all plugins of the given type
2455 * @param string $type the name of the plugintype, eg. mod, auth or workshopform
2456 * @param string $typerootdir full path to the location of the plugin dir
2457 * @param string $typeclass the name of the actually called class
2458 * @return array of plugintype classes, indexed by the plugin name
2460 public static function get_plugins($type, $typerootdir, $typeclass) {
2462 // get the information about plugins at the disk
2463 $plugins = get_plugin_list($type);
2464 $ondisk = array();
2465 foreach ($plugins as $pluginname => $pluginrootdir) {
2466 $ondisk[$pluginname] = plugininfo_default_factory::make($type, $typerootdir,
2467 $pluginname, $pluginrootdir, $typeclass);
2469 return $ondisk;
2473 * Sets {@link $displayname} property to a localized name of the plugin
2475 public function init_display_name() {
2476 if (!get_string_manager()->string_exists('pluginname', $this->component)) {
2477 $this->displayname = '[pluginname,' . $this->component . ']';
2478 } else {
2479 $this->displayname = get_string('pluginname', $this->component);
2484 * Magic method getter, redirects to read only values.
2486 * @param string $name
2487 * @return mixed
2489 public function __get($name) {
2490 switch ($name) {
2491 case 'component': return $this->type . '_' . $this->name;
2493 default:
2494 debugging('Invalid plugin property accessed! '.$name);
2495 return null;
2500 * Return the full path name of a file within the plugin.
2502 * No check is made to see if the file exists.
2504 * @param string $relativepath e.g. 'version.php'.
2505 * @return string e.g. $CFG->dirroot . '/mod/quiz/version.php'.
2507 public function full_path($relativepath) {
2508 if (empty($this->rootdir)) {
2509 return '';
2511 return $this->rootdir . '/' . $relativepath;
2515 * Load the data from version.php.
2517 * @param bool $disablecache do not attempt to obtain data from the cache
2518 * @return stdClass the object called $plugin defined in version.php
2520 protected function load_version_php($disablecache=false) {
2522 $cache = cache::make('core', 'plugininfo_base');
2524 $versionsphp = $cache->get('versions_php');
2526 if (!$disablecache and $versionsphp !== false and isset($versionsphp[$this->component])) {
2527 return $versionsphp[$this->component];
2530 $versionfile = $this->full_path('version.php');
2532 $plugin = new stdClass();
2533 if (is_readable($versionfile)) {
2534 include($versionfile);
2536 $versionsphp[$this->component] = $plugin;
2537 $cache->set('versions_php', $versionsphp);
2539 return $plugin;
2543 * Sets {@link $versiondisk} property to a numerical value representing the
2544 * version of the plugin's source code.
2546 * If the value is null after calling this method, either the plugin
2547 * does not use versioning (typically does not have any database
2548 * data) or is missing from disk.
2550 public function load_disk_version() {
2551 $plugin = $this->load_version_php();
2552 if (isset($plugin->version)) {
2553 $this->versiondisk = $plugin->version;
2558 * Sets {@link $versionrequires} property to a numerical value representing
2559 * the version of Moodle core that this plugin requires.
2561 public function load_required_main_version() {
2562 $plugin = $this->load_version_php();
2563 if (isset($plugin->requires)) {
2564 $this->versionrequires = $plugin->requires;
2569 * Initialise {@link $dependencies} to the list of other plugins (in any)
2570 * that this one requires to be installed.
2572 protected function load_other_required_plugins() {
2573 $plugin = $this->load_version_php();
2574 if (!empty($plugin->dependencies)) {
2575 $this->dependencies = $plugin->dependencies;
2576 } else {
2577 $this->dependencies = array(); // By default, no dependencies.
2582 * Get the list of other plugins that this plugin requires to be installed.
2584 * @return array with keys the frankenstyle plugin name, and values either
2585 * a version string (like '2011101700') or the constant ANY_VERSION.
2587 public function get_other_required_plugins() {
2588 if (is_null($this->dependencies)) {
2589 $this->load_other_required_plugins();
2591 return $this->dependencies;
2595 * Is this is a subplugin?
2597 * @return boolean
2599 public function is_subplugin() {
2600 return ($this->get_parent_plugin() !== false);
2604 * If I am a subplugin, return the name of my parent plugin.
2606 * @return string|bool false if not a subplugin, name of the parent otherwise
2608 public function get_parent_plugin() {
2609 return $this->get_plugin_manager()->get_parent_of_subplugin($this->type);
2613 * Sets {@link $versiondb} property to a numerical value representing the
2614 * currently installed version of the plugin.
2616 * If the value is null after calling this method, either the plugin
2617 * does not use versioning (typically does not have any database
2618 * data) or has not been installed yet.
2620 public function load_db_version() {
2621 if ($ver = self::get_version_from_config_plugins($this->component)) {
2622 $this->versiondb = $ver;
2627 * Sets {@link $source} property to one of plugin_manager::PLUGIN_SOURCE_xxx
2628 * constants.
2630 * If the property's value is null after calling this method, then
2631 * the type of the plugin has not been recognized and you should throw
2632 * an exception.
2634 public function init_is_standard() {
2636 $standard = plugin_manager::standard_plugins_list($this->type);
2638 if ($standard !== false) {
2639 $standard = array_flip($standard);
2640 if (isset($standard[$this->name])) {
2641 $this->source = plugin_manager::PLUGIN_SOURCE_STANDARD;
2642 } else if (!is_null($this->versiondb) and is_null($this->versiondisk)
2643 and plugin_manager::is_deleted_standard_plugin($this->type, $this->name)) {
2644 $this->source = plugin_manager::PLUGIN_SOURCE_STANDARD; // to be deleted
2645 } else {
2646 $this->source = plugin_manager::PLUGIN_SOURCE_EXTENSION;
2652 * Returns true if the plugin is shipped with the official distribution
2653 * of the current Moodle version, false otherwise.
2655 * @return bool
2657 public function is_standard() {
2658 return $this->source === plugin_manager::PLUGIN_SOURCE_STANDARD;
2662 * Returns true if the the given Moodle version is enough to run this plugin
2664 * @param string|int|double $moodleversion
2665 * @return bool
2667 public function is_core_dependency_satisfied($moodleversion) {
2669 if (empty($this->versionrequires)) {
2670 return true;
2672 } else {
2673 return (double)$this->versionrequires <= (double)$moodleversion;
2678 * Returns the status of the plugin
2680 * @return string one of plugin_manager::PLUGIN_STATUS_xxx constants
2682 public function get_status() {
2684 if (is_null($this->versiondb) and is_null($this->versiondisk)) {
2685 return plugin_manager::PLUGIN_STATUS_NODB;
2687 } else if (is_null($this->versiondb) and !is_null($this->versiondisk)) {
2688 return plugin_manager::PLUGIN_STATUS_NEW;
2690 } else if (!is_null($this->versiondb) and is_null($this->versiondisk)) {
2691 if (plugin_manager::is_deleted_standard_plugin($this->type, $this->name)) {
2692 return plugin_manager::PLUGIN_STATUS_DELETE;
2693 } else {
2694 return plugin_manager::PLUGIN_STATUS_MISSING;
2697 } else if ((string)$this->versiondb === (string)$this->versiondisk) {
2698 return plugin_manager::PLUGIN_STATUS_UPTODATE;
2700 } else if ($this->versiondb < $this->versiondisk) {
2701 return plugin_manager::PLUGIN_STATUS_UPGRADE;
2703 } else if ($this->versiondb > $this->versiondisk) {
2704 return plugin_manager::PLUGIN_STATUS_DOWNGRADE;
2706 } else {
2707 // $version = pi(); and similar funny jokes - hopefully Donald E. Knuth will never contribute to Moodle ;-)
2708 throw new coding_exception('Unable to determine plugin state, check the plugin versions');
2713 * Returns the information about plugin availability
2715 * True means that the plugin is enabled. False means that the plugin is
2716 * disabled. Null means that the information is not available, or the
2717 * plugin does not support configurable availability or the availability
2718 * can not be changed.
2720 * @return null|bool
2722 public function is_enabled() {
2723 return null;
2727 * Populates the property {@link $availableupdates} with the information provided by
2728 * available update checker
2730 * @param available_update_checker $provider the class providing the available update info
2732 public function check_available_updates(available_update_checker $provider) {
2733 global $CFG;
2735 if (isset($CFG->updateminmaturity)) {
2736 $minmaturity = $CFG->updateminmaturity;
2737 } else {
2738 // this can happen during the very first upgrade to 2.3
2739 $minmaturity = MATURITY_STABLE;
2742 $this->availableupdates = $provider->get_update_info($this->component,
2743 array('minmaturity' => $minmaturity));
2747 * If there are updates for this plugin available, returns them.
2749 * Returns array of {@link available_update_info} objects, if some update
2750 * is available. Returns null if there is no update available or if the update
2751 * availability is unknown.
2753 * @return array|null
2755 public function available_updates() {
2757 if (empty($this->availableupdates) or !is_array($this->availableupdates)) {
2758 return null;
2761 $updates = array();
2763 foreach ($this->availableupdates as $availableupdate) {
2764 if ($availableupdate->version > $this->versiondisk) {
2765 $updates[] = $availableupdate;
2769 if (empty($updates)) {
2770 return null;
2773 return $updates;
2777 * Returns the node name used in admin settings menu for this plugin settings (if applicable)
2779 * @return null|string node name or null if plugin does not create settings node (default)
2781 public function get_settings_section_name() {
2782 return null;
2786 * Returns the URL of the plugin settings screen
2788 * Null value means that the plugin either does not have the settings screen
2789 * or its location is not available via this library.
2791 * @return null|moodle_url
2793 public function get_settings_url() {
2794 $section = $this->get_settings_section_name();
2795 if ($section === null) {
2796 return null;
2798 $settings = admin_get_root()->locate($section);
2799 if ($settings && $settings instanceof admin_settingpage) {
2800 return new moodle_url('/admin/settings.php', array('section' => $section));
2801 } else if ($settings && $settings instanceof admin_externalpage) {
2802 return new moodle_url($settings->url);
2803 } else {
2804 return null;
2809 * Loads plugin settings to the settings tree
2811 * This function usually includes settings.php file in plugins folder.
2812 * Alternatively it can create a link to some settings page (instance of admin_externalpage)
2814 * @param part_of_admin_tree $adminroot
2815 * @param string $parentnodename
2816 * @param bool $hassiteconfig whether the current user has moodle/site:config capability
2818 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
2822 * Should there be a way to uninstall the plugin via the administration UI
2824 * By default, uninstallation is allowed for all non-standard add-ons. Subclasses
2825 * may want to override this to allow uninstallation of all plugins (simply by
2826 * returning true unconditionally). Subplugins follow their parent plugin's
2827 * decision by default.
2829 * Note that even if true is returned, the core may still prohibit the uninstallation,
2830 * e.g. in case there are other plugins that depend on this one.
2832 * @return boolean
2834 public function is_uninstall_allowed() {
2836 if ($this->is_subplugin()) {
2837 return $this->get_plugin_manager()->get_plugin_info($this->get_parent_plugin())->is_uninstall_allowed();
2840 if ($this->is_standard()) {
2841 return false;
2844 return true;
2848 * Optional extra warning before uninstallation, for example number of uses in courses.
2850 * @return string
2852 public function get_uninstall_extra_warning() {
2853 return '';
2857 * Returns the URL of the screen where this plugin can be uninstalled
2859 * Visiting that URL must be safe, that is a manual confirmation is needed
2860 * for actual uninstallation of the plugin. By default, URL to a common
2861 * uninstalling tool is returned.
2863 * @return moodle_url
2865 public function get_uninstall_url() {
2866 return $this->get_default_uninstall_url();
2870 * Returns relative directory of the plugin with heading '/'
2872 * @return string
2874 public function get_dir() {
2875 global $CFG;
2877 return substr($this->rootdir, strlen($CFG->dirroot));
2881 * Hook method to implement certain steps when uninstalling the plugin.
2883 * This hook is called by {@link plugin_manager::uninstall_plugin()} so
2884 * it is basically usable only for those plugin types that use the default
2885 * uninstall tool provided by {@link self::get_default_uninstall_url()}.
2887 * @param progress_trace $progress traces the process
2888 * @return bool true on success, false on failure
2890 public function uninstall(progress_trace $progress) {
2891 return true;
2895 * Returns URL to a script that handles common plugin uninstall procedure.
2897 * This URL is suitable for plugins that do not have their own UI
2898 * for uninstalling.
2900 * @return moodle_url
2902 protected final function get_default_uninstall_url() {
2903 return new moodle_url('/admin/plugins.php', array(
2904 'sesskey' => sesskey(),
2905 'uninstall' => $this->component,
2906 'confirm' => 0,
2911 * Provides access to plugin versions from the {config_plugins} table
2913 * @param string $plugin plugin name
2914 * @param bool $disablecache do not attempt to obtain data from the cache
2915 * @return int|bool the stored value or false if not found
2917 protected function get_version_from_config_plugins($plugin, $disablecache=false) {
2918 global $DB;
2920 $cache = cache::make('core', 'plugininfo_base');
2922 $pluginversions = $cache->get('versions_db');
2924 if ($pluginversions === false or $disablecache) {
2925 try {
2926 $pluginversions = $DB->get_records_menu('config_plugins', array('name' => 'version'), 'plugin', 'plugin,value');
2927 } catch (dml_exception $e) {
2928 // before install
2929 $pluginversions = array();
2931 $cache->set('versions_db', $pluginversions);
2934 if (isset($pluginversions[$plugin])) {
2935 return $pluginversions[$plugin];
2936 } else {
2937 return false;
2942 * Provides access to the plugin_manager singleton.
2944 * @return plugin_manmager
2946 protected function get_plugin_manager() {
2947 return plugin_manager::instance();
2953 * General class for all plugin types that do not have their own class
2955 class plugininfo_general extends plugininfo_base {
2960 * Class for page side blocks
2962 class plugininfo_block extends plugininfo_base {
2964 public static function get_plugins($type, $typerootdir, $typeclass) {
2966 // get the information about blocks at the disk
2967 $blocks = parent::get_plugins($type, $typerootdir, $typeclass);
2969 // add blocks missing from disk
2970 $blocksinfo = self::get_blocks_info();
2971 foreach ($blocksinfo as $blockname => $blockinfo) {
2972 if (isset($blocks[$blockname])) {
2973 continue;
2975 $plugin = new $typeclass();
2976 $plugin->type = $type;
2977 $plugin->typerootdir = $typerootdir;
2978 $plugin->name = $blockname;
2979 $plugin->rootdir = null;
2980 $plugin->displayname = $blockname;
2981 $plugin->versiondb = $blockinfo->version;
2982 $plugin->init_is_standard();
2984 $blocks[$blockname] = $plugin;
2987 return $blocks;
2991 * Magic method getter, redirects to read only values.
2993 * For block plugins pretends the object has 'visible' property for compatibility
2994 * with plugins developed for Moodle version below 2.4
2996 * @param string $name
2997 * @return mixed
2999 public function __get($name) {
3000 if ($name === 'visible') {
3001 debugging('This is now an instance of plugininfo_block, please use $block->is_enabled() instead of $block->visible', DEBUG_DEVELOPER);
3002 return ($this->is_enabled() !== false);
3004 return parent::__get($name);
3007 public function init_display_name() {
3009 if (get_string_manager()->string_exists('pluginname', 'block_' . $this->name)) {
3010 $this->displayname = get_string('pluginname', 'block_' . $this->name);
3012 } else if (($block = block_instance($this->name)) !== false) {
3013 $this->displayname = $block->get_title();
3015 } else {
3016 parent::init_display_name();
3020 public function load_db_version() {
3021 global $DB;
3023 $blocksinfo = self::get_blocks_info();
3024 if (isset($blocksinfo[$this->name]->version)) {
3025 $this->versiondb = $blocksinfo[$this->name]->version;
3029 public function is_enabled() {
3031 $blocksinfo = self::get_blocks_info();
3032 if (isset($blocksinfo[$this->name]->visible)) {
3033 if ($blocksinfo[$this->name]->visible) {
3034 return true;
3035 } else {
3036 return false;
3038 } else {
3039 return parent::is_enabled();
3043 public function get_settings_section_name() {
3044 return 'blocksetting' . $this->name;
3047 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3048 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3049 $ADMIN = $adminroot; // may be used in settings.php
3050 $block = $this; // also can be used inside settings.php
3051 $section = $this->get_settings_section_name();
3053 if (!$hassiteconfig || (($blockinstance = block_instance($this->name)) === false)) {
3054 return;
3057 $settings = null;
3058 if ($blockinstance->has_config()) {
3059 if (file_exists($this->full_path('settings.php'))) {
3060 $settings = new admin_settingpage($section, $this->displayname,
3061 'moodle/site:config', $this->is_enabled() === false);
3062 include($this->full_path('settings.php')); // this may also set $settings to null
3063 } else {
3064 $blocksinfo = self::get_blocks_info();
3065 $settingsurl = new moodle_url('/admin/block.php', array('block' => $blocksinfo[$this->name]->id));
3066 $settings = new admin_externalpage($section, $this->displayname,
3067 $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
3070 if ($settings) {
3071 $ADMIN->add($parentnodename, $settings);
3075 public function is_uninstall_allowed() {
3076 return true;
3080 * Warnign with number of block instances.
3082 * @return string
3084 public function get_uninstall_extra_warning() {
3085 global $DB;
3087 if (!$count = $DB->count_records('block_instances', array('blockname'=>$this->name))) {
3088 return '';
3091 return '<p>'.get_string('uninstallextraconfirmblock', 'core_plugin', array('instances'=>$count)).'</p>';
3095 * Provides access to the records in {block} table
3097 * @param bool $disablecache do not attempt to obtain data from the cache
3098 * @return array array of stdClasses
3100 protected static function get_blocks_info($disablecache=false) {
3101 global $DB;
3103 $cache = cache::make('core', 'plugininfo_block');
3105 $blocktypes = $cache->get('blocktypes');
3107 if ($blocktypes === false or $disablecache) {
3108 try {
3109 $blocktypes = $DB->get_records('block', null, 'name', 'name,id,version,visible');
3110 } catch (dml_exception $e) {
3111 // before install
3112 $blocktypes = array();
3114 $cache->set('blocktypes', $blocktypes);
3117 return $blocktypes;
3123 * Class for text filters
3125 class plugininfo_filter extends plugininfo_base {
3127 public static function get_plugins($type, $typerootdir, $typeclass) {
3128 global $CFG, $DB;
3130 $filters = array();
3132 // get the list of filters in /filter location
3133 $installed = filter_get_all_installed();
3135 foreach ($installed as $name => $displayname) {
3136 $plugin = new $typeclass();
3137 $plugin->type = $type;
3138 $plugin->typerootdir = $typerootdir;
3139 $plugin->name = $name;
3140 $plugin->rootdir = "$CFG->dirroot/filter/$name";
3141 $plugin->displayname = $displayname;
3143 $plugin->load_disk_version();
3144 $plugin->load_db_version();
3145 $plugin->load_required_main_version();
3146 $plugin->init_is_standard();
3148 $filters[$plugin->name] = $plugin;
3151 // Do not mess with filter registration here!
3153 $globalstates = self::get_global_states();
3155 // make sure that all registered filters are installed, just in case
3156 foreach ($globalstates as $name => $info) {
3157 if (!isset($filters[$name])) {
3158 // oops, there is a record in filter_active but the filter is not installed
3159 $plugin = new $typeclass();
3160 $plugin->type = $type;
3161 $plugin->typerootdir = $typerootdir;
3162 $plugin->name = $name;
3163 $plugin->rootdir = "$CFG->dirroot/filter/$name";
3164 $plugin->displayname = $name;
3166 $plugin->load_db_version();
3168 if (is_null($plugin->versiondb)) {
3169 // this is a hack to stimulate 'Missing from disk' error
3170 // because $plugin->versiondisk will be null !== false
3171 $plugin->versiondb = false;
3174 $filters[$plugin->name] = $plugin;
3178 return $filters;
3181 public function init_display_name() {
3182 // do nothing, the name is set in self::get_plugins()
3185 public function is_enabled() {
3187 $globalstates = self::get_global_states();
3189 foreach ($globalstates as $name => $info) {
3190 if ($name === $this->name) {
3191 if ($info->active == TEXTFILTER_DISABLED) {
3192 return false;
3193 } else {
3194 // it may be 'On' or 'Off, but available'
3195 return null;
3200 return null;
3203 public function get_settings_section_name() {
3204 return 'filtersetting' . $this->name;
3207 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3208 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3209 $ADMIN = $adminroot; // may be used in settings.php
3210 $filter = $this; // also can be used inside settings.php
3212 $settings = null;
3213 if ($hassiteconfig && file_exists($this->full_path('filtersettings.php'))) {
3214 $section = $this->get_settings_section_name();
3215 $settings = new admin_settingpage($section, $this->displayname,
3216 'moodle/site:config', $this->is_enabled() === false);
3217 include($this->full_path('filtersettings.php')); // this may also set $settings to null
3219 if ($settings) {
3220 $ADMIN->add($parentnodename, $settings);
3224 public function is_uninstall_allowed() {
3225 return true;
3228 public function get_uninstall_url() {
3229 return new moodle_url('/admin/filters.php', array('sesskey' => sesskey(), 'filterpath' => $this->name, 'action' => 'delete'));
3233 * Provides access to the results of {@link filter_get_global_states()}
3234 * but indexed by the normalized filter name
3236 * The legacy filter name is available as ->legacyname property.
3238 * @param bool $disablecache do not attempt to obtain data from the cache
3239 * @return array
3241 protected static function get_global_states($disablecache=false) {
3242 global $DB;
3244 $cache = cache::make('core', 'plugininfo_filter');
3246 $globalstates = $cache->get('globalstates');
3248 if ($globalstates === false or $disablecache) {
3250 if (!$DB->get_manager()->table_exists('filter_active')) {
3251 // Not installed yet.
3252 $cache->set('globalstates', array());
3253 return array();
3256 $globalstates = array();
3258 foreach (filter_get_global_states() as $name => $info) {
3259 if (strpos($name, '/') !== false) {
3260 // Skip existing before upgrade to new names.
3261 continue;
3264 $filterinfo = new stdClass();
3265 $filterinfo->active = $info->active;
3266 $filterinfo->sortorder = $info->sortorder;
3267 $globalstates[$name] = $filterinfo;
3270 $cache->set('globalstates', $globalstates);
3273 return $globalstates;
3279 * Class for activity modules
3281 class plugininfo_mod extends plugininfo_base {
3283 public static function get_plugins($type, $typerootdir, $typeclass) {
3285 // get the information about plugins at the disk
3286 $modules = parent::get_plugins($type, $typerootdir, $typeclass);
3288 // add modules missing from disk
3289 $modulesinfo = self::get_modules_info();
3290 foreach ($modulesinfo as $modulename => $moduleinfo) {
3291 if (isset($modules[$modulename])) {
3292 continue;
3294 $plugin = new $typeclass();
3295 $plugin->type = $type;
3296 $plugin->typerootdir = $typerootdir;
3297 $plugin->name = $modulename;
3298 $plugin->rootdir = null;
3299 $plugin->displayname = $modulename;
3300 $plugin->versiondb = $moduleinfo->version;
3301 $plugin->init_is_standard();
3303 $modules[$modulename] = $plugin;
3306 return $modules;
3310 * Magic method getter, redirects to read only values.
3312 * For module plugins we pretend the object has 'visible' property for compatibility
3313 * with plugins developed for Moodle version below 2.4
3315 * @param string $name
3316 * @return mixed
3318 public function __get($name) {
3319 if ($name === 'visible') {
3320 debugging('This is now an instance of plugininfo_mod, please use $module->is_enabled() instead of $module->visible', DEBUG_DEVELOPER);
3321 return ($this->is_enabled() !== false);
3323 return parent::__get($name);
3326 public function init_display_name() {
3327 if (get_string_manager()->string_exists('pluginname', $this->component)) {
3328 $this->displayname = get_string('pluginname', $this->component);
3329 } else {
3330 $this->displayname = get_string('modulename', $this->component);
3335 * Load the data from version.php.
3337 * @param bool $disablecache do not attempt to obtain data from the cache
3338 * @return object the data object defined in version.php.
3340 protected function load_version_php($disablecache=false) {
3342 $cache = cache::make('core', 'plugininfo_base');
3344 $versionsphp = $cache->get('versions_php');
3346 if (!$disablecache and $versionsphp !== false and isset($versionsphp[$this->component])) {
3347 return $versionsphp[$this->component];
3350 $versionfile = $this->full_path('version.php');
3352 $module = new stdClass();
3353 $plugin = new stdClass();
3354 if (is_readable($versionfile)) {
3355 include($versionfile);
3357 if (!isset($module->version) and isset($plugin->version)) {
3358 $module = $plugin;
3360 $versionsphp[$this->component] = $module;
3361 $cache->set('versions_php', $versionsphp);
3363 return $module;
3366 public function load_db_version() {
3367 global $DB;
3369 $modulesinfo = self::get_modules_info();
3370 if (isset($modulesinfo[$this->name]->version)) {
3371 $this->versiondb = $modulesinfo[$this->name]->version;
3375 public function is_enabled() {
3377 $modulesinfo = self::get_modules_info();
3378 if (isset($modulesinfo[$this->name]->visible)) {
3379 if ($modulesinfo[$this->name]->visible) {
3380 return true;
3381 } else {
3382 return false;
3384 } else {
3385 return parent::is_enabled();
3389 public function get_settings_section_name() {
3390 return 'modsetting' . $this->name;
3393 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3394 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3395 $ADMIN = $adminroot; // may be used in settings.php
3396 $module = $this; // also can be used inside settings.php
3397 $section = $this->get_settings_section_name();
3399 $modulesinfo = self::get_modules_info();
3400 $settings = null;
3401 if ($hassiteconfig && isset($modulesinfo[$this->name]) && file_exists($this->full_path('settings.php'))) {
3402 $settings = new admin_settingpage($section, $this->displayname,
3403 'moodle/site:config', $this->is_enabled() === false);
3404 include($this->full_path('settings.php')); // this may also set $settings to null
3406 if ($settings) {
3407 $ADMIN->add($parentnodename, $settings);
3412 * Allow all activity modules but Forum to be uninstalled.
3414 * This exception for the Forum has been hard-coded in Moodle since ages,
3415 * we may want to re-think it one day.
3417 public function is_uninstall_allowed() {
3418 if ($this->name === 'forum') {
3419 return false;
3420 } else {
3421 return true;
3426 * Return warning with number of activities and number of affected courses.
3428 * @return string
3430 public function get_uninstall_extra_warning() {
3431 global $DB;
3433 if (!$module = $DB->get_record('modules', array('name'=>$this->name))) {
3434 return '';
3437 if (!$count = $DB->count_records('course_modules', array('module'=>$module->id))) {
3438 return '';
3441 $sql = "SELECT COUNT('x')
3442 FROM (
3443 SELECT course
3444 FROM {course_modules}
3445 WHERE module = :mid
3446 GROUP BY course
3447 ) c";
3448 $courses = $DB->count_records_sql($sql, array('mid'=>$module->id));
3450 return '<p>'.get_string('uninstallextraconfirmmod', 'core_plugin', array('instances'=>$count, 'courses'=>$courses)).'</p>';
3454 * Provides access to the records in {modules} table
3456 * @param bool $disablecache do not attempt to obtain data from the cache
3457 * @return array array of stdClasses
3459 protected static function get_modules_info($disablecache=false) {
3460 global $DB;
3462 $cache = cache::make('core', 'plugininfo_mod');
3464 $modulesinfo = $cache->get('modulesinfo');
3466 if ($modulesinfo === false or $disablecache) {
3467 try {
3468 $modulesinfo = $DB->get_records('modules', null, 'name', 'name,id,version,visible');
3469 } catch (dml_exception $e) {
3470 // before install
3471 $modulesinfo = array();
3473 $cache->set('modulesinfo', $modulesinfo);
3476 return $modulesinfo;
3482 * Class for question behaviours.
3484 class plugininfo_qbehaviour extends plugininfo_base {
3486 public function is_uninstall_allowed() {
3487 return true;
3490 public function get_uninstall_url() {
3491 return new moodle_url('/admin/qbehaviours.php',
3492 array('delete' => $this->name, 'sesskey' => sesskey()));
3498 * Class for question types
3500 class plugininfo_qtype extends plugininfo_base {
3502 public function is_uninstall_allowed() {
3503 return true;
3506 public function get_uninstall_url() {
3507 return new moodle_url('/admin/qtypes.php',
3508 array('delete' => $this->name, 'sesskey' => sesskey()));
3511 public function get_settings_section_name() {
3512 return 'qtypesetting' . $this->name;
3515 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3516 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3517 $ADMIN = $adminroot; // may be used in settings.php
3518 $qtype = $this; // also can be used inside settings.php
3519 $section = $this->get_settings_section_name();
3521 $settings = null;
3522 $systemcontext = context_system::instance();
3523 if (($hassiteconfig || has_capability('moodle/question:config', $systemcontext)) &&
3524 file_exists($this->full_path('settings.php'))) {
3525 $settings = new admin_settingpage($section, $this->displayname,
3526 'moodle/question:config', $this->is_enabled() === false);
3527 include($this->full_path('settings.php')); // this may also set $settings to null
3529 if ($settings) {
3530 $ADMIN->add($parentnodename, $settings);
3537 * Class for authentication plugins
3539 class plugininfo_auth extends plugininfo_base {
3541 public function is_enabled() {
3542 global $CFG;
3544 if (in_array($this->name, array('nologin', 'manual'))) {
3545 // these two are always enabled and can't be disabled
3546 return null;
3549 $enabled = array_flip(explode(',', $CFG->auth));
3551 return isset($enabled[$this->name]);
3554 public function get_settings_section_name() {
3555 return 'authsetting' . $this->name;
3558 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3559 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3560 $ADMIN = $adminroot; // may be used in settings.php
3561 $auth = $this; // also to be used inside settings.php
3562 $section = $this->get_settings_section_name();
3564 $settings = null;
3565 if ($hassiteconfig) {
3566 if (file_exists($this->full_path('settings.php'))) {
3567 // TODO: finish implementation of common settings - locking, etc.
3568 $settings = new admin_settingpage($section, $this->displayname,
3569 'moodle/site:config', $this->is_enabled() === false);
3570 include($this->full_path('settings.php')); // this may also set $settings to null
3571 } else {
3572 $settingsurl = new moodle_url('/admin/auth_config.php', array('auth' => $this->name));
3573 $settings = new admin_externalpage($section, $this->displayname,
3574 $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
3577 if ($settings) {
3578 $ADMIN->add($parentnodename, $settings);
3585 * Class for enrolment plugins
3587 class plugininfo_enrol extends plugininfo_base {
3589 public function is_enabled() {
3590 global $CFG;
3592 // We do not actually need whole enrolment classes here so we do not call
3593 // {@link enrol_get_plugins()}. Note that this may produce slightly different
3594 // results, for example if the enrolment plugin does not contain lib.php
3595 // but it is listed in $CFG->enrol_plugins_enabled
3597 $enabled = array_flip(explode(',', $CFG->enrol_plugins_enabled));
3599 return isset($enabled[$this->name]);
3602 public function get_settings_section_name() {
3603 if (file_exists($this->full_path('settings.php'))) {
3604 return 'enrolsettings' . $this->name;
3605 } else {
3606 return null;
3610 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3611 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3613 if (!$hassiteconfig or !file_exists($this->full_path('settings.php'))) {
3614 return;
3616 $section = $this->get_settings_section_name();
3618 $ADMIN = $adminroot; // may be used in settings.php
3619 $enrol = $this; // also can be used inside settings.php
3620 $settings = new admin_settingpage($section, $this->displayname,
3621 'moodle/site:config', $this->is_enabled() === false);
3623 include($this->full_path('settings.php')); // This may also set $settings to null!
3625 if ($settings) {
3626 $ADMIN->add($parentnodename, $settings);
3630 public function is_uninstall_allowed() {
3631 if ($this->name === 'manual') {
3632 return false;
3634 return true;
3638 * Return warning with number of activities and number of affected courses.
3640 * @return string
3642 public function get_uninstall_extra_warning() {
3643 global $DB, $OUTPUT;
3645 $sql = "SELECT COUNT('x')
3646 FROM {user_enrolments} ue
3647 JOIN {enrol} e ON e.id = ue.enrolid
3648 WHERE e.enrol = :plugin";
3649 $count = $DB->count_records_sql($sql, array('plugin'=>$this->name));
3651 if (!$count) {
3652 return '';
3655 $migrateurl = new moodle_url('/admin/enrol.php', array('action'=>'migrate', 'enrol'=>$this->name, 'sesskey'=>sesskey()));
3656 $migrate = new single_button($migrateurl, get_string('migratetomanual', 'core_enrol'));
3657 $button = $OUTPUT->render($migrate);
3659 $result = '<p>'.get_string('uninstallextraconfirmenrol', 'core_plugin', array('enrolments'=>$count)).'</p>';
3660 $result .= $button;
3662 return $result;
3668 * Class for messaging processors
3670 class plugininfo_message extends plugininfo_base {
3672 public function get_settings_section_name() {
3673 return 'messagesetting' . $this->name;
3676 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3677 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3678 $ADMIN = $adminroot; // may be used in settings.php
3679 if (!$hassiteconfig) {
3680 return;
3682 $section = $this->get_settings_section_name();
3684 $settings = null;
3685 $processors = get_message_processors();
3686 if (isset($processors[$this->name])) {
3687 $processor = $processors[$this->name];
3688 if ($processor->available && $processor->hassettings) {
3689 $settings = new admin_settingpage($section, $this->displayname,
3690 'moodle/site:config', $this->is_enabled() === false);
3691 include($this->full_path('settings.php')); // this may also set $settings to null
3694 if ($settings) {
3695 $ADMIN->add($parentnodename, $settings);
3700 * @see plugintype_interface::is_enabled()
3702 public function is_enabled() {
3703 $processors = get_message_processors();
3704 if (isset($processors[$this->name])) {
3705 return $processors[$this->name]->configured && $processors[$this->name]->enabled;
3706 } else {
3707 return parent::is_enabled();
3711 public function is_uninstall_allowed() {
3712 $processors = get_message_processors();
3713 if (isset($processors[$this->name])) {
3714 return true;
3715 } else {
3716 return false;
3721 * @see plugintype_interface::get_uninstall_url()
3723 public function get_uninstall_url() {
3724 $processors = get_message_processors();
3725 return new moodle_url('/admin/message.php', array('uninstall' => $processors[$this->name]->id, 'sesskey' => sesskey()));
3731 * Class for repositories
3733 class plugininfo_repository extends plugininfo_base {
3735 public function is_enabled() {
3737 $enabled = self::get_enabled_repositories();
3739 return isset($enabled[$this->name]);
3742 public function get_settings_section_name() {
3743 return 'repositorysettings'.$this->name;
3746 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3747 if ($hassiteconfig && $this->is_enabled()) {
3748 // completely no access to repository setting when it is not enabled
3749 $sectionname = $this->get_settings_section_name();
3750 $settingsurl = new moodle_url('/admin/repository.php',
3751 array('sesskey' => sesskey(), 'action' => 'edit', 'repos' => $this->name));
3752 $settings = new admin_externalpage($sectionname, $this->displayname,
3753 $settingsurl, 'moodle/site:config', false);
3754 $adminroot->add($parentnodename, $settings);
3759 * Provides access to the records in {repository} table
3761 * @param bool $disablecache do not attempt to obtain data from the cache
3762 * @return array array of stdClasses
3764 protected static function get_enabled_repositories($disablecache=false) {
3765 global $DB;
3767 $cache = cache::make('core', 'plugininfo_repository');
3769 $enabled = $cache->get('enabled');
3771 if ($enabled === false or $disablecache) {
3772 $enabled = $DB->get_records('repository', null, 'type', 'type,visible,sortorder');
3773 $cache->set('enabled', $enabled);
3776 return $enabled;
3782 * Class for portfolios
3784 class plugininfo_portfolio extends plugininfo_base {
3786 public function is_enabled() {
3788 $enabled = self::get_enabled_portfolios();
3790 return isset($enabled[$this->name]);
3794 * Returns list of enabled portfolio plugins
3796 * Portfolio plugin is enabled if there is at least one record in the {portfolio_instance}
3797 * table for it.
3799 * @param bool $disablecache do not attempt to obtain data from the cache
3800 * @return array array of stdClasses with properties plugin and visible indexed by plugin
3802 protected static function get_enabled_portfolios($disablecache=false) {
3803 global $DB;
3805 $cache = cache::make('core', 'plugininfo_portfolio');
3807 $enabled = $cache->get('enabled');
3809 if ($enabled === false or $disablecache) {
3810 $enabled = array();
3811 $instances = $DB->get_recordset('portfolio_instance', null, '', 'plugin,visible');
3812 foreach ($instances as $instance) {
3813 if (isset($enabled[$instance->plugin])) {
3814 if ($instance->visible) {
3815 $enabled[$instance->plugin]->visible = $instance->visible;
3817 } else {
3818 $enabled[$instance->plugin] = $instance;
3821 $instances->close();
3822 $cache->set('enabled', $enabled);
3825 return $enabled;
3831 * Class for themes
3833 class plugininfo_theme extends plugininfo_base {
3835 public function is_enabled() {
3836 global $CFG;
3838 if ((!empty($CFG->theme) and $CFG->theme === $this->name) or
3839 (!empty($CFG->themelegacy) and $CFG->themelegacy === $this->name)) {
3840 return true;
3841 } else {
3842 return parent::is_enabled();
3849 * Class representing an MNet service
3851 class plugininfo_mnetservice extends plugininfo_base {
3853 public function is_enabled() {
3854 global $CFG;
3856 if (empty($CFG->mnet_dispatcher_mode) || $CFG->mnet_dispatcher_mode !== 'strict') {
3857 return false;
3858 } else {
3859 return parent::is_enabled();
3866 * Class for admin tool plugins
3868 class plugininfo_tool extends plugininfo_base {
3870 public function is_uninstall_allowed() {
3871 return true;
3877 * Class for admin tool plugins
3879 class plugininfo_report extends plugininfo_base {
3881 public function is_uninstall_allowed() {
3882 return true;
3888 * Class for local plugins
3890 class plugininfo_local extends plugininfo_base {
3892 public function is_uninstall_allowed() {
3893 return true;
3898 * Class for HTML editors
3900 class plugininfo_editor extends plugininfo_base {
3902 public function get_settings_section_name() {
3903 return 'editorsettings' . $this->name;
3906 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3907 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3908 $ADMIN = $adminroot; // may be used in settings.php
3909 $editor = $this; // also can be used inside settings.php
3910 $section = $this->get_settings_section_name();
3912 $settings = null;
3913 if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3914 $settings = new admin_settingpage($section, $this->displayname,
3915 'moodle/site:config', $this->is_enabled() === false);
3916 include($this->full_path('settings.php')); // this may also set $settings to null
3918 if ($settings) {
3919 $ADMIN->add($parentnodename, $settings);
3924 * Basic textarea editor can not be uninstalled.
3926 public function is_uninstall_allowed() {
3927 if ($this->name === 'textarea') {
3928 return false;
3929 } else {
3930 return true;
3935 * Returns the information about plugin availability
3937 * True means that the plugin is enabled. False means that the plugin is
3938 * disabled. Null means that the information is not available, or the
3939 * plugin does not support configurable availability or the availability
3940 * can not be changed.
3942 * @return null|bool
3944 public function is_enabled() {
3945 global $CFG;
3946 if (empty($CFG->texteditors)) {
3947 $CFG->texteditors = 'tinymce,textarea';
3949 if (in_array($this->name, explode(',', $CFG->texteditors))) {
3950 return true;
3952 return false;
3957 * Class for plagiarism plugins
3959 class plugininfo_plagiarism extends plugininfo_base {
3961 public function get_settings_section_name() {
3962 return 'plagiarism'. $this->name;
3965 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3966 // plagiarism plugin just redirect to settings.php in the plugins directory
3967 if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3968 $section = $this->get_settings_section_name();
3969 $settingsurl = new moodle_url($this->get_dir().'/settings.php');
3970 $settings = new admin_externalpage($section, $this->displayname,
3971 $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
3972 $adminroot->add($parentnodename, $settings);
3976 public function is_uninstall_allowed() {
3977 return true;
3982 * Class for webservice protocols
3984 class plugininfo_webservice extends plugininfo_base {
3986 public function get_settings_section_name() {
3987 return 'webservicesetting' . $this->name;
3990 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3991 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3992 $ADMIN = $adminroot; // may be used in settings.php
3993 $webservice = $this; // also can be used inside settings.php
3994 $section = $this->get_settings_section_name();
3996 $settings = null;
3997 if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3998 $settings = new admin_settingpage($section, $this->displayname,
3999 'moodle/site:config', $this->is_enabled() === false);
4000 include($this->full_path('settings.php')); // this may also set $settings to null
4002 if ($settings) {
4003 $ADMIN->add($parentnodename, $settings);
4007 public function is_enabled() {
4008 global $CFG;
4009 if (empty($CFG->enablewebservices)) {
4010 return false;
4012 $active_webservices = empty($CFG->webserviceprotocols) ? array() : explode(',', $CFG->webserviceprotocols);
4013 if (in_array($this->name, $active_webservices)) {
4014 return true;
4016 return false;
4019 public function is_uninstall_allowed() {
4020 return false;
4025 * Class for course formats
4027 class plugininfo_format extends plugininfo_base {
4030 * Gathers and returns the information about all plugins of the given type
4032 * @param string $type the name of the plugintype, eg. mod, auth or workshopform
4033 * @param string $typerootdir full path to the location of the plugin dir
4034 * @param string $typeclass the name of the actually called class
4035 * @return array of plugintype classes, indexed by the plugin name
4037 public static function get_plugins($type, $typerootdir, $typeclass) {
4038 global $CFG;
4039 $formats = parent::get_plugins($type, $typerootdir, $typeclass);
4040 require_once($CFG->dirroot.'/course/lib.php');
4041 $order = get_sorted_course_formats();
4042 $sortedformats = array();
4043 foreach ($order as $formatname) {
4044 $sortedformats[$formatname] = $formats[$formatname];
4046 return $sortedformats;
4049 public function get_settings_section_name() {
4050 return 'formatsetting' . $this->name;
4053 public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
4054 global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
4055 $ADMIN = $adminroot; // also may be used in settings.php
4056 $section = $this->get_settings_section_name();
4058 $settings = null;
4059 if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
4060 $settings = new admin_settingpage($section, $this->displayname,
4061 'moodle/site:config', $this->is_enabled() === false);
4062 include($this->full_path('settings.php')); // this may also set $settings to null
4064 if ($settings) {
4065 $ADMIN->add($parentnodename, $settings);
4069 public function is_enabled() {
4070 return !get_config($this->component, 'disabled');
4073 public function is_uninstall_allowed() {
4074 if ($this->name !== get_config('moodlecourse', 'format') && $this->name !== 'site') {
4075 return true;
4076 } else {
4077 return false;
4081 public function get_uninstall_extra_warning() {
4082 global $DB;
4084 $coursecount = $DB->count_records('course', array('format' => $this->name));
4086 if (!$coursecount) {
4087 return '';
4090 $defaultformat = $this->get_plugin_manager()->plugin_name('format_'.get_config('moodlecourse', 'format'));
4091 $message = get_string(
4092 'formatuninstallwithcourses', 'core_admin',
4093 (object)array('count' => $coursecount, 'format' => $this->displayname,
4094 'defaultformat' => $defaultformat));
4096 return $message;