cf7e1464d0431c64c76775b6d6c6f6a60fe57ced
[moodle.git] / lib / pluginlib.php
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.
14 //
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
20  *
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.
24  *
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
29  */
31 defined('MOODLE_INTERNAL') || die();
33 /**
34  * Singleton class providing general plugins management functionality
35  */
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()}
67      */
68     protected function __construct() {
69     }
71     /**
72      * Sorry, this is singleton
73      */
74     protected function __clone() {
75     }
77     /**
78      * Factory method for this class
79      *
80      * @return plugin_manager the singleton instance
81      */
82     public static function instance() {
83         if (is_null(self::$singletoninstance)) {
84             self::$singletoninstance = new self();
85         }
86         return self::$singletoninstance;
87     }
89     /**
90      * Reset any caches
91      * @param bool $phpunitreset
92      */
93     public static function reset_caches($phpunitreset = false) {
94         if ($phpunitreset) {
95             self::$singletoninstance = null;
96         }
97     }
99     /**
100      * Returns the result of {@link core_component::get_plugin_types()} ordered for humans
101      *
102      * @see self::reorder_plugin_types()
103      * @param bool $fullpaths false means relative paths from dirroot
104      * @return array (string)name => (string)location
105      */
106     public function get_plugin_types($fullpaths = true) {
107         return $this->reorder_plugin_types(core_component::get_plugin_types($fullpaths));
108     }
110     /**
111      * Returns list of known plugins of the given type
112      *
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.
115      *
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}
119      */
120     public function get_plugins_of_type($type, $disablecache=false) {
122         $plugins = $this->get_plugins($disablecache);
124         if (!isset($plugins[$type])) {
125             return array();
126         }
128         return $plugins[$type];
129     }
131     /**
132      * Returns a tree of known plugins and information about them
133      *
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}
138      */
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");
150                     }
151                 }
152             }
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');
158                 }
159                 if (class_exists('plugininfo_' . $plugintype)) {
160                     $plugintypeclass = 'plugininfo_' . $plugintype;
161                 } else {
162                     $plugintypeclass = 'plugininfo_general';
163                 }
164                 if (!in_array('plugininfo_base', class_parents($plugintypeclass))) {
165                     throw new coding_exception('Class ' . $plugintypeclass . ' must extend plugininfo_base');
166                 }
167                 $plugins = call_user_func(array($plugintypeclass, 'get_plugins'), $plugintype, $plugintyperootdir, $plugintypeclass);
168                 $this->pluginsinfo[$plugintype] = $plugins;
169             }
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);
177                     }
178                 }
179             }
180         }
182         return $this->pluginsinfo;
183     }
185     /**
186      * Returns list of all known subplugins of the given plugin
187      *
188      * For plugins that do not provide subplugins (i.e. there is no support for it),
189      * empty array is returned.
190      *
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}
194      */
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();
201         }
203         $subplugins = $this->get_subplugins($disablecache);
205         if (!isset($subplugins[$pluginfo->component])) {
206             return array();
207         }
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;
214             }
215         }
217         return $list;
218     }
220     /**
221      * Returns list of plugins that define their subplugins and the information
222      * about them from the db/subplugins.php file.
223      *
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.
227      */
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;
243                         }
244                         $this->subpluginsinfo[$type . '_' . $component] = $componentsubplugins;
245                     }
246                 }
247             }
248         }
250         return $this->subpluginsinfo;
251     }
253     /**
254      * Returns the name of the plugin that defines the given subplugin type
255      *
256      * If the given subplugin type is not actually a subplugin, returns false.
257      *
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
260      */
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;
268             }
269         }
271         return $parent;
272     }
274     /**
275      * Returns a localized name of a given plugin
276      *
277      * @param string $component name of the plugin, eg mod_workshop or auth_ldap
278      * @return string
279      */
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));
286         }
288         return $pluginfo->displayname;
289     }
291     /**
292      * Returns a localized name of a plugin typed in singular form
293      *
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.
297      *
298      * @param string $type the type of the plugin, e.g. mod or workshopform
299      * @return string
300      */
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;
313             }
315         } else {
316             return $type;
317         }
318     }
320     /**
321      * Returns a localized name of a plugin type in plural form
322      *
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.
326      *
327      * @param string $type the type of the plugin, e.g. mod or workshopform
328      * @return string
329      */
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;
342             }
344         } else {
345             return $type;
346         }
347     }
349     /**
350      * Returns information about the known plugin, or null
351      *
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.
355      */
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;
363         }
364     }
366     /**
367      * Check to see if the current version of the plugin seems to be a checkout of an external repository.
368      *
369      * @see available_update_deployer::plugin_external_source()
370      * @param string $component frankenstyle component name
371      * @return false|string
372      */
373     public function plugin_external_source($component) {
375         $plugininfo = $this->get_plugin_info($component);
377         if (is_null($plugininfo)) {
378             return false;
379         }
381         $pluginroot = $plugininfo->rootdir;
383         if (is_dir($pluginroot.'/.git')) {
384             return 'git';
385         }
387         if (is_dir($pluginroot.'/CVS')) {
388             return 'cvs';
389         }
391         if (is_dir($pluginroot.'/.svn')) {
392             return 'svn';
393         }
395         return false;
396     }
398     /**
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.
402      */
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;
410                 }
411             }
412         }
413         return $others;
414     }
416     /**
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.
420      */
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;
426             }
428             if ($requiredversion != ANY_VERSION and $otherplugin->versiondisk < $requiredversion) {
429                 return false;
430             }
431         }
433         return true;
434     }
436     /**
437      * Checks all dependencies for all installed plugins
438      *
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).
442      *
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.
446      */
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;
456                 }
458                 if (!$this->are_dependencies_satisfied($plugin->get_other_required_plugins())) {
459                     $return = false;
460                     $failedplugins[] = $plugin->component;
461                 }
462             }
463         }
465         return $return;
466     }
468     /**
469      * Is it possible to uninstall the given plugin?
470      *
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).
475      *
476      * @param string $component full frankenstyle name, e.g. mod_foobar
477      * @return bool
478      */
479     public function can_uninstall_plugin($component) {
481         $pluginfo = $this->get_plugin_info($component);
483         if (is_null($pluginfo)) {
484             return false;
485         }
487         if (!$this->common_uninstall_check($pluginfo)) {
488             return false;
489         }
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;
496             }
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;
504                 }
505             }
506         }
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;
514             }
515         }
517         return true;
518     }
520     /**
521      * Returns uninstall URL if exists.
522      *
523      * @param string $component
524      * @return moodle_url uninstall URL, null if uninstall not supported
525      */
526     public function get_uninstall_url($component) {
527         if (!$this->can_uninstall_plugin($component)) {
528             return null;
529         }
531         $pluginfo = $this->get_plugin_info($component);
533         if (is_null($pluginfo)) {
534             return null;
535         }
537         return $pluginfo->get_uninstall_url();
538     }
540     /**
541      * Uninstall the given plugin.
542      *
543      * Automatically cleans-up all remaining configuration data, log records, events,
544      * files from the file pool etc.
545      *
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.
549      *
550      * @param string $component
551      * @param progress_trace $progress traces the process
552      * @return bool true on success, false on errors/problems
553      */
554     public function uninstall_plugin($component, progress_trace $progress) {
556         $pluginfo = $this->get_plugin_info($component);
558         if (is_null($pluginfo)) {
559             return false;
560         }
562         // Give the pluginfo class a chance to execute some steps.
563         $result = $pluginfo->uninstall($progress);
564         if (!$result) {
565             return false;
566         }
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;
574     }
576     /**
577      * Checks if there are some plugins with a known available update
578      *
579      * @return bool true if there is at least one available update
580      */
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;
586                 }
587             }
588         }
590         return false;
591     }
593     /**
594      * Check to see if the given plugin folder can be removed by the web server process.
595      *
596      * @param string $component full frankenstyle component
597      * @return bool
598      */
599     public function is_plugin_folder_removable($component) {
601         $pluginfo = $this->get_plugin_info($component);
603         if (is_null($pluginfo)) {
604             return false;
605         }
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;
610         }
612         // Check that the folder and all its content is writable (thence removable).
613         return $this->is_directory_removable($pluginfo->rootdir);
614     }
616     /**
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.
619      *
620      * The main purpose of this list is to hide missing plugins during upgrade.
621      *
622      * @param string $type plugin type
623      * @param string $name plugin name
624      * @return bool
625      */
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'),
640         );
642         if (!isset($plugins[$type])) {
643             return false;
644         }
645         return in_array($name, $plugins[$type]);
646     }
648     /**
649      * Defines a white list of all plugins shipped in the standard Moodle distribution
650      *
651      * @param string $type
652      * @return false|array array of standard plugins or false if the type is unknown
653      */
654     public static function standard_plugins_list($type) {
655         $standard_plugins = array(
657             'assignment' => array(
658                 'offline', 'online', 'upload', 'uploadsingle'
659             ),
661             'assignsubmission' => array(
662                 'comments', 'file', 'onlinetext'
663             ),
665             'assignfeedback' => array(
666                 'comments', 'file', 'offline'
667             ),
669             'auth' => array(
670                 'cas', 'db', 'email', 'fc', 'imap', 'ldap', 'manual', 'mnet',
671                 'nntp', 'nologin', 'none', 'pam', 'pop3', 'radius',
672                 'shibboleth', 'webservice'
673             ),
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'
687             ),
689             'booktool' => array(
690                 'exportimscp', 'importhtml', 'print'
691             ),
693             'cachelock' => array(
694                 'file'
695             ),
697             'cachestore' => array(
698                 'file', 'memcache', 'memcached', 'mongodb', 'session', 'static'
699             ),
701             'coursereport' => array(
702                 //deprecated!
703             ),
705             'datafield' => array(
706                 'checkbox', 'date', 'file', 'latlong', 'menu', 'multimenu',
707                 'number', 'picture', 'radiobutton', 'text', 'textarea', 'url'
708             ),
710             'datapreset' => array(
711                 'imagegallery'
712             ),
714             'editor' => array(
715                 'textarea', 'tinymce'
716             ),
718             'enrol' => array(
719                 'category', 'cohort', 'database', 'flatfile',
720                 'guest', 'imsenterprise', 'ldap', 'manual', 'meta', 'mnet',
721                 'paypal', 'self'
722             ),
724             'filter' => array(
725                 'activitynames', 'algebra', 'censor', 'emailprotect',
726                 'emoticon', 'mediaplugin', 'multilang', 'tex', 'tidy',
727                 'urltolink', 'data', 'glossary'
728             ),
730             'format' => array(
731                 'scorm', 'singleactivity', 'social', 'topics', 'weeks'
732             ),
734             'gradeexport' => array(
735                 'ods', 'txt', 'xls', 'xml'
736             ),
738             'gradeimport' => array(
739                 'csv', 'xml'
740             ),
742             'gradereport' => array(
743                 'grader', 'outcomes', 'overview', 'user'
744             ),
746             'gradingform' => array(
747                 'rubric', 'guide'
748             ),
750             'local' => array(
751             ),
753             'message' => array(
754                 'email', 'jabber', 'popup'
755             ),
757             'mnetservice' => array(
758                 'enrol'
759             ),
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'
765             ),
767             'plagiarism' => array(
768             ),
770             'portfolio' => array(
771                 'boxnet', 'download', 'flickr', 'googledocs', 'mahara', 'picasa'
772             ),
774             'profilefield' => array(
775                 'checkbox', 'datetime', 'menu', 'text', 'textarea'
776             ),
778             'qbehaviour' => array(
779                 'adaptive', 'adaptivenopenalty', 'deferredcbm',
780                 'deferredfeedback', 'immediatecbm', 'immediatefeedback',
781                 'informationitem', 'interactive', 'interactivecountback',
782                 'manualgraded', 'missing'
783             ),
785             'qformat' => array(
786                 'aiken', 'blackboard_six', 'examview', 'gift',
787                 'learnwise', 'missingword', 'multianswer', 'webct',
788                 'xhtml', 'xml'
789             ),
791             'qtype' => array(
792                 'calculated', 'calculatedmulti', 'calculatedsimple',
793                 'description', 'essay', 'match', 'missingtype', 'multianswer',
794                 'multichoice', 'numerical', 'random', 'randomsamatch',
795                 'shortanswer', 'truefalse'
796             ),
798             'quiz' => array(
799                 'grading', 'overview', 'responses', 'statistics'
800             ),
802             'quizaccess' => array(
803                 'delaybetweenattempts', 'ipaddress', 'numattempts', 'openclosedate',
804                 'password', 'safebrowser', 'securewindow', 'timelimit'
805             ),
807             'report' => array(
808                 'backups', 'completion', 'configlog', 'courseoverview',
809                 'log', 'loglive', 'outline', 'participation', 'progress', 'questioninstances', 'security', 'stats', 'performance'
810             ),
812             'repository' => array(
813                 'alfresco', 'areafiles', 'boxnet', 'coursefiles', 'dropbox', 'equella', 'filesystem',
814                 'flickr', 'flickr_public', 'googledocs', 'local', 'merlot',
815                 'picasa', 'recent', 'skydrive', 's3', 'upload', 'url', 'user', 'webdav',
816                 'wikimedia', 'youtube'
817             ),
819             'scormreport' => array(
820                 'basic',
821                 'interactions',
822                 'graphs',
823                 'objectives'
824             ),
826             'tinymce' => array(
827                 'ctrlhelp', 'dragmath', 'managefiles', 'moodleemoticon', 'moodleimage',
828                 'moodlemedia', 'moodlenolink', 'pdw', 'spellchecker', 'wrap'
829             ),
831             'theme' => array(
832                 'afterburner', 'anomaly', 'arialist', 'base', 'binarius', 'bootstrapbase',
833                 'boxxie', 'brick', 'canvas', 'clean', 'formal_white', 'formfactor',
834                 'fusion', 'leatherbound', 'magazine', 'mymobile', 'nimble',
835                 'nonzero', 'overlay', 'serenity', 'sky_high', 'splash',
836                 'standard', 'standardold'
837             ),
839             'tool' => array(
840                 'assignmentupgrade', 'behat', 'capability', 'customlang',
841                 'dbtransfer', 'generator', 'health', 'innodb', 'installaddon',
842                 'langimport', 'multilangupgrade', 'phpunit', 'profiling',
843                 'qeupgradehelper', 'replace', 'spamcleaner', 'timezoneimport',
844                 'unittest', 'uploadcourse', 'uploaduser', 'unsuproles', 'xmldb'
845             ),
847             'webservice' => array(
848                 'amf', 'rest', 'soap', 'xmlrpc'
849             ),
851             'workshopallocation' => array(
852                 'manual', 'random', 'scheduled'
853             ),
855             'workshopeval' => array(
856                 'best'
857             ),
859             'workshopform' => array(
860                 'accumulative', 'comments', 'numerrors', 'rubric'
861             )
862         );
864         if (isset($standard_plugins[$type])) {
865             return $standard_plugins[$type];
866         } else {
867             return false;
868         }
869     }
871     /**
872      * Wrapper for the core function {@link core_component::normalize_component()}.
873      *
874      * This is here just to make it possible to mock it in unit tests.
875      *
876      * @param string $component
877      * @return array
878      */
879     protected function normalize_component($component) {
880         return core_component::normalize_component($component);
881     }
883     /**
884      * Reorders plugin types into a sequence to be displayed
885      *
886      * For technical reasons, plugin types returned by {@link core_component::get_plugin_types()} are
887      * in a certain order that does not need to fit the expected order for the display.
888      * Particularly, activity modules should be displayed first as they represent the
889      * real heart of Moodle. They should be followed by other plugin types that are
890      * used to build the courses (as that is what one expects from LMS). After that,
891      * other supportive plugin types follow.
892      *
893      * @param array $types associative array
894      * @return array same array with altered order of items
895      */
896     protected function reorder_plugin_types(array $types) {
897         $fix = array(
898             'mod'        => $types['mod'],
899             'block'      => $types['block'],
900             'qtype'      => $types['qtype'],
901             'qbehaviour' => $types['qbehaviour'],
902             'qformat'    => $types['qformat'],
903             'filter'     => $types['filter'],
904             'enrol'      => $types['enrol'],
905         );
906         foreach ($types as $type => $path) {
907             if (!isset($fix[$type])) {
908                 $fix[$type] = $path;
909             }
910         }
911         return $fix;
912     }
914     /**
915      * Check if the given directory can be removed by the web server process.
916      *
917      * This recursively checks that the given directory and all its contents
918      * it writable.
919      *
920      * @param string $fullpath
921      * @return boolean
922      */
923     protected function is_directory_removable($fullpath) {
925         if (!is_writable($fullpath)) {
926             return false;
927         }
929         if (is_dir($fullpath)) {
930             $handle = opendir($fullpath);
931         } else {
932             return false;
933         }
935         $result = true;
937         while ($filename = readdir($handle)) {
939             if ($filename === '.' or $filename === '..') {
940                 continue;
941             }
943             $subfilepath = $fullpath.'/'.$filename;
945             if (is_dir($subfilepath)) {
946                 $result = $result && $this->is_directory_removable($subfilepath);
948             } else {
949                 $result = $result && is_writable($subfilepath);
950             }
951         }
953         closedir($handle);
955         return $result;
956     }
958     /**
959      * Helper method that implements common uninstall prerequisities
960      *
961      * @param plugininfo_base $pluginfo
962      * @return bool
963      */
964     protected function common_uninstall_check(plugininfo_base $pluginfo) {
966         if (!$pluginfo->is_uninstall_allowed()) {
967             // The plugin's plugininfo class declares it should not be uninstalled.
968             return false;
969         }
971         if ($pluginfo->get_status() === plugin_manager::PLUGIN_STATUS_NEW) {
972             // The plugin is not installed. It should be either installed or removed from the disk.
973             // Relying on this temporary state may be tricky.
974             return false;
975         }
977         if (is_null($pluginfo->get_uninstall_url())) {
978             // Backwards compatibility.
979             debugging('plugininfo_base subclasses should use is_uninstall_allowed() instead of returning null in get_uninstall_url()',
980                 DEBUG_DEVELOPER);
981             return false;
982         }
984         return true;
985     }
989 /**
990  * General exception thrown by the {@link available_update_checker} class
991  */
992 class available_update_checker_exception extends moodle_exception {
994     /**
995      * @param string $errorcode exception description identifier
996      * @param mixed $debuginfo debugging data to display
997      */
998     public function __construct($errorcode, $debuginfo=null) {
999         parent::__construct($errorcode, 'core_plugin', '', null, print_r($debuginfo, true));
1000     }
1004 /**
1005  * Singleton class that handles checking for available updates
1006  */
1007 class available_update_checker {
1009     /** @var available_update_checker holds the singleton instance */
1010     protected static $singletoninstance;
1011     /** @var null|int the timestamp of when the most recent response was fetched */
1012     protected $recentfetch = null;
1013     /** @var null|array the recent response from the update notification provider */
1014     protected $recentresponse = null;
1015     /** @var null|string the numerical version of the local Moodle code */
1016     protected $currentversion = null;
1017     /** @var null|string the release info of the local Moodle code */
1018     protected $currentrelease = null;
1019     /** @var null|string branch of the local Moodle code */
1020     protected $currentbranch = null;
1021     /** @var array of (string)frankestyle => (string)version list of additional plugins deployed at this site */
1022     protected $currentplugins = array();
1024     /**
1025      * Direct initiation not allowed, use the factory method {@link self::instance()}
1026      */
1027     protected function __construct() {
1028     }
1030     /**
1031      * Sorry, this is singleton
1032      */
1033     protected function __clone() {
1034     }
1036     /**
1037      * Factory method for this class
1038      *
1039      * @return available_update_checker the singleton instance
1040      */
1041     public static function instance() {
1042         if (is_null(self::$singletoninstance)) {
1043             self::$singletoninstance = new self();
1044         }
1045         return self::$singletoninstance;
1046     }
1048     /**
1049      * Reset any caches
1050      * @param bool $phpunitreset
1051      */
1052     public static function reset_caches($phpunitreset = false) {
1053         if ($phpunitreset) {
1054             self::$singletoninstance = null;
1055         }
1056     }
1058     /**
1059      * Returns the timestamp of the last execution of {@link fetch()}
1060      *
1061      * @return int|null null if it has never been executed or we don't known
1062      */
1063     public function get_last_timefetched() {
1065         $this->restore_response();
1067         if (!empty($this->recentfetch)) {
1068             return $this->recentfetch;
1070         } else {
1071             return null;
1072         }
1073     }
1075     /**
1076      * Fetches the available update status from the remote site
1077      *
1078      * @throws available_update_checker_exception
1079      */
1080     public function fetch() {
1081         $response = $this->get_response();
1082         $this->validate_response($response);
1083         $this->store_response($response);
1084     }
1086     /**
1087      * Returns the available update information for the given component
1088      *
1089      * This method returns null if the most recent response does not contain any information
1090      * about it. The returned structure is an array of available updates for the given
1091      * component. Each update info is an object with at least one property called
1092      * 'version'. Other possible properties are 'release', 'maturity', 'url' and 'downloadurl'.
1093      *
1094      * For the 'core' component, the method returns real updates only (those with higher version).
1095      * For all other components, the list of all known remote updates is returned and the caller
1096      * (usually the {@link plugin_manager}) is supposed to make the actual comparison of versions.
1097      *
1098      * @param string $component frankenstyle
1099      * @param array $options with supported keys 'minmaturity' and/or 'notifybuilds'
1100      * @return null|array null or array of available_update_info objects
1101      */
1102     public function get_update_info($component, array $options = array()) {
1104         if (!isset($options['minmaturity'])) {
1105             $options['minmaturity'] = 0;
1106         }
1108         if (!isset($options['notifybuilds'])) {
1109             $options['notifybuilds'] = false;
1110         }
1112         if ($component == 'core') {
1113             $this->load_current_environment();
1114         }
1116         $this->restore_response();
1118         if (empty($this->recentresponse['updates'][$component])) {
1119             return null;
1120         }
1122         $updates = array();
1123         foreach ($this->recentresponse['updates'][$component] as $info) {
1124             $update = new available_update_info($component, $info);
1125             if (isset($update->maturity) and ($update->maturity < $options['minmaturity'])) {
1126                 continue;
1127             }
1128             if ($component == 'core') {
1129                 if ($update->version <= $this->currentversion) {
1130                     continue;
1131                 }
1132                 if (empty($options['notifybuilds']) and $this->is_same_release($update->release)) {
1133                     continue;
1134                 }
1135             }
1136             $updates[] = $update;
1137         }
1139         if (empty($updates)) {
1140             return null;
1141         }
1143         return $updates;
1144     }
1146     /**
1147      * The method being run via cron.php
1148      */
1149     public function cron() {
1150         global $CFG;
1152         if (!$this->cron_autocheck_enabled()) {
1153             $this->cron_mtrace('Automatic check for available updates not enabled, skipping.');
1154             return;
1155         }
1157         $now = $this->cron_current_timestamp();
1159         if ($this->cron_has_fresh_fetch($now)) {
1160             $this->cron_mtrace('Recently fetched info about available updates is still fresh enough, skipping.');
1161             return;
1162         }
1164         if ($this->cron_has_outdated_fetch($now)) {
1165             $this->cron_mtrace('Outdated or missing info about available updates, forced fetching ... ', '');
1166             $this->cron_execute();
1167             return;
1168         }
1170         $offset = $this->cron_execution_offset();
1171         $start = mktime(1, 0, 0, date('n', $now), date('j', $now), date('Y', $now)); // 01:00 AM today local time
1172         if ($now > $start + $offset) {
1173             $this->cron_mtrace('Regular daily check for available updates ... ', '');
1174             $this->cron_execute();
1175             return;
1176         }
1177     }
1179     /// end of public API //////////////////////////////////////////////////////
1181     /**
1182      * Makes cURL request to get data from the remote site
1183      *
1184      * @return string raw request result
1185      * @throws available_update_checker_exception
1186      */
1187     protected function get_response() {
1188         global $CFG;
1189         require_once($CFG->libdir.'/filelib.php');
1191         $curl = new curl(array('proxy' => true));
1192         $response = $curl->post($this->prepare_request_url(), $this->prepare_request_params(), $this->prepare_request_options());
1193         $curlerrno = $curl->get_errno();
1194         if (!empty($curlerrno)) {
1195             throw new available_update_checker_exception('err_response_curl', 'cURL error '.$curlerrno.': '.$curl->error);
1196         }
1197         $curlinfo = $curl->get_info();
1198         if ($curlinfo['http_code'] != 200) {
1199             throw new available_update_checker_exception('err_response_http_code', $curlinfo['http_code']);
1200         }
1201         return $response;
1202     }
1204     /**
1205      * Makes sure the response is valid, has correct API format etc.
1206      *
1207      * @param string $response raw response as returned by the {@link self::get_response()}
1208      * @throws available_update_checker_exception
1209      */
1210     protected function validate_response($response) {
1212         $response = $this->decode_response($response);
1214         if (empty($response)) {
1215             throw new available_update_checker_exception('err_response_empty');
1216         }
1218         if (empty($response['status']) or $response['status'] !== 'OK') {
1219             throw new available_update_checker_exception('err_response_status', $response['status']);
1220         }
1222         if (empty($response['apiver']) or $response['apiver'] !== '1.2') {
1223             throw new available_update_checker_exception('err_response_format_version', $response['apiver']);
1224         }
1226         if (empty($response['forbranch']) or $response['forbranch'] !== moodle_major_version(true)) {
1227             throw new available_update_checker_exception('err_response_target_version', $response['forbranch']);
1228         }
1229     }
1231     /**
1232      * Decodes the raw string response from the update notifications provider
1233      *
1234      * @param string $response as returned by {@link self::get_response()}
1235      * @return array decoded response structure
1236      */
1237     protected function decode_response($response) {
1238         return json_decode($response, true);
1239     }
1241     /**
1242      * Stores the valid fetched response for later usage
1243      *
1244      * This implementation uses the config_plugins table as the permanent storage.
1245      *
1246      * @param string $response raw valid data returned by {@link self::get_response()}
1247      */
1248     protected function store_response($response) {
1250         set_config('recentfetch', time(), 'core_plugin');
1251         set_config('recentresponse', $response, 'core_plugin');
1253         $this->restore_response(true);
1254     }
1256     /**
1257      * Loads the most recent raw response record we have fetched
1258      *
1259      * After this method is called, $this->recentresponse is set to an array. If the
1260      * array is empty, then either no data have been fetched yet or the fetched data
1261      * do not have expected format (and thence they are ignored and a debugging
1262      * message is displayed).
1263      *
1264      * This implementation uses the config_plugins table as the permanent storage.
1265      *
1266      * @param bool $forcereload reload even if it was already loaded
1267      */
1268     protected function restore_response($forcereload = false) {
1270         if (!$forcereload and !is_null($this->recentresponse)) {
1271             // we already have it, nothing to do
1272             return;
1273         }
1275         $config = get_config('core_plugin');
1277         if (!empty($config->recentresponse) and !empty($config->recentfetch)) {
1278             try {
1279                 $this->validate_response($config->recentresponse);
1280                 $this->recentfetch = $config->recentfetch;
1281                 $this->recentresponse = $this->decode_response($config->recentresponse);
1282             } catch (available_update_checker_exception $e) {
1283                 // The server response is not valid. Behave as if no data were fetched yet.
1284                 // This may happen when the most recent update info (cached locally) has been
1285                 // fetched with the previous branch of Moodle (like during an upgrade from 2.x
1286                 // to 2.y) or when the API of the response has changed.
1287                 $this->recentresponse = array();
1288             }
1290         } else {
1291             $this->recentresponse = array();
1292         }
1293     }
1295     /**
1296      * Compares two raw {@link $recentresponse} records and returns the list of changed updates
1297      *
1298      * This method is used to populate potential update info to be sent to site admins.
1299      *
1300      * @param array $old
1301      * @param array $new
1302      * @throws available_update_checker_exception
1303      * @return array parts of $new['updates'] that have changed
1304      */
1305     protected function compare_responses(array $old, array $new) {
1307         if (empty($new)) {
1308             return array();
1309         }
1311         if (!array_key_exists('updates', $new)) {
1312             throw new available_update_checker_exception('err_response_format');
1313         }
1315         if (empty($old)) {
1316             return $new['updates'];
1317         }
1319         if (!array_key_exists('updates', $old)) {
1320             throw new available_update_checker_exception('err_response_format');
1321         }
1323         $changes = array();
1325         foreach ($new['updates'] as $newcomponent => $newcomponentupdates) {
1326             if (empty($old['updates'][$newcomponent])) {
1327                 $changes[$newcomponent] = $newcomponentupdates;
1328                 continue;
1329             }
1330             foreach ($newcomponentupdates as $newcomponentupdate) {
1331                 $inold = false;
1332                 foreach ($old['updates'][$newcomponent] as $oldcomponentupdate) {
1333                     if ($newcomponentupdate['version'] == $oldcomponentupdate['version']) {
1334                         $inold = true;
1335                     }
1336                 }
1337                 if (!$inold) {
1338                     if (!isset($changes[$newcomponent])) {
1339                         $changes[$newcomponent] = array();
1340                     }
1341                     $changes[$newcomponent][] = $newcomponentupdate;
1342                 }
1343             }
1344         }
1346         return $changes;
1347     }
1349     /**
1350      * Returns the URL to send update requests to
1351      *
1352      * During the development or testing, you can set $CFG->alternativeupdateproviderurl
1353      * to a custom URL that will be used. Otherwise the standard URL will be returned.
1354      *
1355      * @return string URL
1356      */
1357     protected function prepare_request_url() {
1358         global $CFG;
1360         if (!empty($CFG->config_php_settings['alternativeupdateproviderurl'])) {
1361             return $CFG->config_php_settings['alternativeupdateproviderurl'];
1362         } else {
1363             return 'https://download.moodle.org/api/1.2/updates.php';
1364         }
1365     }
1367     /**
1368      * Sets the properties currentversion, currentrelease, currentbranch and currentplugins
1369      *
1370      * @param bool $forcereload
1371      */
1372     protected function load_current_environment($forcereload=false) {
1373         global $CFG;
1375         if (!is_null($this->currentversion) and !$forcereload) {
1376             // nothing to do
1377             return;
1378         }
1380         $version = null;
1381         $release = null;
1383         require($CFG->dirroot.'/version.php');
1384         $this->currentversion = $version;
1385         $this->currentrelease = $release;
1386         $this->currentbranch = moodle_major_version(true);
1388         $pluginman = plugin_manager::instance();
1389         foreach ($pluginman->get_plugins() as $type => $plugins) {
1390             foreach ($plugins as $plugin) {
1391                 if (!$plugin->is_standard()) {
1392                     $this->currentplugins[$plugin->component] = $plugin->versiondisk;
1393                 }
1394             }
1395         }
1396     }
1398     /**
1399      * Returns the list of HTTP params to be sent to the updates provider URL
1400      *
1401      * @return array of (string)param => (string)value
1402      */
1403     protected function prepare_request_params() {
1404         global $CFG;
1406         $this->load_current_environment();
1407         $this->restore_response();
1409         $params = array();
1410         $params['format'] = 'json';
1412         if (isset($this->recentresponse['ticket'])) {
1413             $params['ticket'] = $this->recentresponse['ticket'];
1414         }
1416         if (isset($this->currentversion)) {
1417             $params['version'] = $this->currentversion;
1418         } else {
1419             throw new coding_exception('Main Moodle version must be already known here');
1420         }
1422         if (isset($this->currentbranch)) {
1423             $params['branch'] = $this->currentbranch;
1424         } else {
1425             throw new coding_exception('Moodle release must be already known here');
1426         }
1428         $plugins = array();
1429         foreach ($this->currentplugins as $plugin => $version) {
1430             $plugins[] = $plugin.'@'.$version;
1431         }
1432         if (!empty($plugins)) {
1433             $params['plugins'] = implode(',', $plugins);
1434         }
1436         return $params;
1437     }
1439     /**
1440      * Returns the list of cURL options to use when fetching available updates data
1441      *
1442      * @return array of (string)param => (string)value
1443      */
1444     protected function prepare_request_options() {
1445         global $CFG;
1447         $options = array(
1448             'CURLOPT_SSL_VERIFYHOST' => 2,      // this is the default in {@link curl} class but just in case
1449             'CURLOPT_SSL_VERIFYPEER' => true,
1450         );
1452         return $options;
1453     }
1455     /**
1456      * Returns the current timestamp
1457      *
1458      * @return int the timestamp
1459      */
1460     protected function cron_current_timestamp() {
1461         return time();
1462     }
1464     /**
1465      * Output cron debugging info
1466      *
1467      * @see mtrace()
1468      * @param string $msg output message
1469      * @param string $eol end of line
1470      */
1471     protected function cron_mtrace($msg, $eol = PHP_EOL) {
1472         mtrace($msg, $eol);
1473     }
1475     /**
1476      * Decide if the autocheck feature is disabled in the server setting
1477      *
1478      * @return bool true if autocheck enabled, false if disabled
1479      */
1480     protected function cron_autocheck_enabled() {
1481         global $CFG;
1483         if (empty($CFG->updateautocheck)) {
1484             return false;
1485         } else {
1486             return true;
1487         }
1488     }
1490     /**
1491      * Decide if the recently fetched data are still fresh enough
1492      *
1493      * @param int $now current timestamp
1494      * @return bool true if no need to re-fetch, false otherwise
1495      */
1496     protected function cron_has_fresh_fetch($now) {
1497         $recent = $this->get_last_timefetched();
1499         if (empty($recent)) {
1500             return false;
1501         }
1503         if ($now < $recent) {
1504             $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1505             return true;
1506         }
1508         if ($now - $recent > 24 * HOURSECS) {
1509             return false;
1510         }
1512         return true;
1513     }
1515     /**
1516      * Decide if the fetch is outadated or even missing
1517      *
1518      * @param int $now current timestamp
1519      * @return bool false if no need to re-fetch, true otherwise
1520      */
1521     protected function cron_has_outdated_fetch($now) {
1522         $recent = $this->get_last_timefetched();
1524         if (empty($recent)) {
1525             return true;
1526         }
1528         if ($now < $recent) {
1529             $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1530             return false;
1531         }
1533         if ($now - $recent > 48 * HOURSECS) {
1534             return true;
1535         }
1537         return false;
1538     }
1540     /**
1541      * Returns the cron execution offset for this site
1542      *
1543      * The main {@link self::cron()} is supposed to run every night in some random time
1544      * between 01:00 and 06:00 AM (local time). The exact moment is defined by so called
1545      * execution offset, that is the amount of time after 01:00 AM. The offset value is
1546      * initially generated randomly and then used consistently at the site. This way, the
1547      * regular checks against the download.moodle.org server are spread in time.
1548      *
1549      * @return int the offset number of seconds from range 1 sec to 5 hours
1550      */
1551     protected function cron_execution_offset() {
1552         global $CFG;
1554         if (empty($CFG->updatecronoffset)) {
1555             set_config('updatecronoffset', rand(1, 5 * HOURSECS));
1556         }
1558         return $CFG->updatecronoffset;
1559     }
1561     /**
1562      * Fetch available updates info and eventually send notification to site admins
1563      */
1564     protected function cron_execute() {
1566         try {
1567             $this->restore_response();
1568             $previous = $this->recentresponse;
1569             $this->fetch();
1570             $this->restore_response(true);
1571             $current = $this->recentresponse;
1572             $changes = $this->compare_responses($previous, $current);
1573             $notifications = $this->cron_notifications($changes);
1574             $this->cron_notify($notifications);
1575             $this->cron_mtrace('done');
1576         } catch (available_update_checker_exception $e) {
1577             $this->cron_mtrace('FAILED!');
1578         }
1579     }
1581     /**
1582      * Given the list of changes in available updates, pick those to send to site admins
1583      *
1584      * @param array $changes as returned by {@link self::compare_responses()}
1585      * @return array of available_update_info objects to send to site admins
1586      */
1587     protected function cron_notifications(array $changes) {
1588         global $CFG;
1590         $notifications = array();
1591         $pluginman = plugin_manager::instance();
1592         $plugins = $pluginman->get_plugins(true);
1594         foreach ($changes as $component => $componentchanges) {
1595             if (empty($componentchanges)) {
1596                 continue;
1597             }
1598             $componentupdates = $this->get_update_info($component,
1599                 array('minmaturity' => $CFG->updateminmaturity, 'notifybuilds' => $CFG->updatenotifybuilds));
1600             if (empty($componentupdates)) {
1601                 continue;
1602             }
1603             // notify only about those $componentchanges that are present in $componentupdates
1604             // to respect the preferences
1605             foreach ($componentchanges as $componentchange) {
1606                 foreach ($componentupdates as $componentupdate) {
1607                     if ($componentupdate->version == $componentchange['version']) {
1608                         if ($component == 'core') {
1609                             // In case of 'core', we already know that the $componentupdate
1610                             // is a real update with higher version ({@see self::get_update_info()}).
1611                             // We just perform additional check for the release property as there
1612                             // can be two Moodle releases having the same version (e.g. 2.4.0 and 2.5dev shortly
1613                             // after the release). We can do that because we have the release info
1614                             // always available for the core.
1615                             if ((string)$componentupdate->release === (string)$componentchange['release']) {
1616                                 $notifications[] = $componentupdate;
1617                             }
1618                         } else {
1619                             // Use the plugin_manager to check if the detected $componentchange
1620                             // is a real update with higher version. That is, the $componentchange
1621                             // is present in the array of {@link available_update_info} objects
1622                             // returned by the plugin's available_updates() method.
1623                             list($plugintype, $pluginname) = core_component::normalize_component($component);
1624                             if (!empty($plugins[$plugintype][$pluginname])) {
1625                                 $availableupdates = $plugins[$plugintype][$pluginname]->available_updates();
1626                                 if (!empty($availableupdates)) {
1627                                     foreach ($availableupdates as $availableupdate) {
1628                                         if ($availableupdate->version == $componentchange['version']) {
1629                                             $notifications[] = $componentupdate;
1630                                         }
1631                                     }
1632                                 }
1633                             }
1634                         }
1635                     }
1636                 }
1637             }
1638         }
1640         return $notifications;
1641     }
1643     /**
1644      * Sends the given notifications to site admins via messaging API
1645      *
1646      * @param array $notifications array of available_update_info objects to send
1647      */
1648     protected function cron_notify(array $notifications) {
1649         global $CFG;
1651         if (empty($notifications)) {
1652             return;
1653         }
1655         $admins = get_admins();
1657         if (empty($admins)) {
1658             return;
1659         }
1661         $this->cron_mtrace('sending notifications ... ', '');
1663         $text = get_string('updatenotifications', 'core_admin') . PHP_EOL;
1664         $html = html_writer::tag('h1', get_string('updatenotifications', 'core_admin')) . PHP_EOL;
1666         $coreupdates = array();
1667         $pluginupdates = array();
1669         foreach ($notifications as $notification) {
1670             if ($notification->component == 'core') {
1671                 $coreupdates[] = $notification;
1672             } else {
1673                 $pluginupdates[] = $notification;
1674             }
1675         }
1677         if (!empty($coreupdates)) {
1678             $text .= PHP_EOL . get_string('updateavailable', 'core_admin') . PHP_EOL;
1679             $html .= html_writer::tag('h2', get_string('updateavailable', 'core_admin')) . PHP_EOL;
1680             $html .= html_writer::start_tag('ul') . PHP_EOL;
1681             foreach ($coreupdates as $coreupdate) {
1682                 $html .= html_writer::start_tag('li');
1683                 if (isset($coreupdate->release)) {
1684                     $text .= get_string('updateavailable_release', 'core_admin', $coreupdate->release);
1685                     $html .= html_writer::tag('strong', get_string('updateavailable_release', 'core_admin', $coreupdate->release));
1686                 }
1687                 if (isset($coreupdate->version)) {
1688                     $text .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
1689                     $html .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
1690                 }
1691                 if (isset($coreupdate->maturity)) {
1692                     $text .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
1693                     $html .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
1694                 }
1695                 $text .= PHP_EOL;
1696                 $html .= html_writer::end_tag('li') . PHP_EOL;
1697             }
1698             $text .= PHP_EOL;
1699             $html .= html_writer::end_tag('ul') . PHP_EOL;
1701             $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/index.php');
1702             $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
1703             $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/index.php', $CFG->wwwroot.'/'.$CFG->admin.'/index.php'));
1704             $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
1705         }
1707         if (!empty($pluginupdates)) {
1708             $text .= PHP_EOL . get_string('updateavailableforplugin', 'core_admin') . PHP_EOL;
1709             $html .= html_writer::tag('h2', get_string('updateavailableforplugin', 'core_admin')) . PHP_EOL;
1711             $html .= html_writer::start_tag('ul') . PHP_EOL;
1712             foreach ($pluginupdates as $pluginupdate) {
1713                 $html .= html_writer::start_tag('li');
1714                 $text .= get_string('pluginname', $pluginupdate->component);
1715                 $html .= html_writer::tag('strong', get_string('pluginname', $pluginupdate->component));
1717                 $text .= ' ('.$pluginupdate->component.')';
1718                 $html .= ' ('.$pluginupdate->component.')';
1720                 $text .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
1721                 $html .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
1723                 $text .= PHP_EOL;
1724                 $html .= html_writer::end_tag('li') . PHP_EOL;
1725             }
1726             $text .= PHP_EOL;
1727             $html .= html_writer::end_tag('ul') . PHP_EOL;
1729             $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php');
1730             $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
1731             $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/plugins.php', $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php'));
1732             $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
1733         }
1735         $a = array('siteurl' => $CFG->wwwroot);
1736         $text .= get_string('updatenotificationfooter', 'core_admin', $a) . PHP_EOL;
1737         $a = array('siteurl' => html_writer::link($CFG->wwwroot, $CFG->wwwroot));
1738         $html .= html_writer::tag('footer', html_writer::tag('p', get_string('updatenotificationfooter', 'core_admin', $a),
1739             array('style' => 'font-size:smaller; color:#333;')));
1741         foreach ($admins as $admin) {
1742             $message = new stdClass();
1743             $message->component         = 'moodle';
1744             $message->name              = 'availableupdate';
1745             $message->userfrom          = get_admin();
1746             $message->userto            = $admin;
1747             $message->subject           = get_string('updatenotificationsubject', 'core_admin', array('siteurl' => $CFG->wwwroot));
1748             $message->fullmessage       = $text;
1749             $message->fullmessageformat = FORMAT_PLAIN;
1750             $message->fullmessagehtml   = $html;
1751             $message->smallmessage      = get_string('updatenotifications', 'core_admin');
1752             $message->notification      = 1;
1753             message_send($message);
1754         }
1755     }
1757     /**
1758      * Compare two release labels and decide if they are the same
1759      *
1760      * @param string $remote release info of the available update
1761      * @param null|string $local release info of the local code, defaults to $release defined in version.php
1762      * @return boolean true if the releases declare the same minor+major version
1763      */
1764     protected function is_same_release($remote, $local=null) {
1766         if (is_null($local)) {
1767             $this->load_current_environment();
1768             $local = $this->currentrelease;
1769         }
1771         $pattern = '/^([0-9\.\+]+)([^(]*)/';
1773         preg_match($pattern, $remote, $remotematches);
1774         preg_match($pattern, $local, $localmatches);
1776         $remotematches[1] = str_replace('+', '', $remotematches[1]);
1777         $localmatches[1] = str_replace('+', '', $localmatches[1]);
1779         if ($remotematches[1] === $localmatches[1] and rtrim($remotematches[2]) === rtrim($localmatches[2])) {
1780             return true;
1781         } else {
1782             return false;
1783         }
1784     }
1788 /**
1789  * Defines the structure of objects returned by {@link available_update_checker::get_update_info()}
1790  */
1791 class available_update_info {
1793     /** @var string frankenstyle component name */
1794     public $component;
1795     /** @var int the available version of the component */
1796     public $version;
1797     /** @var string|null optional release name */
1798     public $release = null;
1799     /** @var int|null optional maturity info, eg {@link MATURITY_STABLE} */
1800     public $maturity = null;
1801     /** @var string|null optional URL of a page with more info about the update */
1802     public $url = null;
1803     /** @var string|null optional URL of a ZIP package that can be downloaded and installed */
1804     public $download = null;
1805     /** @var string|null of self::download is set, then this must be the MD5 hash of the ZIP */
1806     public $downloadmd5 = null;
1808     /**
1809      * Creates new instance of the class
1810      *
1811      * The $info array must provide at least the 'version' value and optionally all other
1812      * values to populate the object's properties.
1813      *
1814      * @param string $name the frankenstyle component name
1815      * @param array $info associative array with other properties
1816      */
1817     public function __construct($name, array $info) {
1818         $this->component = $name;
1819         foreach ($info as $k => $v) {
1820             if (property_exists('available_update_info', $k) and $k != 'component') {
1821                 $this->$k = $v;
1822             }
1823         }
1824     }
1828 /**
1829  * Implements a communication bridge to the mdeploy.php utility
1830  */
1831 class available_update_deployer {
1833     const HTTP_PARAM_PREFIX     = 'updteautodpldata_';  // Hey, even Google has not heard of such a prefix! So it MUST be safe :-p
1834     const HTTP_PARAM_CHECKER    = 'datapackagesize';    // Name of the parameter that holds the number of items in the received data items
1836     /** @var available_update_deployer holds the singleton instance */
1837     protected static $singletoninstance;
1838     /** @var moodle_url URL of a page that includes the deployer UI */
1839     protected $callerurl;
1840     /** @var moodle_url URL to return after the deployment */
1841     protected $returnurl;
1843     /**
1844      * Direct instantiation not allowed, use the factory method {@link self::instance()}
1845      */
1846     protected function __construct() {
1847     }
1849     /**
1850      * Sorry, this is singleton
1851      */
1852     protected function __clone() {
1853     }
1855     /**
1856      * Factory method for this class
1857      *
1858      * @return available_update_deployer the singleton instance
1859      */
1860     public static function instance() {
1861         if (is_null(self::$singletoninstance)) {
1862             self::$singletoninstance = new self();
1863         }
1864         return self::$singletoninstance;
1865     }
1867     /**
1868      * Reset caches used by this script
1869      *
1870      * @param bool $phpunitreset is this called as a part of PHPUnit reset?
1871      */
1872     public static function reset_caches($phpunitreset = false) {
1873         if ($phpunitreset) {
1874             self::$singletoninstance = null;
1875         }
1876     }
1878     /**
1879      * Is automatic deployment enabled?
1880      *
1881      * @return bool
1882      */
1883     public function enabled() {
1884         global $CFG;
1886         if (!empty($CFG->disableupdateautodeploy)) {
1887             // The feature is prohibited via config.php
1888             return false;
1889         }
1891         return get_config('updateautodeploy');
1892     }
1894     /**
1895      * Sets some base properties of the class to make it usable.
1896      *
1897      * @param moodle_url $callerurl the base URL of a script that will handle the class'es form data
1898      * @param moodle_url $returnurl the final URL to return to when the deployment is finished
1899      */
1900     public function initialize(moodle_url $callerurl, moodle_url $returnurl) {
1902         if (!$this->enabled()) {
1903             throw new coding_exception('Unable to initialize the deployer, the feature is not enabled.');
1904         }
1906         $this->callerurl = $callerurl;
1907         $this->returnurl = $returnurl;
1908     }
1910     /**
1911      * Has the deployer been initialized?
1912      *
1913      * Initialized deployer means that the following properties were set:
1914      * callerurl, returnurl
1915      *
1916      * @return bool
1917      */
1918     public function initialized() {
1920         if (!$this->enabled()) {
1921             return false;
1922         }
1924         if (empty($this->callerurl)) {
1925             return false;
1926         }
1928         if (empty($this->returnurl)) {
1929             return false;
1930         }
1932         return true;
1933     }
1935     /**
1936      * Returns a list of reasons why the deployment can not happen
1937      *
1938      * If the returned array is empty, the deployment seems to be possible. The returned
1939      * structure is an associative array with keys representing individual impediments.
1940      * Possible keys are: missingdownloadurl, missingdownloadmd5, notwritable.
1941      *
1942      * @param available_update_info $info
1943      * @return array
1944      */
1945     public function deployment_impediments(available_update_info $info) {
1947         $impediments = array();
1949         if (empty($info->download)) {
1950             $impediments['missingdownloadurl'] = true;
1951         }
1953         if (empty($info->downloadmd5)) {
1954             $impediments['missingdownloadmd5'] = true;
1955         }
1957         if (!empty($info->download) and !$this->update_downloadable($info->download)) {
1958             $impediments['notdownloadable'] = true;
1959         }
1961         if (!$this->component_writable($info->component)) {
1962             $impediments['notwritable'] = true;
1963         }
1965         return $impediments;
1966     }
1968     /**
1969      * Check to see if the current version of the plugin seems to be a checkout of an external repository.
1970      *
1971      * @see plugin_manager::plugin_external_source()
1972      * @param available_update_info $info
1973      * @return false|string
1974      */
1975     public function plugin_external_source(available_update_info $info) {
1977         $paths = core_component::get_plugin_types();
1978         list($plugintype, $pluginname) = core_component::normalize_component($info->component);
1979         $pluginroot = $paths[$plugintype].'/'.$pluginname;
1981         if (is_dir($pluginroot.'/.git')) {
1982             return 'git';
1983         }
1985         if (is_dir($pluginroot.'/CVS')) {
1986             return 'cvs';
1987         }
1989         if (is_dir($pluginroot.'/.svn')) {
1990             return 'svn';
1991         }
1993         return false;
1994     }
1996     /**
1997      * Prepares a renderable widget to confirm installation of an available update.
1998      *
1999      * @param available_update_info $info component version to deploy
2000      * @return renderable
2001      */
2002     public function make_confirm_widget(available_update_info $info) {
2004         if (!$this->initialized()) {
2005             throw new coding_exception('Illegal method call - deployer not initialized.');
2006         }
2008         $params = $this->data_to_params(array(
2009             'updateinfo' => (array)$info,   // see http://www.php.net/manual/en/language.types.array.php#language.types.array.casting
2010         ));
2012         $widget = new single_button(
2013             new moodle_url($this->callerurl, $params),
2014             get_string('updateavailableinstall', 'core_admin'),
2015             'post'
2016         );
2018         return $widget;
2019     }
2021     /**
2022      * Prepares a renderable widget to execute installation of an available update.
2023      *
2024      * @param available_update_info $info component version to deploy
2025      * @param moodle_url $returnurl URL to return after the installation execution
2026      * @return renderable
2027      */
2028     public function make_execution_widget(available_update_info $info, moodle_url $returnurl = null) {
2029         global $CFG;
2031         if (!$this->initialized()) {
2032             throw new coding_exception('Illegal method call - deployer not initialized.');
2033         }
2035         $pluginrootpaths = core_component::get_plugin_types();
2037         list($plugintype, $pluginname) = core_component::normalize_component($info->component);
2039         if (empty($pluginrootpaths[$plugintype])) {
2040             throw new coding_exception('Unknown plugin type root location', $plugintype);
2041         }
2043         list($passfile, $password) = $this->prepare_authorization();
2045         if (is_null($returnurl)) {
2046             $returnurl = new moodle_url('/admin');
2047         } else {
2048             $returnurl = $returnurl;
2049         }
2051         $params = array(
2052             'upgrade' => true,
2053             'type' => $plugintype,
2054             'name' => $pluginname,
2055             'typeroot' => $pluginrootpaths[$plugintype],
2056             'package' => $info->download,
2057             'md5' => $info->downloadmd5,
2058             'dataroot' => $CFG->dataroot,
2059             'dirroot' => $CFG->dirroot,
2060             'passfile' => $passfile,
2061             'password' => $password,
2062             'returnurl' => $returnurl->out(false),
2063         );
2065         if (!empty($CFG->proxyhost)) {
2066             // MDL-36973 - Beware - we should call just !is_proxybypass() here. But currently, our
2067             // cURL wrapper class does not do it. So, to have consistent behaviour, we pass proxy
2068             // setting regardless the $CFG->proxybypass setting. Once the {@link curl} class is
2069             // fixed, the condition should be amended.
2070             if (true or !is_proxybypass($info->download)) {
2071                 if (empty($CFG->proxyport)) {
2072                     $params['proxy'] = $CFG->proxyhost;
2073                 } else {
2074                     $params['proxy'] = $CFG->proxyhost.':'.$CFG->proxyport;
2075                 }
2077                 if (!empty($CFG->proxyuser) and !empty($CFG->proxypassword)) {
2078                     $params['proxyuserpwd'] = $CFG->proxyuser.':'.$CFG->proxypassword;
2079                 }
2081                 if (!empty($CFG->proxytype)) {
2082                     $params['proxytype'] = $CFG->proxytype;
2083                 }
2084             }
2085         }
2087         $widget = new single_button(
2088             new moodle_url('/mdeploy.php', $params),
2089             get_string('updateavailableinstall', 'core_admin'),
2090             'post'
2091         );
2093         return $widget;
2094     }
2096     /**
2097      * Returns array of data objects passed to this tool.
2098      *
2099      * @return array
2100      */
2101     public function submitted_data() {
2103         $data = $this->params_to_data($_POST);
2105         if (empty($data) or empty($data[self::HTTP_PARAM_CHECKER])) {
2106             return false;
2107         }
2109         if (!empty($data['updateinfo']) and is_object($data['updateinfo'])) {
2110             $updateinfo = $data['updateinfo'];
2111             if (!empty($updateinfo->component) and !empty($updateinfo->version)) {
2112                 $data['updateinfo'] = new available_update_info($updateinfo->component, (array)$updateinfo);
2113             }
2114         }
2116         if (!empty($data['callerurl'])) {
2117             $data['callerurl'] = new moodle_url($data['callerurl']);
2118         }
2120         if (!empty($data['returnurl'])) {
2121             $data['returnurl'] = new moodle_url($data['returnurl']);
2122         }
2124         return $data;
2125     }
2127     /**
2128      * Handles magic getters and setters for protected properties.
2129      *
2130      * @param string $name method name, e.g. set_returnurl()
2131      * @param array $arguments arguments to be passed to the array
2132      */
2133     public function __call($name, array $arguments = array()) {
2135         if (substr($name, 0, 4) === 'set_') {
2136             $property = substr($name, 4);
2137             if (empty($property)) {
2138                 throw new coding_exception('Invalid property name (empty)');
2139             }
2140             if (empty($arguments)) {
2141                 $arguments = array(true); // Default value for flag-like properties.
2142             }
2143             // Make sure it is a protected property.
2144             $isprotected = false;
2145             $reflection = new ReflectionObject($this);
2146             foreach ($reflection->getProperties(ReflectionProperty::IS_PROTECTED) as $reflectionproperty) {
2147                 if ($reflectionproperty->getName() === $property) {
2148                     $isprotected = true;
2149                     break;
2150                 }
2151             }
2152             if (!$isprotected) {
2153                 throw new coding_exception('Unable to set property - it does not exist or it is not protected');
2154             }
2155             $value = reset($arguments);
2156             $this->$property = $value;
2157             return;
2158         }
2160         if (substr($name, 0, 4) === 'get_') {
2161             $property = substr($name, 4);
2162             if (empty($property)) {
2163                 throw new coding_exception('Invalid property name (empty)');
2164             }
2165             if (!empty($arguments)) {
2166                 throw new coding_exception('No parameter expected');
2167             }
2168             // Make sure it is a protected property.
2169             $isprotected = false;
2170             $reflection = new ReflectionObject($this);
2171             foreach ($reflection->getProperties(ReflectionProperty::IS_PROTECTED) as $reflectionproperty) {
2172                 if ($reflectionproperty->getName() === $property) {
2173                     $isprotected = true;
2174                     break;
2175                 }
2176             }
2177             if (!$isprotected) {
2178                 throw new coding_exception('Unable to get property - it does not exist or it is not protected');
2179             }
2180             return $this->$property;
2181         }
2182     }
2184     /**
2185      * Generates a random token and stores it in a file in moodledata directory.
2186      *
2187      * @return array of the (string)filename and (string)password in this order
2188      */
2189     public function prepare_authorization() {
2190         global $CFG;
2192         make_upload_directory('mdeploy/auth/');
2194         $attempts = 0;
2195         $success = false;
2197         while (!$success and $attempts < 5) {
2198             $attempts++;
2200             $passfile = $this->generate_passfile();
2201             $password = $this->generate_password();
2202             $now = time();
2204             $filepath = $CFG->dataroot.'/mdeploy/auth/'.$passfile;
2206             if (!file_exists($filepath)) {
2207                 $success = file_put_contents($filepath, $password . PHP_EOL . $now . PHP_EOL, LOCK_EX);
2208                 chmod($filepath, $CFG->filepermissions);
2209             }
2210         }
2212         if ($success) {
2213             return array($passfile, $password);
2215         } else {
2216             throw new moodle_exception('unable_prepare_authorization', 'core_plugin');
2217         }
2218     }
2220     // End of external API
2222     /**
2223      * Prepares an array of HTTP parameters that can be passed to another page.
2224      *
2225      * @param array|object $data associative array or an object holding the data, data JSON-able
2226      * @return array suitable as a param for moodle_url
2227      */
2228     protected function data_to_params($data) {
2230         // Append some our own data
2231         if (!empty($this->callerurl)) {
2232             $data['callerurl'] = $this->callerurl->out(false);
2233         }
2234         if (!empty($this->returnurl)) {
2235             $data['returnurl'] = $this->returnurl->out(false);
2236         }
2238         // Finally append the count of items in the package.
2239         $data[self::HTTP_PARAM_CHECKER] = count($data);
2241         // Generate params
2242         $params = array();
2243         foreach ($data as $name => $value) {
2244             $transname = self::HTTP_PARAM_PREFIX.$name;
2245             $transvalue = json_encode($value);
2246             $params[$transname] = $transvalue;
2247         }
2249         return $params;
2250     }
2252     /**
2253      * Converts HTTP parameters passed to the script into native PHP data
2254      *
2255      * @param array $params such as $_REQUEST or $_POST
2256      * @return array data passed for this class
2257      */
2258     protected function params_to_data(array $params) {
2260         if (empty($params)) {
2261             return array();
2262         }
2264         $data = array();
2265         foreach ($params as $name => $value) {
2266             if (strpos($name, self::HTTP_PARAM_PREFIX) === 0) {
2267                 $realname = substr($name, strlen(self::HTTP_PARAM_PREFIX));
2268                 $realvalue = json_decode($value);
2269                 $data[$realname] = $realvalue;
2270             }
2271         }
2273         return $data;
2274     }
2276     /**
2277      * Returns a random string to be used as a filename of the password storage.
2278      *
2279      * @return string
2280      */
2281     protected function generate_passfile() {
2282         return clean_param(uniqid('mdeploy_', true), PARAM_FILE);
2283     }
2285     /**
2286      * Returns a random string to be used as the authorization token
2287      *
2288      * @return string
2289      */
2290     protected function generate_password() {
2291         return complex_random_string();
2292     }
2294     /**
2295      * Checks if the given component's directory is writable
2296      *
2297      * For the purpose of the deployment, the web server process has to have
2298      * write access to all files in the component's directory (recursively) and for the
2299      * directory itself.
2300      *
2301      * @see worker::move_directory_source_precheck()
2302      * @param string $component normalized component name
2303      * @return boolean
2304      */
2305     protected function component_writable($component) {
2307         list($plugintype, $pluginname) = core_component::normalize_component($component);
2309         $directory = core_component::get_plugin_directory($plugintype, $pluginname);
2311         if (is_null($directory)) {
2312             throw new coding_exception('Unknown component location', $component);
2313         }
2315         return $this->directory_writable($directory);
2316     }
2318     /**
2319      * Checks if the mdeploy.php will be able to fetch the ZIP from the given URL
2320      *
2321      * This is mainly supposed to check if the transmission over HTTPS would
2322      * work. That is, if the CA certificates are present at the server.
2323      *
2324      * @param string $downloadurl the URL of the ZIP package to download
2325      * @return bool
2326      */
2327     protected function update_downloadable($downloadurl) {
2328         global $CFG;
2330         $curloptions = array(
2331             'CURLOPT_SSL_VERIFYHOST' => 2,      // this is the default in {@link curl} class but just in case
2332             'CURLOPT_SSL_VERIFYPEER' => true,
2333         );
2335         $curl = new curl(array('proxy' => true));
2336         $result = $curl->head($downloadurl, $curloptions);
2337         $errno = $curl->get_errno();
2338         if (empty($errno)) {
2339             return true;
2340         } else {
2341             return false;
2342         }
2343     }
2345     /**
2346      * Checks if the directory and all its contents (recursively) is writable
2347      *
2348      * @param string $path full path to a directory
2349      * @return boolean
2350      */
2351     private function directory_writable($path) {
2353         if (!is_writable($path)) {
2354             return false;
2355         }
2357         if (is_dir($path)) {
2358             $handle = opendir($path);
2359         } else {
2360             return false;
2361         }
2363         $result = true;
2365         while ($filename = readdir($handle)) {
2366             $filepath = $path.'/'.$filename;
2368             if ($filename === '.' or $filename === '..') {
2369                 continue;
2370             }
2372             if (is_dir($filepath)) {
2373                 $result = $result && $this->directory_writable($filepath);
2375             } else {
2376                 $result = $result && is_writable($filepath);
2377             }
2378         }
2380         closedir($handle);
2382         return $result;
2383     }
2387 /**
2388  * Factory class producing required subclasses of {@link plugininfo_base}
2389  */
2390 class plugininfo_default_factory {
2392     /**
2393      * Makes a new instance of the plugininfo class
2394      *
2395      * @param string $type the plugin type, eg. 'mod'
2396      * @param string $typerootdir full path to the location of all the plugins of this type
2397      * @param string $name the plugin name, eg. 'workshop'
2398      * @param string $namerootdir full path to the location of the plugin
2399      * @param string $typeclass the name of class that holds the info about the plugin
2400      * @return plugininfo_base the instance of $typeclass
2401      */
2402     public static function make($type, $typerootdir, $name, $namerootdir, $typeclass) {
2403         $plugin              = new $typeclass();
2404         $plugin->type        = $type;
2405         $plugin->typerootdir = $typerootdir;
2406         $plugin->name        = $name;
2407         $plugin->rootdir     = $namerootdir;
2409         $plugin->init_display_name();
2410         $plugin->load_disk_version();
2411         $plugin->load_db_version();
2412         $plugin->load_required_main_version();
2413         $plugin->init_is_standard();
2415         return $plugin;
2416     }
2420 /**
2421  * Base class providing access to the information about a plugin
2422  *
2423  * @property-read string component the component name, type_name
2424  */
2425 abstract class plugininfo_base {
2427     /** @var string the plugintype name, eg. mod, auth or workshopform */
2428     public $type;
2429     /** @var string full path to the location of all the plugins of this type */
2430     public $typerootdir;
2431     /** @var string the plugin name, eg. assignment, ldap */
2432     public $name;
2433     /** @var string the localized plugin name */
2434     public $displayname;
2435     /** @var string the plugin source, one of plugin_manager::PLUGIN_SOURCE_xxx constants */
2436     public $source;
2437     /** @var fullpath to the location of this plugin */
2438     public $rootdir;
2439     /** @var int|string the version of the plugin's source code */
2440     public $versiondisk;
2441     /** @var int|string the version of the installed plugin */
2442     public $versiondb;
2443     /** @var int|float|string required version of Moodle core  */
2444     public $versionrequires;
2445     /** @var array other plugins that this one depends on, lazy-loaded by {@link get_other_required_plugins()} */
2446     public $dependencies;
2447     /** @var int number of instances of the plugin - not supported yet */
2448     public $instances;
2449     /** @var int order of the plugin among other plugins of the same type - not supported yet */
2450     public $sortorder;
2451     /** @var array|null array of {@link available_update_info} for this plugin */
2452     public $availableupdates;
2454     /**
2455      * Gathers and returns the information about all plugins of the given type
2456      *
2457      * @param string $type the name of the plugintype, eg. mod, auth or workshopform
2458      * @param string $typerootdir full path to the location of the plugin dir
2459      * @param string $typeclass the name of the actually called class
2460      * @return array of plugintype classes, indexed by the plugin name
2461      */
2462     public static function get_plugins($type, $typerootdir, $typeclass) {
2464         // get the information about plugins at the disk
2465         $plugins = core_component::get_plugin_list($type);
2466         $ondisk = array();
2467         foreach ($plugins as $pluginname => $pluginrootdir) {
2468             $ondisk[$pluginname] = plugininfo_default_factory::make($type, $typerootdir,
2469                 $pluginname, $pluginrootdir, $typeclass);
2470         }
2471         return $ondisk;
2472     }
2474     /**
2475      * Sets {@link $displayname} property to a localized name of the plugin
2476      */
2477     public function init_display_name() {
2478         if (!get_string_manager()->string_exists('pluginname', $this->component)) {
2479             $this->displayname = '[pluginname,' . $this->component . ']';
2480         } else {
2481             $this->displayname = get_string('pluginname', $this->component);
2482         }
2483     }
2485     /**
2486      * Magic method getter, redirects to read only values.
2487      *
2488      * @param string $name
2489      * @return mixed
2490      */
2491     public function __get($name) {
2492         switch ($name) {
2493             case 'component': return $this->type . '_' . $this->name;
2495             default:
2496                 debugging('Invalid plugin property accessed! '.$name);
2497                 return null;
2498         }
2499     }
2501     /**
2502      * Return the full path name of a file within the plugin.
2503      *
2504      * No check is made to see if the file exists.
2505      *
2506      * @param string $relativepath e.g. 'version.php'.
2507      * @return string e.g. $CFG->dirroot . '/mod/quiz/version.php'.
2508      */
2509     public function full_path($relativepath) {
2510         if (empty($this->rootdir)) {
2511             return '';
2512         }
2513         return $this->rootdir . '/' . $relativepath;
2514     }
2516     /**
2517      * Load the data from version.php.
2518      *
2519      * @param bool $disablecache do not attempt to obtain data from the cache
2520      * @return stdClass the object called $plugin defined in version.php
2521      */
2522     protected function load_version_php($disablecache=false) {
2524         $cache = cache::make('core', 'plugininfo_base');
2526         $versionsphp = $cache->get('versions_php');
2528         if (!$disablecache and $versionsphp !== false and isset($versionsphp[$this->component])) {
2529             return $versionsphp[$this->component];
2530         }
2532         $versionfile = $this->full_path('version.php');
2534         $plugin = new stdClass();
2535         if (is_readable($versionfile)) {
2536             include($versionfile);
2537         }
2538         $versionsphp[$this->component] = $plugin;
2539         $cache->set('versions_php', $versionsphp);
2541         return $plugin;
2542     }
2544     /**
2545      * Sets {@link $versiondisk} property to a numerical value representing the
2546      * version of the plugin's source code.
2547      *
2548      * If the value is null after calling this method, either the plugin
2549      * does not use versioning (typically does not have any database
2550      * data) or is missing from disk.
2551      */
2552     public function load_disk_version() {
2553         $plugin = $this->load_version_php();
2554         if (isset($plugin->version)) {
2555             $this->versiondisk = $plugin->version;
2556         }
2557     }
2559     /**
2560      * Sets {@link $versionrequires} property to a numerical value representing
2561      * the version of Moodle core that this plugin requires.
2562      */
2563     public function load_required_main_version() {
2564         $plugin = $this->load_version_php();
2565         if (isset($plugin->requires)) {
2566             $this->versionrequires = $plugin->requires;
2567         }
2568     }
2570     /**
2571      * Initialise {@link $dependencies} to the list of other plugins (in any)
2572      * that this one requires to be installed.
2573      */
2574     protected function load_other_required_plugins() {
2575         $plugin = $this->load_version_php();
2576         if (!empty($plugin->dependencies)) {
2577             $this->dependencies = $plugin->dependencies;
2578         } else {
2579             $this->dependencies = array(); // By default, no dependencies.
2580         }
2581     }
2583     /**
2584      * Get the list of other plugins that this plugin requires to be installed.
2585      *
2586      * @return array with keys the frankenstyle plugin name, and values either
2587      *      a version string (like '2011101700') or the constant ANY_VERSION.
2588      */
2589     public function get_other_required_plugins() {
2590         if (is_null($this->dependencies)) {
2591             $this->load_other_required_plugins();
2592         }
2593         return $this->dependencies;
2594     }
2596     /**
2597      * Is this is a subplugin?
2598      *
2599      * @return boolean
2600      */
2601     public function is_subplugin() {
2602         return ($this->get_parent_plugin() !== false);
2603     }
2605     /**
2606      * If I am a subplugin, return the name of my parent plugin.
2607      *
2608      * @return string|bool false if not a subplugin, name of the parent otherwise
2609      */
2610     public function get_parent_plugin() {
2611         return $this->get_plugin_manager()->get_parent_of_subplugin($this->type);
2612     }
2614     /**
2615      * Sets {@link $versiondb} property to a numerical value representing the
2616      * currently installed version of the plugin.
2617      *
2618      * If the value is null after calling this method, either the plugin
2619      * does not use versioning (typically does not have any database
2620      * data) or has not been installed yet.
2621      */
2622     public function load_db_version() {
2623         if ($ver = self::get_version_from_config_plugins($this->component)) {
2624             $this->versiondb = $ver;
2625         }
2626     }
2628     /**
2629      * Sets {@link $source} property to one of plugin_manager::PLUGIN_SOURCE_xxx
2630      * constants.
2631      *
2632      * If the property's value is null after calling this method, then
2633      * the type of the plugin has not been recognized and you should throw
2634      * an exception.
2635      */
2636     public function init_is_standard() {
2638         $standard = plugin_manager::standard_plugins_list($this->type);
2640         if ($standard !== false) {
2641             $standard = array_flip($standard);
2642             if (isset($standard[$this->name])) {
2643                 $this->source = plugin_manager::PLUGIN_SOURCE_STANDARD;
2644             } else if (!is_null($this->versiondb) and is_null($this->versiondisk)
2645                     and plugin_manager::is_deleted_standard_plugin($this->type, $this->name)) {
2646                 $this->source = plugin_manager::PLUGIN_SOURCE_STANDARD; // to be deleted
2647             } else {
2648                 $this->source = plugin_manager::PLUGIN_SOURCE_EXTENSION;
2649             }
2650         }
2651     }
2653     /**
2654      * Returns true if the plugin is shipped with the official distribution
2655      * of the current Moodle version, false otherwise.
2656      *
2657      * @return bool
2658      */
2659     public function is_standard() {
2660         return $this->source === plugin_manager::PLUGIN_SOURCE_STANDARD;
2661     }
2663     /**
2664      * Returns true if the the given Moodle version is enough to run this plugin
2665      *
2666      * @param string|int|double $moodleversion
2667      * @return bool
2668      */
2669     public function is_core_dependency_satisfied($moodleversion) {
2671         if (empty($this->versionrequires)) {
2672             return true;
2674         } else {
2675             return (double)$this->versionrequires <= (double)$moodleversion;
2676         }
2677     }
2679     /**
2680      * Returns the status of the plugin
2681      *
2682      * @return string one of plugin_manager::PLUGIN_STATUS_xxx constants
2683      */
2684     public function get_status() {
2686         if (is_null($this->versiondb) and is_null($this->versiondisk)) {
2687             return plugin_manager::PLUGIN_STATUS_NODB;
2689         } else if (is_null($this->versiondb) and !is_null($this->versiondisk)) {
2690             return plugin_manager::PLUGIN_STATUS_NEW;
2692         } else if (!is_null($this->versiondb) and is_null($this->versiondisk)) {
2693             if (plugin_manager::is_deleted_standard_plugin($this->type, $this->name)) {
2694                 return plugin_manager::PLUGIN_STATUS_DELETE;
2695             } else {
2696                 return plugin_manager::PLUGIN_STATUS_MISSING;
2697             }
2699         } else if ((string)$this->versiondb === (string)$this->versiondisk) {
2700             return plugin_manager::PLUGIN_STATUS_UPTODATE;
2702         } else if ($this->versiondb < $this->versiondisk) {
2703             return plugin_manager::PLUGIN_STATUS_UPGRADE;
2705         } else if ($this->versiondb > $this->versiondisk) {
2706             return plugin_manager::PLUGIN_STATUS_DOWNGRADE;
2708         } else {
2709             // $version = pi(); and similar funny jokes - hopefully Donald E. Knuth will never contribute to Moodle ;-)
2710             throw new coding_exception('Unable to determine plugin state, check the plugin versions');
2711         }
2712     }
2714     /**
2715      * Returns the information about plugin availability
2716      *
2717      * True means that the plugin is enabled. False means that the plugin is
2718      * disabled. Null means that the information is not available, or the
2719      * plugin does not support configurable availability or the availability
2720      * can not be changed.
2721      *
2722      * @return null|bool
2723      */
2724     public function is_enabled() {
2725         return null;
2726     }
2728     /**
2729      * Populates the property {@link $availableupdates} with the information provided by
2730      * available update checker
2731      *
2732      * @param available_update_checker $provider the class providing the available update info
2733      */
2734     public function check_available_updates(available_update_checker $provider) {
2735         global $CFG;
2737         if (isset($CFG->updateminmaturity)) {
2738             $minmaturity = $CFG->updateminmaturity;
2739         } else {
2740             // this can happen during the very first upgrade to 2.3
2741             $minmaturity = MATURITY_STABLE;
2742         }
2744         $this->availableupdates = $provider->get_update_info($this->component,
2745             array('minmaturity' => $minmaturity));
2746     }
2748     /**
2749      * If there are updates for this plugin available, returns them.
2750      *
2751      * Returns array of {@link available_update_info} objects, if some update
2752      * is available. Returns null if there is no update available or if the update
2753      * availability is unknown.
2754      *
2755      * @return array|null
2756      */
2757     public function available_updates() {
2759         if (empty($this->availableupdates) or !is_array($this->availableupdates)) {
2760             return null;
2761         }
2763         $updates = array();
2765         foreach ($this->availableupdates as $availableupdate) {
2766             if ($availableupdate->version > $this->versiondisk) {
2767                 $updates[] = $availableupdate;
2768             }
2769         }
2771         if (empty($updates)) {
2772             return null;
2773         }
2775         return $updates;
2776     }
2778     /**
2779      * Returns the node name used in admin settings menu for this plugin settings (if applicable)
2780      *
2781      * @return null|string node name or null if plugin does not create settings node (default)
2782      */
2783     public function get_settings_section_name() {
2784         return null;
2785     }
2787     /**
2788      * Returns the URL of the plugin settings screen
2789      *
2790      * Null value means that the plugin either does not have the settings screen
2791      * or its location is not available via this library.
2792      *
2793      * @return null|moodle_url
2794      */
2795     public function get_settings_url() {
2796         $section = $this->get_settings_section_name();
2797         if ($section === null) {
2798             return null;
2799         }
2800         $settings = admin_get_root()->locate($section);
2801         if ($settings && $settings instanceof admin_settingpage) {
2802             return new moodle_url('/admin/settings.php', array('section' => $section));
2803         } else if ($settings && $settings instanceof admin_externalpage) {
2804             return new moodle_url($settings->url);
2805         } else {
2806             return null;
2807         }
2808     }
2810     /**
2811      * Loads plugin settings to the settings tree
2812      *
2813      * This function usually includes settings.php file in plugins folder.
2814      * Alternatively it can create a link to some settings page (instance of admin_externalpage)
2815      *
2816      * @param part_of_admin_tree $adminroot
2817      * @param string $parentnodename
2818      * @param bool $hassiteconfig whether the current user has moodle/site:config capability
2819      */
2820     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
2821     }
2823     /**
2824      * Should there be a way to uninstall the plugin via the administration UI
2825      *
2826      * By default, uninstallation is allowed for all non-standard add-ons. Subclasses
2827      * may want to override this to allow uninstallation of all plugins (simply by
2828      * returning true unconditionally). Subplugins follow their parent plugin's
2829      * decision by default.
2830      *
2831      * Note that even if true is returned, the core may still prohibit the uninstallation,
2832      * e.g. in case there are other plugins that depend on this one.
2833      *
2834      * @return boolean
2835      */
2836     public function is_uninstall_allowed() {
2838         if ($this->is_subplugin()) {
2839             return $this->get_plugin_manager()->get_plugin_info($this->get_parent_plugin())->is_uninstall_allowed();
2840         }
2842         if ($this->is_standard()) {
2843             return false;
2844         }
2846         return true;
2847     }
2849     /**
2850      * Optional extra warning before uninstallation, for example number of uses in courses.
2851      *
2852      * @return string
2853      */
2854     public function get_uninstall_extra_warning() {
2855         return '';
2856     }
2858     /**
2859      * Returns the URL of the screen where this plugin can be uninstalled
2860      *
2861      * Visiting that URL must be safe, that is a manual confirmation is needed
2862      * for actual uninstallation of the plugin. By default, URL to a common
2863      * uninstalling tool is returned.
2864      *
2865      * @return moodle_url
2866      */
2867     public function get_uninstall_url() {
2868         return $this->get_default_uninstall_url();
2869     }
2871     /**
2872      * Returns relative directory of the plugin with heading '/'
2873      *
2874      * @return string
2875      */
2876     public function get_dir() {
2877         global $CFG;
2879         return substr($this->rootdir, strlen($CFG->dirroot));
2880     }
2882     /**
2883      * Hook method to implement certain steps when uninstalling the plugin.
2884      *
2885      * This hook is called by {@link plugin_manager::uninstall_plugin()} so
2886      * it is basically usable only for those plugin types that use the default
2887      * uninstall tool provided by {@link self::get_default_uninstall_url()}.
2888      *
2889      * @param progress_trace $progress traces the process
2890      * @return bool true on success, false on failure
2891      */
2892     public function uninstall(progress_trace $progress) {
2893         return true;
2894     }
2896     /**
2897      * Returns URL to a script that handles common plugin uninstall procedure.
2898      *
2899      * This URL is suitable for plugins that do not have their own UI
2900      * for uninstalling.
2901      *
2902      * @return moodle_url
2903      */
2904     protected final function get_default_uninstall_url() {
2905         return new moodle_url('/admin/plugins.php', array(
2906             'sesskey' => sesskey(),
2907             'uninstall' => $this->component,
2908             'confirm' => 0,
2909         ));
2910     }
2912     /**
2913      * Provides access to plugin versions from the {config_plugins} table
2914      *
2915      * @param string $plugin plugin name
2916      * @param bool $disablecache do not attempt to obtain data from the cache
2917      * @return int|bool the stored value or false if not found
2918      */
2919     protected function get_version_from_config_plugins($plugin, $disablecache=false) {
2920         global $DB;
2922         $cache = cache::make('core', 'plugininfo_base');
2924         $pluginversions = $cache->get('versions_db');
2926         if ($pluginversions === false or $disablecache) {
2927             try {
2928                 $pluginversions = $DB->get_records_menu('config_plugins', array('name' => 'version'), 'plugin', 'plugin,value');
2929             } catch (dml_exception $e) {
2930                 // before install
2931                 $pluginversions = array();
2932             }
2933             $cache->set('versions_db', $pluginversions);
2934         }
2936         if (isset($pluginversions[$plugin])) {
2937             return $pluginversions[$plugin];
2938         } else {
2939             return false;
2940         }
2941     }
2943     /**
2944      * Provides access to the plugin_manager singleton.
2945      *
2946      * @return plugin_manmager
2947      */
2948     protected function get_plugin_manager() {
2949         return plugin_manager::instance();
2950     }
2954 /**
2955  * General class for all plugin types that do not have their own class
2956  */
2957 class plugininfo_general extends plugininfo_base {
2961 /**
2962  * Class for page side blocks
2963  */
2964 class plugininfo_block extends plugininfo_base {
2966     public static function get_plugins($type, $typerootdir, $typeclass) {
2968         // get the information about blocks at the disk
2969         $blocks = parent::get_plugins($type, $typerootdir, $typeclass);
2971         // add blocks missing from disk
2972         $blocksinfo = self::get_blocks_info();
2973         foreach ($blocksinfo as $blockname => $blockinfo) {
2974             if (isset($blocks[$blockname])) {
2975                 continue;
2976             }
2977             $plugin                 = new $typeclass();
2978             $plugin->type           = $type;
2979             $plugin->typerootdir    = $typerootdir;
2980             $plugin->name           = $blockname;
2981             $plugin->rootdir        = null;
2982             $plugin->displayname    = $blockname;
2983             $plugin->versiondb      = $blockinfo->version;
2984             $plugin->init_is_standard();
2986             $blocks[$blockname]   = $plugin;
2987         }
2989         return $blocks;
2990     }
2992     /**
2993      * Magic method getter, redirects to read only values.
2994      *
2995      * For block plugins pretends the object has 'visible' property for compatibility
2996      * with plugins developed for Moodle version below 2.4
2997      *
2998      * @param string $name
2999      * @return mixed
3000      */
3001     public function __get($name) {
3002         if ($name === 'visible') {
3003             debugging('This is now an instance of plugininfo_block, please use $block->is_enabled() instead of $block->visible', DEBUG_DEVELOPER);
3004             return ($this->is_enabled() !== false);
3005         }
3006         return parent::__get($name);
3007     }
3009     public function init_display_name() {
3011         if (get_string_manager()->string_exists('pluginname', 'block_' . $this->name)) {
3012             $this->displayname = get_string('pluginname', 'block_' . $this->name);
3014         } else if (($block = block_instance($this->name)) !== false) {
3015             $this->displayname = $block->get_title();
3017         } else {
3018             parent::init_display_name();
3019         }
3020     }
3022     public function load_db_version() {
3023         global $DB;
3025         $blocksinfo = self::get_blocks_info();
3026         if (isset($blocksinfo[$this->name]->version)) {
3027             $this->versiondb = $blocksinfo[$this->name]->version;
3028         }
3029     }
3031     public function is_enabled() {
3033         $blocksinfo = self::get_blocks_info();
3034         if (isset($blocksinfo[$this->name]->visible)) {
3035             if ($blocksinfo[$this->name]->visible) {
3036                 return true;
3037             } else {
3038                 return false;
3039             }
3040         } else {
3041             return parent::is_enabled();
3042         }
3043     }
3045     public function get_settings_section_name() {
3046         return 'blocksetting' . $this->name;
3047     }
3049     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3050         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3051         $ADMIN = $adminroot; // may be used in settings.php
3052         $block = $this; // also can be used inside settings.php
3053         $section = $this->get_settings_section_name();
3055         if (!$hassiteconfig || (($blockinstance = block_instance($this->name)) === false)) {
3056             return;
3057         }
3059         $settings = null;
3060         if ($blockinstance->has_config()) {
3061             if (file_exists($this->full_path('settings.php'))) {
3062                 $settings = new admin_settingpage($section, $this->displayname,
3063                         'moodle/site:config', $this->is_enabled() === false);
3064                 include($this->full_path('settings.php')); // this may also set $settings to null
3065             } else {
3066                 $blocksinfo = self::get_blocks_info();
3067                 $settingsurl = new moodle_url('/admin/block.php', array('block' => $blocksinfo[$this->name]->id));
3068                 $settings = new admin_externalpage($section, $this->displayname,
3069                         $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
3070             }
3071         }
3072         if ($settings) {
3073             $ADMIN->add($parentnodename, $settings);
3074         }
3075     }
3077     public function is_uninstall_allowed() {
3078         return true;
3079     }
3081     /**
3082      * Warnign with number of block instances.
3083      *
3084      * @return string
3085      */
3086     public function get_uninstall_extra_warning() {
3087         global $DB;
3089         if (!$count = $DB->count_records('block_instances', array('blockname'=>$this->name))) {
3090             return '';
3091         }
3093         return '<p>'.get_string('uninstallextraconfirmblock', 'core_plugin', array('instances'=>$count)).'</p>';
3094     }
3096     /**
3097      * Provides access to the records in {block} table
3098      *
3099      * @param bool $disablecache do not attempt to obtain data from the cache
3100      * @return array array of stdClasses
3101      */
3102     protected static function get_blocks_info($disablecache=false) {
3103         global $DB;
3105         $cache = cache::make('core', 'plugininfo_block');
3107         $blocktypes = $cache->get('blocktypes');
3109         if ($blocktypes === false or $disablecache) {
3110             try {
3111                 $blocktypes = $DB->get_records('block', null, 'name', 'name,id,version,visible');
3112             } catch (dml_exception $e) {
3113                 // before install
3114                 $blocktypes = array();
3115             }
3116             $cache->set('blocktypes', $blocktypes);
3117         }
3119         return $blocktypes;
3120     }
3124 /**
3125  * Class for text filters
3126  */
3127 class plugininfo_filter extends plugininfo_base {
3129     public static function get_plugins($type, $typerootdir, $typeclass) {
3130         global $CFG, $DB;
3132         $filters = array();
3134         // get the list of filters in /filter location
3135         $installed = filter_get_all_installed();
3137         foreach ($installed as $name => $displayname) {
3138             $plugin                 = new $typeclass();
3139             $plugin->type           = $type;
3140             $plugin->typerootdir    = $typerootdir;
3141             $plugin->name           = $name;
3142             $plugin->rootdir        = "$CFG->dirroot/filter/$name";
3143             $plugin->displayname    = $displayname;
3145             $plugin->load_disk_version();
3146             $plugin->load_db_version();
3147             $plugin->load_required_main_version();
3148             $plugin->init_is_standard();
3150             $filters[$plugin->name] = $plugin;
3151         }
3153         // Do not mess with filter registration here!
3155         $globalstates = self::get_global_states();
3157         // make sure that all registered filters are installed, just in case
3158         foreach ($globalstates as $name => $info) {
3159             if (!isset($filters[$name])) {
3160                 // oops, there is a record in filter_active but the filter is not installed
3161                 $plugin                 = new $typeclass();
3162                 $plugin->type           = $type;
3163                 $plugin->typerootdir    = $typerootdir;
3164                 $plugin->name           = $name;
3165                 $plugin->rootdir        = "$CFG->dirroot/filter/$name";
3166                 $plugin->displayname    = $name;
3168                 $plugin->load_db_version();
3170                 if (is_null($plugin->versiondb)) {
3171                     // this is a hack to stimulate 'Missing from disk' error
3172                     // because $plugin->versiondisk will be null !== false
3173                     $plugin->versiondb = false;
3174                 }
3176                 $filters[$plugin->name] = $plugin;
3177             }
3178         }
3180         return $filters;
3181     }
3183     public function init_display_name() {
3184         // do nothing, the name is set in self::get_plugins()
3185     }
3187     public function is_enabled() {
3189         $globalstates = self::get_global_states();
3191         foreach ($globalstates as $name => $info) {
3192             if ($name === $this->name) {
3193                 if ($info->active == TEXTFILTER_DISABLED) {
3194                     return false;
3195                 } else {
3196                     // it may be 'On' or 'Off, but available'
3197                     return null;
3198                 }
3199             }
3200         }
3202         return null;
3203     }
3205     public function get_settings_section_name() {
3206         return 'filtersetting' . $this->name;
3207     }
3209     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3210         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3211         $ADMIN = $adminroot; // may be used in settings.php
3212         $filter = $this; // also can be used inside settings.php
3214         $settings = null;
3215         if ($hassiteconfig && file_exists($this->full_path('filtersettings.php'))) {
3216             $section = $this->get_settings_section_name();
3217             $settings = new admin_settingpage($section, $this->displayname,
3218                     'moodle/site:config', $this->is_enabled() === false);
3219             include($this->full_path('filtersettings.php')); // this may also set $settings to null
3220         }
3221         if ($settings) {
3222             $ADMIN->add($parentnodename, $settings);
3223         }
3224     }
3226     public function is_uninstall_allowed() {
3227         return true;
3228     }
3230     public function get_uninstall_url() {
3231         return new moodle_url('/admin/filters.php', array('sesskey' => sesskey(), 'filterpath' => $this->name, 'action' => 'delete'));
3232     }
3234     /**
3235      * Provides access to the results of {@link filter_get_global_states()}
3236      * but indexed by the normalized filter name
3237      *
3238      * The legacy filter name is available as ->legacyname property.
3239      *
3240      * @param bool $disablecache do not attempt to obtain data from the cache
3241      * @return array
3242      */
3243     protected static function get_global_states($disablecache=false) {
3244         global $DB;
3246         $cache = cache::make('core', 'plugininfo_filter');
3248         $globalstates = $cache->get('globalstates');
3250         if ($globalstates === false or $disablecache) {
3252             if (!$DB->get_manager()->table_exists('filter_active')) {
3253                 // Not installed yet.
3254                 $cache->set('globalstates', array());
3255                 return array();
3256             }
3258             $globalstates = array();
3260             foreach (filter_get_global_states() as $name => $info) {
3261                 if (strpos($name, '/') !== false) {
3262                     // Skip existing before upgrade to new names.
3263                     continue;
3264                 }
3266                 $filterinfo = new stdClass();
3267                 $filterinfo->active = $info->active;
3268                 $filterinfo->sortorder = $info->sortorder;
3269                 $globalstates[$name] = $filterinfo;
3270             }
3272             $cache->set('globalstates', $globalstates);
3273         }
3275         return $globalstates;
3276     }
3280 /**
3281  * Class for activity modules
3282  */
3283 class plugininfo_mod extends plugininfo_base {
3285     public static function get_plugins($type, $typerootdir, $typeclass) {
3287         // get the information about plugins at the disk
3288         $modules = parent::get_plugins($type, $typerootdir, $typeclass);
3290         // add modules missing from disk
3291         $modulesinfo = self::get_modules_info();
3292         foreach ($modulesinfo as $modulename => $moduleinfo) {
3293             if (isset($modules[$modulename])) {
3294                 continue;
3295             }
3296             $plugin                 = new $typeclass();
3297             $plugin->type           = $type;
3298             $plugin->typerootdir    = $typerootdir;
3299             $plugin->name           = $modulename;
3300             $plugin->rootdir        = null;
3301             $plugin->displayname    = $modulename;
3302             $plugin->versiondb      = $moduleinfo->version;
3303             $plugin->init_is_standard();
3305             $modules[$modulename]   = $plugin;
3306         }
3308         return $modules;
3309     }
3311     /**
3312      * Magic method getter, redirects to read only values.
3313      *
3314      * For module plugins we pretend the object has 'visible' property for compatibility
3315      * with plugins developed for Moodle version below 2.4
3316      *
3317      * @param string $name
3318      * @return mixed
3319      */
3320     public function __get($name) {
3321         if ($name === 'visible') {
3322             debugging('This is now an instance of plugininfo_mod, please use $module->is_enabled() instead of $module->visible', DEBUG_DEVELOPER);
3323             return ($this->is_enabled() !== false);
3324         }
3325         return parent::__get($name);
3326     }
3328     public function init_display_name() {
3329         if (get_string_manager()->string_exists('pluginname', $this->component)) {
3330             $this->displayname = get_string('pluginname', $this->component);
3331         } else {
3332             $this->displayname = get_string('modulename', $this->component);
3333         }
3334     }
3336     /**
3337      * Load the data from version.php.
3338      *
3339      * @param bool $disablecache do not attempt to obtain data from the cache
3340      * @return object the data object defined in version.php.
3341      */
3342     protected function load_version_php($disablecache=false) {
3344         $cache = cache::make('core', 'plugininfo_base');
3346         $versionsphp = $cache->get('versions_php');
3348         if (!$disablecache and $versionsphp !== false and isset($versionsphp[$this->component])) {
3349             return $versionsphp[$this->component];
3350         }
3352         $versionfile = $this->full_path('version.php');
3354         $module = new stdClass();
3355         $plugin = new stdClass();
3356         if (is_readable($versionfile)) {
3357             include($versionfile);
3358         }
3359         if (!isset($module->version) and isset($plugin->version)) {
3360             $module = $plugin;
3361         }
3362         $versionsphp[$this->component] = $module;
3363         $cache->set('versions_php', $versionsphp);
3365         return $module;
3366     }
3368     public function load_db_version() {
3369         global $DB;
3371         $modulesinfo = self::get_modules_info();
3372         if (isset($modulesinfo[$this->name]->version)) {
3373             $this->versiondb = $modulesinfo[$this->name]->version;
3374         }
3375     }
3377     public function is_enabled() {
3379         $modulesinfo = self::get_modules_info();
3380         if (isset($modulesinfo[$this->name]->visible)) {
3381             if ($modulesinfo[$this->name]->visible) {
3382                 return true;
3383             } else {
3384                 return false;
3385             }
3386         } else {
3387             return parent::is_enabled();
3388         }
3389     }
3391     public function get_settings_section_name() {
3392         return 'modsetting' . $this->name;
3393     }
3395     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3396         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3397         $ADMIN = $adminroot; // may be used in settings.php
3398         $module = $this; // also can be used inside settings.php
3399         $section = $this->get_settings_section_name();
3401         $modulesinfo = self::get_modules_info();
3402         $settings = null;
3403         if ($hassiteconfig && isset($modulesinfo[$this->name]) && file_exists($this->full_path('settings.php'))) {
3404             $settings = new admin_settingpage($section, $this->displayname,
3405                     'moodle/site:config', $this->is_enabled() === false);
3406             include($this->full_path('settings.php')); // this may also set $settings to null
3407         }
3408         if ($settings) {
3409             $ADMIN->add($parentnodename, $settings);
3410         }
3411     }
3413     /**
3414      * Allow all activity modules but Forum to be uninstalled.
3416      * This exception for the Forum has been hard-coded in Moodle since ages,
3417      * we may want to re-think it one day.
3418      */
3419     public function is_uninstall_allowed() {
3420         if ($this->name === 'forum') {
3421             return false;
3422         } else {
3423             return true;
3424         }
3425     }
3427     /**
3428      * Return warning with number of activities and number of affected courses.
3429      *
3430      * @return string
3431      */
3432     public function get_uninstall_extra_warning() {
3433         global $DB;
3435         if (!$module = $DB->get_record('modules', array('name'=>$this->name))) {
3436             return '';
3437         }
3439         if (!$count = $DB->count_records('course_modules', array('module'=>$module->id))) {
3440             return '';
3441         }
3443         $sql = "SELECT COUNT('x')
3444                   FROM (
3445                     SELECT course
3446                       FROM {course_modules}
3447                      WHERE module = :mid
3448                   GROUP BY course
3449                   ) c";
3450         $courses = $DB->count_records_sql($sql, array('mid'=>$module->id));
3452         return '<p>'.get_string('uninstallextraconfirmmod', 'core_plugin', array('instances'=>$count, 'courses'=>$courses)).'</p>';
3453     }
3455     /**
3456      * Provides access to the records in {modules} table
3457      *
3458      * @param bool $disablecache do not attempt to obtain data from the cache
3459      * @return array array of stdClasses
3460      */
3461     protected static function get_modules_info($disablecache=false) {
3462         global $DB;
3464         $cache = cache::make('core', 'plugininfo_mod');
3466         $modulesinfo = $cache->get('modulesinfo');
3468         if ($modulesinfo === false or $disablecache) {
3469             try {
3470                 $modulesinfo = $DB->get_records('modules', null, 'name', 'name,id,version,visible');
3471             } catch (dml_exception $e) {
3472                 // before install
3473                 $modulesinfo = array();
3474             }
3475             $cache->set('modulesinfo', $modulesinfo);
3476         }
3478         return $modulesinfo;
3479     }
3483 /**
3484  * Class for question behaviours.
3485  */
3486 class plugininfo_qbehaviour extends plugininfo_base {
3488     public function is_uninstall_allowed() {
3489         return true;
3490     }
3492     public function get_uninstall_url() {
3493         return new moodle_url('/admin/qbehaviours.php',
3494                 array('delete' => $this->name, 'sesskey' => sesskey()));
3495     }
3499 /**
3500  * Class for question types
3501  */
3502 class plugininfo_qtype extends plugininfo_base {
3504     public function is_uninstall_allowed() {
3505         return true;
3506     }
3508     public function get_uninstall_url() {
3509         return new moodle_url('/admin/qtypes.php',
3510                 array('delete' => $this->name, 'sesskey' => sesskey()));
3511     }
3513     public function get_settings_section_name() {
3514         return 'qtypesetting' . $this->name;
3515     }
3517     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3518         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3519         $ADMIN = $adminroot; // may be used in settings.php
3520         $qtype = $this; // also can be used inside settings.php
3521         $section = $this->get_settings_section_name();
3523         $settings = null;
3524         $systemcontext = context_system::instance();
3525         if (($hassiteconfig || has_capability('moodle/question:config', $systemcontext)) &&
3526                 file_exists($this->full_path('settings.php'))) {
3527             $settings = new admin_settingpage($section, $this->displayname,
3528                     'moodle/question:config', $this->is_enabled() === false);
3529             include($this->full_path('settings.php')); // this may also set $settings to null
3530         }
3531         if ($settings) {
3532             $ADMIN->add($parentnodename, $settings);
3533         }
3534     }
3538 /**
3539  * Class for authentication plugins
3540  */
3541 class plugininfo_auth extends plugininfo_base {
3543     public function is_enabled() {
3544         global $CFG;
3546         if (in_array($this->name, array('nologin', 'manual'))) {
3547             // these two are always enabled and can't be disabled
3548             return null;
3549         }
3551         $enabled = array_flip(explode(',', $CFG->auth));
3553         return isset($enabled[$this->name]);
3554     }
3556     public function get_settings_section_name() {
3557         return 'authsetting' . $this->name;
3558     }
3560     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3561         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3562         $ADMIN = $adminroot; // may be used in settings.php
3563         $auth = $this; // also to be used inside settings.php
3564         $section = $this->get_settings_section_name();
3566         $settings = null;
3567         if ($hassiteconfig) {
3568             if (file_exists($this->full_path('settings.php'))) {
3569                 // TODO: finish implementation of common settings - locking, etc.
3570                 $settings = new admin_settingpage($section, $this->displayname,
3571                         'moodle/site:config', $this->is_enabled() === false);
3572                 include($this->full_path('settings.php')); // this may also set $settings to null
3573             } else {
3574                 $settingsurl = new moodle_url('/admin/auth_config.php', array('auth' => $this->name));
3575                 $settings = new admin_externalpage($section, $this->displayname,
3576                         $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
3577             }
3578         }
3579         if ($settings) {
3580             $ADMIN->add($parentnodename, $settings);
3581         }
3582     }
3586 /**
3587  * Class for enrolment plugins
3588  */
3589 class plugininfo_enrol extends plugininfo_base {
3591     public function is_enabled() {
3592         global $CFG;
3594         // We do not actually need whole enrolment classes here so we do not call
3595         // {@link enrol_get_plugins()}. Note that this may produce slightly different
3596         // results, for example if the enrolment plugin does not contain lib.php
3597         // but it is listed in $CFG->enrol_plugins_enabled
3599         $enabled = array_flip(explode(',', $CFG->enrol_plugins_enabled));
3601         return isset($enabled[$this->name]);
3602     }
3604     public function get_settings_section_name() {
3605         if (file_exists($this->full_path('settings.php'))) {
3606             return 'enrolsettings' . $this->name;
3607         } else {
3608             return null;
3609         }
3610     }
3612     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3613         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3615         if (!$hassiteconfig or !file_exists($this->full_path('settings.php'))) {
3616             return;
3617         }
3618         $section = $this->get_settings_section_name();
3620         $ADMIN = $adminroot; // may be used in settings.php
3621         $enrol = $this; // also can be used inside settings.php
3622         $settings = new admin_settingpage($section, $this->displayname,
3623                 'moodle/site:config', $this->is_enabled() === false);
3625         include($this->full_path('settings.php')); // This may also set $settings to null!
3627         if ($settings) {
3628             $ADMIN->add($parentnodename, $settings);
3629         }
3630     }
3632     public function is_uninstall_allowed() {
3633         if ($this->name === 'manual') {
3634             return false;
3635         }
3636         return true;
3637     }
3639     /**
3640      * Return warning with number of activities and number of affected courses.
3641      *
3642      * @return string
3643      */
3644     public function get_uninstall_extra_warning() {
3645         global $DB, $OUTPUT;
3647         $sql = "SELECT COUNT('x')
3648                   FROM {user_enrolments} ue
3649                   JOIN {enrol} e ON e.id = ue.enrolid
3650                  WHERE e.enrol = :plugin";
3651         $count = $DB->count_records_sql($sql, array('plugin'=>$this->name));
3653         if (!$count) {
3654             return '';
3655         }
3657         $migrateurl = new moodle_url('/admin/enrol.php', array('action'=>'migrate', 'enrol'=>$this->name, 'sesskey'=>sesskey()));
3658         $migrate = new single_button($migrateurl, get_string('migratetomanual', 'core_enrol'));
3659         $button = $OUTPUT->render($migrate);
3661         $result = '<p>'.get_string('uninstallextraconfirmenrol', 'core_plugin', array('enrolments'=>$count)).'</p>';
3662         $result .= $button;
3664         return $result;
3665     }
3669 /**
3670  * Class for messaging processors
3671  */
3672 class plugininfo_message extends plugininfo_base {
3674     public function get_settings_section_name() {
3675         return 'messagesetting' . $this->name;
3676     }
3678     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3679         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3680         $ADMIN = $adminroot; // may be used in settings.php
3681         if (!$hassiteconfig) {
3682             return;
3683         }
3684         $section = $this->get_settings_section_name();
3686         $settings = null;
3687         $processors = get_message_processors();
3688         if (isset($processors[$this->name])) {
3689             $processor = $processors[$this->name];
3690             if ($processor->available && $processor->hassettings) {
3691                 $settings = new admin_settingpage($section, $this->displayname,
3692                         'moodle/site:config', $this->is_enabled() === false);
3693                 include($this->full_path('settings.php')); // this may also set $settings to null
3694             }
3695         }
3696         if ($settings) {
3697             $ADMIN->add($parentnodename, $settings);
3698         }
3699     }
3701     /**
3702      * @see plugintype_interface::is_enabled()
3703      */
3704     public function is_enabled() {
3705         $processors = get_message_processors();
3706         if (isset($processors[$this->name])) {
3707             return $processors[$this->name]->configured && $processors[$this->name]->enabled;
3708         } else {
3709             return parent::is_enabled();
3710         }
3711     }
3713     public function is_uninstall_allowed() {
3714         $processors = get_message_processors();
3715         if (isset($processors[$this->name])) {
3716             return true;
3717         } else {
3718             return false;
3719         }
3720     }
3722     /**
3723      * @see plugintype_interface::get_uninstall_url()
3724      */
3725     public function get_uninstall_url() {
3726         $processors = get_message_processors();
3727         return new moodle_url('/admin/message.php', array('uninstall' => $processors[$this->name]->id, 'sesskey' => sesskey()));
3728     }
3732 /**
3733  * Class for repositories
3734  */
3735 class plugininfo_repository extends plugininfo_base {
3737     public function is_enabled() {
3739         $enabled = self::get_enabled_repositories();
3741         return isset($enabled[$this->name]);
3742     }
3744     public function get_settings_section_name() {
3745         return 'repositorysettings'.$this->name;
3746     }
3748     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3749         if ($hassiteconfig && $this->is_enabled()) {
3750             // completely no access to repository setting when it is not enabled
3751             $sectionname = $this->get_settings_section_name();
3752             $settingsurl = new moodle_url('/admin/repository.php',
3753                     array('sesskey' => sesskey(), 'action' => 'edit', 'repos' => $this->name));
3754             $settings = new admin_externalpage($sectionname, $this->displayname,
3755                     $settingsurl, 'moodle/site:config', false);
3756             $adminroot->add($parentnodename, $settings);
3757         }
3758     }
3760     /**
3761      * Provides access to the records in {repository} table
3762      *
3763      * @param bool $disablecache do not attempt to obtain data from the cache
3764      * @return array array of stdClasses
3765      */
3766     protected static function get_enabled_repositories($disablecache=false) {
3767         global $DB;
3769         $cache = cache::make('core', 'plugininfo_repository');
3771         $enabled = $cache->get('enabled');
3773         if ($enabled === false or $disablecache) {
3774             $enabled = $DB->get_records('repository', null, 'type', 'type,visible,sortorder');
3775             $cache->set('enabled', $enabled);
3776         }
3778         return $enabled;
3779     }
3783 /**
3784  * Class for portfolios
3785  */
3786 class plugininfo_portfolio extends plugininfo_base {
3788     public function is_enabled() {
3790         $enabled = self::get_enabled_portfolios();
3792         return isset($enabled[$this->name]);
3793     }
3795     /**
3796      * Returns list of enabled portfolio plugins
3797      *
3798      * Portfolio plugin is enabled if there is at least one record in the {portfolio_instance}
3799      * table for it.
3800      *
3801      * @param bool $disablecache do not attempt to obtain data from the cache
3802      * @return array array of stdClasses with properties plugin and visible indexed by plugin
3803      */
3804     protected static function get_enabled_portfolios($disablecache=false) {
3805         global $DB;
3807         $cache = cache::make('core', 'plugininfo_portfolio');
3809         $enabled = $cache->get('enabled');
3811         if ($enabled === false or $disablecache) {
3812             $enabled = array();
3813             $instances = $DB->get_recordset('portfolio_instance', null, '', 'plugin,visible');
3814             foreach ($instances as $instance) {
3815                 if (isset($enabled[$instance->plugin])) {
3816                     if ($instance->visible) {
3817                         $enabled[$instance->plugin]->visible = $instance->visible;
3818                     }
3819                 } else {
3820                     $enabled[$instance->plugin] = $instance;
3821                 }
3822             }
3823             $instances->close();
3824             $cache->set('enabled', $enabled);
3825         }
3827         return $enabled;
3828     }
3832 /**
3833  * Class for themes
3834  */
3835 class plugininfo_theme extends plugininfo_base {
3837     public function is_enabled() {
3838         global $CFG;
3840         if ((!empty($CFG->theme) and $CFG->theme === $this->name) or
3841             (!empty($CFG->themelegacy) and $CFG->themelegacy === $this->name)) {
3842             return true;
3843         } else {
3844             return parent::is_enabled();
3845         }
3846     }
3850 /**
3851  * Class representing an MNet service
3852  */
3853 class plugininfo_mnetservice extends plugininfo_base {
3855     public function is_enabled() {
3856         global $CFG;
3858         if (empty($CFG->mnet_dispatcher_mode) || $CFG->mnet_dispatcher_mode !== 'strict') {
3859             return false;
3860         } else {
3861             return parent::is_enabled();
3862         }
3863     }
3867 /**
3868  * Class for admin tool plugins
3869  */
3870 class plugininfo_tool extends plugininfo_base {
3872     public function is_uninstall_allowed() {
3873         return true;
3874     }
3878 /**
3879  * Class for admin tool plugins
3880  */
3881 class plugininfo_report extends plugininfo_base {
3883     public function is_uninstall_allowed() {
3884         return true;
3885     }
3889 /**
3890  * Class for local plugins
3891  */
3892 class plugininfo_local extends plugininfo_base {
3894     public function is_uninstall_allowed() {
3895         return true;
3896     }
3899 /**
3900  * Class for HTML editors
3901  */
3902 class plugininfo_editor extends plugininfo_base {
3904     public function get_settings_section_name() {
3905         return 'editorsettings' . $this->name;
3906     }
3908     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3909         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3910         $ADMIN = $adminroot; // may be used in settings.php
3911         $editor = $this; // also can be used inside settings.php
3912         $section = $this->get_settings_section_name();
3914         $settings = null;
3915         if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3916             $settings = new admin_settingpage($section, $this->displayname,
3917                     'moodle/site:config', $this->is_enabled() === false);
3918             include($this->full_path('settings.php')); // this may also set $settings to null
3919         }
3920         if ($settings) {
3921             $ADMIN->add($parentnodename, $settings);
3922         }
3923     }
3925     /**
3926      * Basic textarea editor can not be uninstalled.
3927      */
3928     public function is_uninstall_allowed() {
3929         if ($this->name === 'textarea') {
3930             return false;
3931         } else {
3932             return true;
3933         }
3934     }
3936     /**
3937      * Returns the information about plugin availability
3938      *
3939      * True means that the plugin is enabled. False means that the plugin is
3940      * disabled. Null means that the information is not available, or the
3941      * plugin does not support configurable availability or the availability
3942      * can not be changed.
3943      *
3944      * @return null|bool
3945      */
3946     public function is_enabled() {
3947         global $CFG;
3948         if (empty($CFG->texteditors)) {
3949             $CFG->texteditors = 'tinymce,textarea';
3950         }
3951         if (in_array($this->name, explode(',', $CFG->texteditors))) {
3952             return true;
3953         }
3954         return false;
3955     }
3958 /**
3959  * Class for plagiarism plugins
3960  */
3961 class plugininfo_plagiarism extends plugininfo_base {
3963     public function get_settings_section_name() {
3964         return 'plagiarism'. $this->name;
3965     }
3967     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3968         // plagiarism plugin just redirect to settings.php in the plugins directory
3969         if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3970             $section = $this->get_settings_section_name();
3971             $settingsurl = new moodle_url($this->get_dir().'/settings.php');
3972             $settings = new admin_externalpage($section, $this->displayname,
3973                     $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
3974             $adminroot->add($parentnodename, $settings);
3975         }
3976     }
3978     public function is_uninstall_allowed() {
3979         return true;
3980     }
3983 /**
3984  * Class for webservice protocols
3985  */
3986 class plugininfo_webservice extends plugininfo_base {
3988     public function get_settings_section_name() {
3989         return 'webservicesetting' . $this->name;
3990     }
3992     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3993         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3994         $ADMIN = $adminroot; // may be used in settings.php
3995         $webservice = $this; // also can be used inside settings.php
3996         $section = $this->get_settings_section_name();
3998         $settings = null;
3999         if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
4000             $settings = new admin_settingpage($section, $this->displayname,
4001                     'moodle/site:config', $this->is_enabled() === false);
4002             include($this->full_path('settings.php')); // this may also set $settings to null
4003         }
4004         if ($settings) {
4005             $ADMIN->add($parentnodename, $settings);
4006         }
4007     }
4009     public function is_enabled() {
4010         global $CFG;
4011         if (empty($CFG->enablewebservices)) {
4012             return false;
4013         }
4014         $active_webservices = empty($CFG->webserviceprotocols) ? array() : explode(',', $CFG->webserviceprotocols);
4015         if (in_array($this->name, $active_webservices)) {
4016             return true;
4017         }
4018         return false;
4019     }
4021     public function is_uninstall_allowed() {
4022         return false;
4023     }
4026 /**
4027  * Class for course formats
4028  */
4029 class plugininfo_format extends plugininfo_base {
4031     /**
4032      * Gathers and returns the information about all plugins of the given type
4033      *
4034      * @param string $type the name of the plugintype, eg. mod, auth or workshopform
4035      * @param string $typerootdir full path to the location of the plugin dir
4036      * @param string $typeclass the name of the actually called class
4037      * @return array of plugintype classes, indexed by the plugin name
4038      */
4039     public static function get_plugins($type, $typerootdir, $typeclass) {
4040         global $CFG;
4041         $formats = parent::get_plugins($type, $typerootdir, $typeclass);
4042         require_once($CFG->dirroot.'/course/lib.php');
4043         $order = get_sorted_course_formats();
4044         $sortedformats = array();
4045         foreach ($order as $formatname) {
4046             $sortedformats[$formatname] = $formats[$formatname];
4047         }
4048         return $sortedformats;
4049     }
4051     public function get_settings_section_name() {
4052         return 'formatsetting' . $this->name;
4053     }
4055     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
4056         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
4057         $ADMIN = $adminroot; // also may be used in settings.php
4058         $section = $this->get_settings_section_name();
4060         $settings = null;
4061         if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
4062             $settings = new admin_settingpage($section, $this->displayname,
4063                     'moodle/site:config', $this->is_enabled() === false);
4064             include($this->full_path('settings.php')); // this may also set $settings to null
4065         }
4066         if ($settings) {
4067             $ADMIN->add($parentnodename, $settings);
4068         }
4069     }
4071     public function is_enabled() {
4072         return !get_config($this->component, 'disabled');
4073     }
4075     public function is_uninstall_allowed() {
4076         if ($this->name !== get_config('moodlecourse', 'format') && $this->name !== 'site') {
4077             return true;
4078         } else {
4079             return false;
4080         }
4081     }
4083     public function get_uninstall_extra_warning() {
4084         global $DB;
4086         $coursecount = $DB->count_records('course', array('format' => $this->name));
4088         if (!$coursecount) {
4089             return '';
4090         }
4092         $defaultformat = $this->get_plugin_manager()->plugin_name('format_'.get_config('moodlecourse', 'format'));
4093         $message = get_string(
4094             'formatuninstallwithcourses', 'core_admin',
4095             (object)array('count' => $coursecount, 'format' => $this->displayname,
4096             'defaultformat' => $defaultformat));
4098         return $message;
4099     }