Merge branch 'MDL-28466-master' of git://github.com/danpoltawski/moodle
[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 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(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', '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', 'boxnet', 'coursefiles', 'dropbox', 'equella', 'filesystem',
814                 'flickr', 'flickr_public', 'googledocs', 'local', 'merlot',
815                 'picasa', 'recent', 's3', 'upload', 'url', 'user', 'webdav',
816                 'wikimedia', 'youtube'
817             ),
819             'scormreport' => array(
820                 'basic',
821                 'interactions',
822                 'graphs'
823             ),
825             'tinymce' => array(
826                 'ctrlhelp', 'dragmath', 'moodleemoticon', 'moodleimage', 'moodlemedia', 'moodlenolink', 'spellchecker',
827                 'pdw', 'wrap'
828             ),
830             'theme' => array(
831                 'afterburner', 'anomaly', 'arialist', 'base', 'binarius', 'bootstrapbase',
832                 'boxxie', 'brick', 'canvas', 'clean', 'formal_white', 'formfactor',
833                 'fusion', 'leatherbound', 'magazine', 'mymobile', 'nimble',
834                 'nonzero', 'overlay', 'serenity', 'sky_high', 'splash',
835                 'standard', 'standardold'
836             ),
838             'tool' => array(
839                 'assignmentupgrade', 'behat', 'capability', 'customlang',
840                 'dbtransfer', 'generator', 'health', 'innodb', 'installaddon',
841                 'langimport', 'multilangupgrade', 'phpunit', 'profiling',
842                 'qeupgradehelper', 'replace', 'spamcleaner', 'timezoneimport',
843                 'unittest', 'uploaduser', 'unsuproles', 'xmldb'
844             ),
846             'webservice' => array(
847                 'amf', 'rest', 'soap', 'xmlrpc'
848             ),
850             'workshopallocation' => array(
851                 'manual', 'random', 'scheduled'
852             ),
854             'workshopeval' => array(
855                 'best'
856             ),
858             'workshopform' => array(
859                 'accumulative', 'comments', 'numerrors', 'rubric'
860             )
861         );
863         if (isset($standard_plugins[$type])) {
864             return $standard_plugins[$type];
865         } else {
866             return false;
867         }
868     }
870     /**
871      * Wrapper for the core function {@link normalize_component()}.
872      *
873      * This is here just to make it possible to mock it in unit tests.
874      *
875      * @param string $component
876      * @return array
877      */
878     protected function normalize_component($component) {
879         return normalize_component($component);
880     }
882     /**
883      * Reorders plugin types into a sequence to be displayed
884      *
885      * For technical reasons, plugin types returned by {@link get_plugin_types()} are
886      * in a certain order that does not need to fit the expected order for the display.
887      * Particularly, activity modules should be displayed first as they represent the
888      * real heart of Moodle. They should be followed by other plugin types that are
889      * used to build the courses (as that is what one expects from LMS). After that,
890      * other supportive plugin types follow.
891      *
892      * @param array $types associative array
893      * @return array same array with altered order of items
894      */
895     protected function reorder_plugin_types(array $types) {
896         $fix = array(
897             'mod'        => $types['mod'],
898             'block'      => $types['block'],
899             'qtype'      => $types['qtype'],
900             'qbehaviour' => $types['qbehaviour'],
901             'qformat'    => $types['qformat'],
902             'filter'     => $types['filter'],
903             'enrol'      => $types['enrol'],
904         );
905         foreach ($types as $type => $path) {
906             if (!isset($fix[$type])) {
907                 $fix[$type] = $path;
908             }
909         }
910         return $fix;
911     }
913     /**
914      * Check if the given directory can be removed by the web server process.
915      *
916      * This recursively checks that the given directory and all its contents
917      * it writable.
918      *
919      * @param string $fullpath
920      * @return boolean
921      */
922     protected function is_directory_removable($fullpath) {
924         if (!is_writable($fullpath)) {
925             return false;
926         }
928         if (is_dir($fullpath)) {
929             $handle = opendir($fullpath);
930         } else {
931             return false;
932         }
934         $result = true;
936         while ($filename = readdir($handle)) {
938             if ($filename === '.' or $filename === '..') {
939                 continue;
940             }
942             $subfilepath = $fullpath.'/'.$filename;
944             if (is_dir($subfilepath)) {
945                 $result = $result && $this->is_directory_removable($subfilepath);
947             } else {
948                 $result = $result && is_writable($subfilepath);
949             }
950         }
952         closedir($handle);
954         return $result;
955     }
957     /**
958      * Helper method that implements common uninstall prerequisities
959      *
960      * @param plugininfo_base $pluginfo
961      * @return bool
962      */
963     protected function common_uninstall_check(plugininfo_base $pluginfo) {
965         if (!$pluginfo->is_uninstall_allowed()) {
966             // The plugin's plugininfo class declares it should not be uninstalled.
967             return false;
968         }
970         if ($pluginfo->get_status() === plugin_manager::PLUGIN_STATUS_NEW) {
971             // The plugin is not installed. It should be either installed or removed from the disk.
972             // Relying on this temporary state may be tricky.
973             return false;
974         }
976         if (is_null($pluginfo->get_uninstall_url())) {
977             // Backwards compatibility.
978             debugging('plugininfo_base subclasses should use is_uninstall_allowed() instead of returning null in get_uninstall_url()',
979                 DEBUG_DEVELOPER);
980             return false;
981         }
983         return true;
984     }
988 /**
989  * General exception thrown by the {@link available_update_checker} class
990  */
991 class available_update_checker_exception extends moodle_exception {
993     /**
994      * @param string $errorcode exception description identifier
995      * @param mixed $debuginfo debugging data to display
996      */
997     public function __construct($errorcode, $debuginfo=null) {
998         parent::__construct($errorcode, 'core_plugin', '', null, print_r($debuginfo, true));
999     }
1003 /**
1004  * Singleton class that handles checking for available updates
1005  */
1006 class available_update_checker {
1008     /** @var available_update_checker holds the singleton instance */
1009     protected static $singletoninstance;
1010     /** @var null|int the timestamp of when the most recent response was fetched */
1011     protected $recentfetch = null;
1012     /** @var null|array the recent response from the update notification provider */
1013     protected $recentresponse = null;
1014     /** @var null|string the numerical version of the local Moodle code */
1015     protected $currentversion = null;
1016     /** @var null|string the release info of the local Moodle code */
1017     protected $currentrelease = null;
1018     /** @var null|string branch of the local Moodle code */
1019     protected $currentbranch = null;
1020     /** @var array of (string)frankestyle => (string)version list of additional plugins deployed at this site */
1021     protected $currentplugins = array();
1023     /**
1024      * Direct initiation not allowed, use the factory method {@link self::instance()}
1025      */
1026     protected function __construct() {
1027     }
1029     /**
1030      * Sorry, this is singleton
1031      */
1032     protected function __clone() {
1033     }
1035     /**
1036      * Factory method for this class
1037      *
1038      * @return available_update_checker the singleton instance
1039      */
1040     public static function instance() {
1041         if (is_null(self::$singletoninstance)) {
1042             self::$singletoninstance = new self();
1043         }
1044         return self::$singletoninstance;
1045     }
1047     /**
1048      * Reset any caches
1049      * @param bool $phpunitreset
1050      */
1051     public static function reset_caches($phpunitreset = false) {
1052         if ($phpunitreset) {
1053             self::$singletoninstance = null;
1054         }
1055     }
1057     /**
1058      * Returns the timestamp of the last execution of {@link fetch()}
1059      *
1060      * @return int|null null if it has never been executed or we don't known
1061      */
1062     public function get_last_timefetched() {
1064         $this->restore_response();
1066         if (!empty($this->recentfetch)) {
1067             return $this->recentfetch;
1069         } else {
1070             return null;
1071         }
1072     }
1074     /**
1075      * Fetches the available update status from the remote site
1076      *
1077      * @throws available_update_checker_exception
1078      */
1079     public function fetch() {
1080         $response = $this->get_response();
1081         $this->validate_response($response);
1082         $this->store_response($response);
1083     }
1085     /**
1086      * Returns the available update information for the given component
1087      *
1088      * This method returns null if the most recent response does not contain any information
1089      * about it. The returned structure is an array of available updates for the given
1090      * component. Each update info is an object with at least one property called
1091      * 'version'. Other possible properties are 'release', 'maturity', 'url' and 'downloadurl'.
1092      *
1093      * For the 'core' component, the method returns real updates only (those with higher version).
1094      * For all other components, the list of all known remote updates is returned and the caller
1095      * (usually the {@link plugin_manager}) is supposed to make the actual comparison of versions.
1096      *
1097      * @param string $component frankenstyle
1098      * @param array $options with supported keys 'minmaturity' and/or 'notifybuilds'
1099      * @return null|array null or array of available_update_info objects
1100      */
1101     public function get_update_info($component, array $options = array()) {
1103         if (!isset($options['minmaturity'])) {
1104             $options['minmaturity'] = 0;
1105         }
1107         if (!isset($options['notifybuilds'])) {
1108             $options['notifybuilds'] = false;
1109         }
1111         if ($component == 'core') {
1112             $this->load_current_environment();
1113         }
1115         $this->restore_response();
1117         if (empty($this->recentresponse['updates'][$component])) {
1118             return null;
1119         }
1121         $updates = array();
1122         foreach ($this->recentresponse['updates'][$component] as $info) {
1123             $update = new available_update_info($component, $info);
1124             if (isset($update->maturity) and ($update->maturity < $options['minmaturity'])) {
1125                 continue;
1126             }
1127             if ($component == 'core') {
1128                 if ($update->version <= $this->currentversion) {
1129                     continue;
1130                 }
1131                 if (empty($options['notifybuilds']) and $this->is_same_release($update->release)) {
1132                     continue;
1133                 }
1134             }
1135             $updates[] = $update;
1136         }
1138         if (empty($updates)) {
1139             return null;
1140         }
1142         return $updates;
1143     }
1145     /**
1146      * The method being run via cron.php
1147      */
1148     public function cron() {
1149         global $CFG;
1151         if (!$this->cron_autocheck_enabled()) {
1152             $this->cron_mtrace('Automatic check for available updates not enabled, skipping.');
1153             return;
1154         }
1156         $now = $this->cron_current_timestamp();
1158         if ($this->cron_has_fresh_fetch($now)) {
1159             $this->cron_mtrace('Recently fetched info about available updates is still fresh enough, skipping.');
1160             return;
1161         }
1163         if ($this->cron_has_outdated_fetch($now)) {
1164             $this->cron_mtrace('Outdated or missing info about available updates, forced fetching ... ', '');
1165             $this->cron_execute();
1166             return;
1167         }
1169         $offset = $this->cron_execution_offset();
1170         $start = mktime(1, 0, 0, date('n', $now), date('j', $now), date('Y', $now)); // 01:00 AM today local time
1171         if ($now > $start + $offset) {
1172             $this->cron_mtrace('Regular daily check for available updates ... ', '');
1173             $this->cron_execute();
1174             return;
1175         }
1176     }
1178     /// end of public API //////////////////////////////////////////////////////
1180     /**
1181      * Makes cURL request to get data from the remote site
1182      *
1183      * @return string raw request result
1184      * @throws available_update_checker_exception
1185      */
1186     protected function get_response() {
1187         global $CFG;
1188         require_once($CFG->libdir.'/filelib.php');
1190         $curl = new curl(array('proxy' => true));
1191         $response = $curl->post($this->prepare_request_url(), $this->prepare_request_params(), $this->prepare_request_options());
1192         $curlerrno = $curl->get_errno();
1193         if (!empty($curlerrno)) {
1194             throw new available_update_checker_exception('err_response_curl', 'cURL error '.$curlerrno.': '.$curl->error);
1195         }
1196         $curlinfo = $curl->get_info();
1197         if ($curlinfo['http_code'] != 200) {
1198             throw new available_update_checker_exception('err_response_http_code', $curlinfo['http_code']);
1199         }
1200         return $response;
1201     }
1203     /**
1204      * Makes sure the response is valid, has correct API format etc.
1205      *
1206      * @param string $response raw response as returned by the {@link self::get_response()}
1207      * @throws available_update_checker_exception
1208      */
1209     protected function validate_response($response) {
1211         $response = $this->decode_response($response);
1213         if (empty($response)) {
1214             throw new available_update_checker_exception('err_response_empty');
1215         }
1217         if (empty($response['status']) or $response['status'] !== 'OK') {
1218             throw new available_update_checker_exception('err_response_status', $response['status']);
1219         }
1221         if (empty($response['apiver']) or $response['apiver'] !== '1.2') {
1222             throw new available_update_checker_exception('err_response_format_version', $response['apiver']);
1223         }
1225         if (empty($response['forbranch']) or $response['forbranch'] !== moodle_major_version(true)) {
1226             throw new available_update_checker_exception('err_response_target_version', $response['forbranch']);
1227         }
1228     }
1230     /**
1231      * Decodes the raw string response from the update notifications provider
1232      *
1233      * @param string $response as returned by {@link self::get_response()}
1234      * @return array decoded response structure
1235      */
1236     protected function decode_response($response) {
1237         return json_decode($response, true);
1238     }
1240     /**
1241      * Stores the valid fetched response for later usage
1242      *
1243      * This implementation uses the config_plugins table as the permanent storage.
1244      *
1245      * @param string $response raw valid data returned by {@link self::get_response()}
1246      */
1247     protected function store_response($response) {
1249         set_config('recentfetch', time(), 'core_plugin');
1250         set_config('recentresponse', $response, 'core_plugin');
1252         $this->restore_response(true);
1253     }
1255     /**
1256      * Loads the most recent raw response record we have fetched
1257      *
1258      * After this method is called, $this->recentresponse is set to an array. If the
1259      * array is empty, then either no data have been fetched yet or the fetched data
1260      * do not have expected format (and thence they are ignored and a debugging
1261      * message is displayed).
1262      *
1263      * This implementation uses the config_plugins table as the permanent storage.
1264      *
1265      * @param bool $forcereload reload even if it was already loaded
1266      */
1267     protected function restore_response($forcereload = false) {
1269         if (!$forcereload and !is_null($this->recentresponse)) {
1270             // we already have it, nothing to do
1271             return;
1272         }
1274         $config = get_config('core_plugin');
1276         if (!empty($config->recentresponse) and !empty($config->recentfetch)) {
1277             try {
1278                 $this->validate_response($config->recentresponse);
1279                 $this->recentfetch = $config->recentfetch;
1280                 $this->recentresponse = $this->decode_response($config->recentresponse);
1281             } catch (available_update_checker_exception $e) {
1282                 // The server response is not valid. Behave as if no data were fetched yet.
1283                 // This may happen when the most recent update info (cached locally) has been
1284                 // fetched with the previous branch of Moodle (like during an upgrade from 2.x
1285                 // to 2.y) or when the API of the response has changed.
1286                 $this->recentresponse = array();
1287             }
1289         } else {
1290             $this->recentresponse = array();
1291         }
1292     }
1294     /**
1295      * Compares two raw {@link $recentresponse} records and returns the list of changed updates
1296      *
1297      * This method is used to populate potential update info to be sent to site admins.
1298      *
1299      * @param array $old
1300      * @param array $new
1301      * @throws available_update_checker_exception
1302      * @return array parts of $new['updates'] that have changed
1303      */
1304     protected function compare_responses(array $old, array $new) {
1306         if (empty($new)) {
1307             return array();
1308         }
1310         if (!array_key_exists('updates', $new)) {
1311             throw new available_update_checker_exception('err_response_format');
1312         }
1314         if (empty($old)) {
1315             return $new['updates'];
1316         }
1318         if (!array_key_exists('updates', $old)) {
1319             throw new available_update_checker_exception('err_response_format');
1320         }
1322         $changes = array();
1324         foreach ($new['updates'] as $newcomponent => $newcomponentupdates) {
1325             if (empty($old['updates'][$newcomponent])) {
1326                 $changes[$newcomponent] = $newcomponentupdates;
1327                 continue;
1328             }
1329             foreach ($newcomponentupdates as $newcomponentupdate) {
1330                 $inold = false;
1331                 foreach ($old['updates'][$newcomponent] as $oldcomponentupdate) {
1332                     if ($newcomponentupdate['version'] == $oldcomponentupdate['version']) {
1333                         $inold = true;
1334                     }
1335                 }
1336                 if (!$inold) {
1337                     if (!isset($changes[$newcomponent])) {
1338                         $changes[$newcomponent] = array();
1339                     }
1340                     $changes[$newcomponent][] = $newcomponentupdate;
1341                 }
1342             }
1343         }
1345         return $changes;
1346     }
1348     /**
1349      * Returns the URL to send update requests to
1350      *
1351      * During the development or testing, you can set $CFG->alternativeupdateproviderurl
1352      * to a custom URL that will be used. Otherwise the standard URL will be returned.
1353      *
1354      * @return string URL
1355      */
1356     protected function prepare_request_url() {
1357         global $CFG;
1359         if (!empty($CFG->config_php_settings['alternativeupdateproviderurl'])) {
1360             return $CFG->config_php_settings['alternativeupdateproviderurl'];
1361         } else {
1362             return 'https://download.moodle.org/api/1.2/updates.php';
1363         }
1364     }
1366     /**
1367      * Sets the properties currentversion, currentrelease, currentbranch and currentplugins
1368      *
1369      * @param bool $forcereload
1370      */
1371     protected function load_current_environment($forcereload=false) {
1372         global $CFG;
1374         if (!is_null($this->currentversion) and !$forcereload) {
1375             // nothing to do
1376             return;
1377         }
1379         $version = null;
1380         $release = null;
1382         require($CFG->dirroot.'/version.php');
1383         $this->currentversion = $version;
1384         $this->currentrelease = $release;
1385         $this->currentbranch = moodle_major_version(true);
1387         $pluginman = plugin_manager::instance();
1388         foreach ($pluginman->get_plugins() as $type => $plugins) {
1389             foreach ($plugins as $plugin) {
1390                 if (!$plugin->is_standard()) {
1391                     $this->currentplugins[$plugin->component] = $plugin->versiondisk;
1392                 }
1393             }
1394         }
1395     }
1397     /**
1398      * Returns the list of HTTP params to be sent to the updates provider URL
1399      *
1400      * @return array of (string)param => (string)value
1401      */
1402     protected function prepare_request_params() {
1403         global $CFG;
1405         $this->load_current_environment();
1406         $this->restore_response();
1408         $params = array();
1409         $params['format'] = 'json';
1411         if (isset($this->recentresponse['ticket'])) {
1412             $params['ticket'] = $this->recentresponse['ticket'];
1413         }
1415         if (isset($this->currentversion)) {
1416             $params['version'] = $this->currentversion;
1417         } else {
1418             throw new coding_exception('Main Moodle version must be already known here');
1419         }
1421         if (isset($this->currentbranch)) {
1422             $params['branch'] = $this->currentbranch;
1423         } else {
1424             throw new coding_exception('Moodle release must be already known here');
1425         }
1427         $plugins = array();
1428         foreach ($this->currentplugins as $plugin => $version) {
1429             $plugins[] = $plugin.'@'.$version;
1430         }
1431         if (!empty($plugins)) {
1432             $params['plugins'] = implode(',', $plugins);
1433         }
1435         return $params;
1436     }
1438     /**
1439      * Returns the list of cURL options to use when fetching available updates data
1440      *
1441      * @return array of (string)param => (string)value
1442      */
1443     protected function prepare_request_options() {
1444         global $CFG;
1446         $options = array(
1447             'CURLOPT_SSL_VERIFYHOST' => 2,      // this is the default in {@link curl} class but just in case
1448             'CURLOPT_SSL_VERIFYPEER' => true,
1449         );
1451         return $options;
1452     }
1454     /**
1455      * Returns the current timestamp
1456      *
1457      * @return int the timestamp
1458      */
1459     protected function cron_current_timestamp() {
1460         return time();
1461     }
1463     /**
1464      * Output cron debugging info
1465      *
1466      * @see mtrace()
1467      * @param string $msg output message
1468      * @param string $eol end of line
1469      */
1470     protected function cron_mtrace($msg, $eol = PHP_EOL) {
1471         mtrace($msg, $eol);
1472     }
1474     /**
1475      * Decide if the autocheck feature is disabled in the server setting
1476      *
1477      * @return bool true if autocheck enabled, false if disabled
1478      */
1479     protected function cron_autocheck_enabled() {
1480         global $CFG;
1482         if (empty($CFG->updateautocheck)) {
1483             return false;
1484         } else {
1485             return true;
1486         }
1487     }
1489     /**
1490      * Decide if the recently fetched data are still fresh enough
1491      *
1492      * @param int $now current timestamp
1493      * @return bool true if no need to re-fetch, false otherwise
1494      */
1495     protected function cron_has_fresh_fetch($now) {
1496         $recent = $this->get_last_timefetched();
1498         if (empty($recent)) {
1499             return false;
1500         }
1502         if ($now < $recent) {
1503             $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1504             return true;
1505         }
1507         if ($now - $recent > 24 * HOURSECS) {
1508             return false;
1509         }
1511         return true;
1512     }
1514     /**
1515      * Decide if the fetch is outadated or even missing
1516      *
1517      * @param int $now current timestamp
1518      * @return bool false if no need to re-fetch, true otherwise
1519      */
1520     protected function cron_has_outdated_fetch($now) {
1521         $recent = $this->get_last_timefetched();
1523         if (empty($recent)) {
1524             return true;
1525         }
1527         if ($now < $recent) {
1528             $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1529             return false;
1530         }
1532         if ($now - $recent > 48 * HOURSECS) {
1533             return true;
1534         }
1536         return false;
1537     }
1539     /**
1540      * Returns the cron execution offset for this site
1541      *
1542      * The main {@link self::cron()} is supposed to run every night in some random time
1543      * between 01:00 and 06:00 AM (local time). The exact moment is defined by so called
1544      * execution offset, that is the amount of time after 01:00 AM. The offset value is
1545      * initially generated randomly and then used consistently at the site. This way, the
1546      * regular checks against the download.moodle.org server are spread in time.
1547      *
1548      * @return int the offset number of seconds from range 1 sec to 5 hours
1549      */
1550     protected function cron_execution_offset() {
1551         global $CFG;
1553         if (empty($CFG->updatecronoffset)) {
1554             set_config('updatecronoffset', rand(1, 5 * HOURSECS));
1555         }
1557         return $CFG->updatecronoffset;
1558     }
1560     /**
1561      * Fetch available updates info and eventually send notification to site admins
1562      */
1563     protected function cron_execute() {
1565         try {
1566             $this->restore_response();
1567             $previous = $this->recentresponse;
1568             $this->fetch();
1569             $this->restore_response(true);
1570             $current = $this->recentresponse;
1571             $changes = $this->compare_responses($previous, $current);
1572             $notifications = $this->cron_notifications($changes);
1573             $this->cron_notify($notifications);
1574             $this->cron_mtrace('done');
1575         } catch (available_update_checker_exception $e) {
1576             $this->cron_mtrace('FAILED!');
1577         }
1578     }
1580     /**
1581      * Given the list of changes in available updates, pick those to send to site admins
1582      *
1583      * @param array $changes as returned by {@link self::compare_responses()}
1584      * @return array of available_update_info objects to send to site admins
1585      */
1586     protected function cron_notifications(array $changes) {
1587         global $CFG;
1589         $notifications = array();
1590         $pluginman = plugin_manager::instance();
1591         $plugins = $pluginman->get_plugins(true);
1593         foreach ($changes as $component => $componentchanges) {
1594             if (empty($componentchanges)) {
1595                 continue;
1596             }
1597             $componentupdates = $this->get_update_info($component,
1598                 array('minmaturity' => $CFG->updateminmaturity, 'notifybuilds' => $CFG->updatenotifybuilds));
1599             if (empty($componentupdates)) {
1600                 continue;
1601             }
1602             // notify only about those $componentchanges that are present in $componentupdates
1603             // to respect the preferences
1604             foreach ($componentchanges as $componentchange) {
1605                 foreach ($componentupdates as $componentupdate) {
1606                     if ($componentupdate->version == $componentchange['version']) {
1607                         if ($component == 'core') {
1608                             // In case of 'core', we already know that the $componentupdate
1609                             // is a real update with higher version ({@see self::get_update_info()}).
1610                             // We just perform additional check for the release property as there
1611                             // can be two Moodle releases having the same version (e.g. 2.4.0 and 2.5dev shortly
1612                             // after the release). We can do that because we have the release info
1613                             // always available for the core.
1614                             if ((string)$componentupdate->release === (string)$componentchange['release']) {
1615                                 $notifications[] = $componentupdate;
1616                             }
1617                         } else {
1618                             // Use the plugin_manager to check if the detected $componentchange
1619                             // is a real update with higher version. That is, the $componentchange
1620                             // is present in the array of {@link available_update_info} objects
1621                             // returned by the plugin's available_updates() method.
1622                             list($plugintype, $pluginname) = normalize_component($component);
1623                             if (!empty($plugins[$plugintype][$pluginname])) {
1624                                 $availableupdates = $plugins[$plugintype][$pluginname]->available_updates();
1625                                 if (!empty($availableupdates)) {
1626                                     foreach ($availableupdates as $availableupdate) {
1627                                         if ($availableupdate->version == $componentchange['version']) {
1628                                             $notifications[] = $componentupdate;
1629                                         }
1630                                     }
1631                                 }
1632                             }
1633                         }
1634                     }
1635                 }
1636             }
1637         }
1639         return $notifications;
1640     }
1642     /**
1643      * Sends the given notifications to site admins via messaging API
1644      *
1645      * @param array $notifications array of available_update_info objects to send
1646      */
1647     protected function cron_notify(array $notifications) {
1648         global $CFG;
1650         if (empty($notifications)) {
1651             return;
1652         }
1654         $admins = get_admins();
1656         if (empty($admins)) {
1657             return;
1658         }
1660         $this->cron_mtrace('sending notifications ... ', '');
1662         $text = get_string('updatenotifications', 'core_admin') . PHP_EOL;
1663         $html = html_writer::tag('h1', get_string('updatenotifications', 'core_admin')) . PHP_EOL;
1665         $coreupdates = array();
1666         $pluginupdates = array();
1668         foreach ($notifications as $notification) {
1669             if ($notification->component == 'core') {
1670                 $coreupdates[] = $notification;
1671             } else {
1672                 $pluginupdates[] = $notification;
1673             }
1674         }
1676         if (!empty($coreupdates)) {
1677             $text .= PHP_EOL . get_string('updateavailable', 'core_admin') . PHP_EOL;
1678             $html .= html_writer::tag('h2', get_string('updateavailable', 'core_admin')) . PHP_EOL;
1679             $html .= html_writer::start_tag('ul') . PHP_EOL;
1680             foreach ($coreupdates as $coreupdate) {
1681                 $html .= html_writer::start_tag('li');
1682                 if (isset($coreupdate->release)) {
1683                     $text .= get_string('updateavailable_release', 'core_admin', $coreupdate->release);
1684                     $html .= html_writer::tag('strong', get_string('updateavailable_release', 'core_admin', $coreupdate->release));
1685                 }
1686                 if (isset($coreupdate->version)) {
1687                     $text .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
1688                     $html .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
1689                 }
1690                 if (isset($coreupdate->maturity)) {
1691                     $text .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
1692                     $html .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
1693                 }
1694                 $text .= PHP_EOL;
1695                 $html .= html_writer::end_tag('li') . PHP_EOL;
1696             }
1697             $text .= PHP_EOL;
1698             $html .= html_writer::end_tag('ul') . PHP_EOL;
1700             $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/index.php');
1701             $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
1702             $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/index.php', $CFG->wwwroot.'/'.$CFG->admin.'/index.php'));
1703             $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
1704         }
1706         if (!empty($pluginupdates)) {
1707             $text .= PHP_EOL . get_string('updateavailableforplugin', 'core_admin') . PHP_EOL;
1708             $html .= html_writer::tag('h2', get_string('updateavailableforplugin', 'core_admin')) . PHP_EOL;
1710             $html .= html_writer::start_tag('ul') . PHP_EOL;
1711             foreach ($pluginupdates as $pluginupdate) {
1712                 $html .= html_writer::start_tag('li');
1713                 $text .= get_string('pluginname', $pluginupdate->component);
1714                 $html .= html_writer::tag('strong', get_string('pluginname', $pluginupdate->component));
1716                 $text .= ' ('.$pluginupdate->component.')';
1717                 $html .= ' ('.$pluginupdate->component.')';
1719                 $text .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
1720                 $html .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
1722                 $text .= PHP_EOL;
1723                 $html .= html_writer::end_tag('li') . PHP_EOL;
1724             }
1725             $text .= PHP_EOL;
1726             $html .= html_writer::end_tag('ul') . PHP_EOL;
1728             $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php');
1729             $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
1730             $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/plugins.php', $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php'));
1731             $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
1732         }
1734         $a = array('siteurl' => $CFG->wwwroot);
1735         $text .= get_string('updatenotificationfooter', 'core_admin', $a) . PHP_EOL;
1736         $a = array('siteurl' => html_writer::link($CFG->wwwroot, $CFG->wwwroot));
1737         $html .= html_writer::tag('footer', html_writer::tag('p', get_string('updatenotificationfooter', 'core_admin', $a),
1738             array('style' => 'font-size:smaller; color:#333;')));
1740         foreach ($admins as $admin) {
1741             $message = new stdClass();
1742             $message->component         = 'moodle';
1743             $message->name              = 'availableupdate';
1744             $message->userfrom          = get_admin();
1745             $message->userto            = $admin;
1746             $message->subject           = get_string('updatenotificationsubject', 'core_admin', array('siteurl' => $CFG->wwwroot));
1747             $message->fullmessage       = $text;
1748             $message->fullmessageformat = FORMAT_PLAIN;
1749             $message->fullmessagehtml   = $html;
1750             $message->smallmessage      = get_string('updatenotifications', 'core_admin');
1751             $message->notification      = 1;
1752             message_send($message);
1753         }
1754     }
1756     /**
1757      * Compare two release labels and decide if they are the same
1758      *
1759      * @param string $remote release info of the available update
1760      * @param null|string $local release info of the local code, defaults to $release defined in version.php
1761      * @return boolean true if the releases declare the same minor+major version
1762      */
1763     protected function is_same_release($remote, $local=null) {
1765         if (is_null($local)) {
1766             $this->load_current_environment();
1767             $local = $this->currentrelease;
1768         }
1770         $pattern = '/^([0-9\.\+]+)([^(]*)/';
1772         preg_match($pattern, $remote, $remotematches);
1773         preg_match($pattern, $local, $localmatches);
1775         $remotematches[1] = str_replace('+', '', $remotematches[1]);
1776         $localmatches[1] = str_replace('+', '', $localmatches[1]);
1778         if ($remotematches[1] === $localmatches[1] and rtrim($remotematches[2]) === rtrim($localmatches[2])) {
1779             return true;
1780         } else {
1781             return false;
1782         }
1783     }
1787 /**
1788  * Defines the structure of objects returned by {@link available_update_checker::get_update_info()}
1789  */
1790 class available_update_info {
1792     /** @var string frankenstyle component name */
1793     public $component;
1794     /** @var int the available version of the component */
1795     public $version;
1796     /** @var string|null optional release name */
1797     public $release = null;
1798     /** @var int|null optional maturity info, eg {@link MATURITY_STABLE} */
1799     public $maturity = null;
1800     /** @var string|null optional URL of a page with more info about the update */
1801     public $url = null;
1802     /** @var string|null optional URL of a ZIP package that can be downloaded and installed */
1803     public $download = null;
1804     /** @var string|null of self::download is set, then this must be the MD5 hash of the ZIP */
1805     public $downloadmd5 = null;
1807     /**
1808      * Creates new instance of the class
1809      *
1810      * The $info array must provide at least the 'version' value and optionally all other
1811      * values to populate the object's properties.
1812      *
1813      * @param string $name the frankenstyle component name
1814      * @param array $info associative array with other properties
1815      */
1816     public function __construct($name, array $info) {
1817         $this->component = $name;
1818         foreach ($info as $k => $v) {
1819             if (property_exists('available_update_info', $k) and $k != 'component') {
1820                 $this->$k = $v;
1821             }
1822         }
1823     }
1827 /**
1828  * Implements a communication bridge to the mdeploy.php utility
1829  */
1830 class available_update_deployer {
1832     const HTTP_PARAM_PREFIX     = 'updteautodpldata_';  // Hey, even Google has not heard of such a prefix! So it MUST be safe :-p
1833     const HTTP_PARAM_CHECKER    = 'datapackagesize';    // Name of the parameter that holds the number of items in the received data items
1835     /** @var available_update_deployer holds the singleton instance */
1836     protected static $singletoninstance;
1837     /** @var moodle_url URL of a page that includes the deployer UI */
1838     protected $callerurl;
1839     /** @var moodle_url URL to return after the deployment */
1840     protected $returnurl;
1842     /**
1843      * Direct instantiation not allowed, use the factory method {@link self::instance()}
1844      */
1845     protected function __construct() {
1846     }
1848     /**
1849      * Sorry, this is singleton
1850      */
1851     protected function __clone() {
1852     }
1854     /**
1855      * Factory method for this class
1856      *
1857      * @return available_update_deployer the singleton instance
1858      */
1859     public static function instance() {
1860         if (is_null(self::$singletoninstance)) {
1861             self::$singletoninstance = new self();
1862         }
1863         return self::$singletoninstance;
1864     }
1866     /**
1867      * Reset caches used by this script
1868      *
1869      * @param bool $phpunitreset is this called as a part of PHPUnit reset?
1870      */
1871     public static function reset_caches($phpunitreset = false) {
1872         if ($phpunitreset) {
1873             self::$singletoninstance = null;
1874         }
1875     }
1877     /**
1878      * Is automatic deployment enabled?
1879      *
1880      * @return bool
1881      */
1882     public function enabled() {
1883         global $CFG;
1885         if (!empty($CFG->disableupdateautodeploy)) {
1886             // The feature is prohibited via config.php
1887             return false;
1888         }
1890         return get_config('updateautodeploy');
1891     }
1893     /**
1894      * Sets some base properties of the class to make it usable.
1895      *
1896      * @param moodle_url $callerurl the base URL of a script that will handle the class'es form data
1897      * @param moodle_url $returnurl the final URL to return to when the deployment is finished
1898      */
1899     public function initialize(moodle_url $callerurl, moodle_url $returnurl) {
1901         if (!$this->enabled()) {
1902             throw new coding_exception('Unable to initialize the deployer, the feature is not enabled.');
1903         }
1905         $this->callerurl = $callerurl;
1906         $this->returnurl = $returnurl;
1907     }
1909     /**
1910      * Has the deployer been initialized?
1911      *
1912      * Initialized deployer means that the following properties were set:
1913      * callerurl, returnurl
1914      *
1915      * @return bool
1916      */
1917     public function initialized() {
1919         if (!$this->enabled()) {
1920             return false;
1921         }
1923         if (empty($this->callerurl)) {
1924             return false;
1925         }
1927         if (empty($this->returnurl)) {
1928             return false;
1929         }
1931         return true;
1932     }
1934     /**
1935      * Returns a list of reasons why the deployment can not happen
1936      *
1937      * If the returned array is empty, the deployment seems to be possible. The returned
1938      * structure is an associative array with keys representing individual impediments.
1939      * Possible keys are: missingdownloadurl, missingdownloadmd5, notwritable.
1940      *
1941      * @param available_update_info $info
1942      * @return array
1943      */
1944     public function deployment_impediments(available_update_info $info) {
1946         $impediments = array();
1948         if (empty($info->download)) {
1949             $impediments['missingdownloadurl'] = true;
1950         }
1952         if (empty($info->downloadmd5)) {
1953             $impediments['missingdownloadmd5'] = true;
1954         }
1956         if (!empty($info->download) and !$this->update_downloadable($info->download)) {
1957             $impediments['notdownloadable'] = true;
1958         }
1960         if (!$this->component_writable($info->component)) {
1961             $impediments['notwritable'] = true;
1962         }
1964         return $impediments;
1965     }
1967     /**
1968      * Check to see if the current version of the plugin seems to be a checkout of an external repository.
1969      *
1970      * @see plugin_manager::plugin_external_source()
1971      * @param available_update_info $info
1972      * @return false|string
1973      */
1974     public function plugin_external_source(available_update_info $info) {
1976         $paths = get_plugin_types(true);
1977         list($plugintype, $pluginname) = normalize_component($info->component);
1978         $pluginroot = $paths[$plugintype].'/'.$pluginname;
1980         if (is_dir($pluginroot.'/.git')) {
1981             return 'git';
1982         }
1984         if (is_dir($pluginroot.'/CVS')) {
1985             return 'cvs';
1986         }
1988         if (is_dir($pluginroot.'/.svn')) {
1989             return 'svn';
1990         }
1992         return false;
1993     }
1995     /**
1996      * Prepares a renderable widget to confirm installation of an available update.
1997      *
1998      * @param available_update_info $info component version to deploy
1999      * @return renderable
2000      */
2001     public function make_confirm_widget(available_update_info $info) {
2003         if (!$this->initialized()) {
2004             throw new coding_exception('Illegal method call - deployer not initialized.');
2005         }
2007         $params = $this->data_to_params(array(
2008             'updateinfo' => (array)$info,   // see http://www.php.net/manual/en/language.types.array.php#language.types.array.casting
2009         ));
2011         $widget = new single_button(
2012             new moodle_url($this->callerurl, $params),
2013             get_string('updateavailableinstall', 'core_admin'),
2014             'post'
2015         );
2017         return $widget;
2018     }
2020     /**
2021      * Prepares a renderable widget to execute installation of an available update.
2022      *
2023      * @param available_update_info $info component version to deploy
2024      * @param moodle_url $returnurl URL to return after the installation execution
2025      * @return renderable
2026      */
2027     public function make_execution_widget(available_update_info $info, moodle_url $returnurl = null) {
2028         global $CFG;
2030         if (!$this->initialized()) {
2031             throw new coding_exception('Illegal method call - deployer not initialized.');
2032         }
2034         $pluginrootpaths = get_plugin_types(true);
2036         list($plugintype, $pluginname) = normalize_component($info->component);
2038         if (empty($pluginrootpaths[$plugintype])) {
2039             throw new coding_exception('Unknown plugin type root location', $plugintype);
2040         }
2042         list($passfile, $password) = $this->prepare_authorization();
2044         if (is_null($returnurl)) {
2045             $returnurl = new moodle_url('/admin');
2046         } else {
2047             $returnurl = $returnurl;
2048         }
2050         $params = array(
2051             'upgrade' => true,
2052             'type' => $plugintype,
2053             'name' => $pluginname,
2054             'typeroot' => $pluginrootpaths[$plugintype],
2055             'package' => $info->download,
2056             'md5' => $info->downloadmd5,
2057             'dataroot' => $CFG->dataroot,
2058             'dirroot' => $CFG->dirroot,
2059             'passfile' => $passfile,
2060             'password' => $password,
2061             'returnurl' => $returnurl->out(false),
2062         );
2064         if (!empty($CFG->proxyhost)) {
2065             // MDL-36973 - Beware - we should call just !is_proxybypass() here. But currently, our
2066             // cURL wrapper class does not do it. So, to have consistent behaviour, we pass proxy
2067             // setting regardless the $CFG->proxybypass setting. Once the {@link curl} class is
2068             // fixed, the condition should be amended.
2069             if (true or !is_proxybypass($info->download)) {
2070                 if (empty($CFG->proxyport)) {
2071                     $params['proxy'] = $CFG->proxyhost;
2072                 } else {
2073                     $params['proxy'] = $CFG->proxyhost.':'.$CFG->proxyport;
2074                 }
2076                 if (!empty($CFG->proxyuser) and !empty($CFG->proxypassword)) {
2077                     $params['proxyuserpwd'] = $CFG->proxyuser.':'.$CFG->proxypassword;
2078                 }
2080                 if (!empty($CFG->proxytype)) {
2081                     $params['proxytype'] = $CFG->proxytype;
2082                 }
2083             }
2084         }
2086         $widget = new single_button(
2087             new moodle_url('/mdeploy.php', $params),
2088             get_string('updateavailableinstall', 'core_admin'),
2089             'post'
2090         );
2092         return $widget;
2093     }
2095     /**
2096      * Returns array of data objects passed to this tool.
2097      *
2098      * @return array
2099      */
2100     public function submitted_data() {
2102         $data = $this->params_to_data($_POST);
2104         if (empty($data) or empty($data[self::HTTP_PARAM_CHECKER])) {
2105             return false;
2106         }
2108         if (!empty($data['updateinfo']) and is_object($data['updateinfo'])) {
2109             $updateinfo = $data['updateinfo'];
2110             if (!empty($updateinfo->component) and !empty($updateinfo->version)) {
2111                 $data['updateinfo'] = new available_update_info($updateinfo->component, (array)$updateinfo);
2112             }
2113         }
2115         if (!empty($data['callerurl'])) {
2116             $data['callerurl'] = new moodle_url($data['callerurl']);
2117         }
2119         if (!empty($data['returnurl'])) {
2120             $data['returnurl'] = new moodle_url($data['returnurl']);
2121         }
2123         return $data;
2124     }
2126     /**
2127      * Handles magic getters and setters for protected properties.
2128      *
2129      * @param string $name method name, e.g. set_returnurl()
2130      * @param array $arguments arguments to be passed to the array
2131      */
2132     public function __call($name, array $arguments = array()) {
2134         if (substr($name, 0, 4) === 'set_') {
2135             $property = substr($name, 4);
2136             if (empty($property)) {
2137                 throw new coding_exception('Invalid property name (empty)');
2138             }
2139             if (empty($arguments)) {
2140                 $arguments = array(true); // Default value for flag-like properties.
2141             }
2142             // Make sure it is a protected property.
2143             $isprotected = false;
2144             $reflection = new ReflectionObject($this);
2145             foreach ($reflection->getProperties(ReflectionProperty::IS_PROTECTED) as $reflectionproperty) {
2146                 if ($reflectionproperty->getName() === $property) {
2147                     $isprotected = true;
2148                     break;
2149                 }
2150             }
2151             if (!$isprotected) {
2152                 throw new coding_exception('Unable to set property - it does not exist or it is not protected');
2153             }
2154             $value = reset($arguments);
2155             $this->$property = $value;
2156             return;
2157         }
2159         if (substr($name, 0, 4) === 'get_') {
2160             $property = substr($name, 4);
2161             if (empty($property)) {
2162                 throw new coding_exception('Invalid property name (empty)');
2163             }
2164             if (!empty($arguments)) {
2165                 throw new coding_exception('No parameter expected');
2166             }
2167             // Make sure it is a protected property.
2168             $isprotected = false;
2169             $reflection = new ReflectionObject($this);
2170             foreach ($reflection->getProperties(ReflectionProperty::IS_PROTECTED) as $reflectionproperty) {
2171                 if ($reflectionproperty->getName() === $property) {
2172                     $isprotected = true;
2173                     break;
2174                 }
2175             }
2176             if (!$isprotected) {
2177                 throw new coding_exception('Unable to get property - it does not exist or it is not protected');
2178             }
2179             return $this->$property;
2180         }
2181     }
2183     /**
2184      * Generates a random token and stores it in a file in moodledata directory.
2185      *
2186      * @return array of the (string)filename and (string)password in this order
2187      */
2188     public function prepare_authorization() {
2189         global $CFG;
2191         make_upload_directory('mdeploy/auth/');
2193         $attempts = 0;
2194         $success = false;
2196         while (!$success and $attempts < 5) {
2197             $attempts++;
2199             $passfile = $this->generate_passfile();
2200             $password = $this->generate_password();
2201             $now = time();
2203             $filepath = $CFG->dataroot.'/mdeploy/auth/'.$passfile;
2205             if (!file_exists($filepath)) {
2206                 $success = file_put_contents($filepath, $password . PHP_EOL . $now . PHP_EOL, LOCK_EX);
2207             }
2208         }
2210         if ($success) {
2211             return array($passfile, $password);
2213         } else {
2214             throw new moodle_exception('unable_prepare_authorization', 'core_plugin');
2215         }
2216     }
2218     // End of external API
2220     /**
2221      * Prepares an array of HTTP parameters that can be passed to another page.
2222      *
2223      * @param array|object $data associative array or an object holding the data, data JSON-able
2224      * @return array suitable as a param for moodle_url
2225      */
2226     protected function data_to_params($data) {
2228         // Append some our own data
2229         if (!empty($this->callerurl)) {
2230             $data['callerurl'] = $this->callerurl->out(false);
2231         }
2232         if (!empty($this->returnurl)) {
2233             $data['returnurl'] = $this->returnurl->out(false);
2234         }
2236         // Finally append the count of items in the package.
2237         $data[self::HTTP_PARAM_CHECKER] = count($data);
2239         // Generate params
2240         $params = array();
2241         foreach ($data as $name => $value) {
2242             $transname = self::HTTP_PARAM_PREFIX.$name;
2243             $transvalue = json_encode($value);
2244             $params[$transname] = $transvalue;
2245         }
2247         return $params;
2248     }
2250     /**
2251      * Converts HTTP parameters passed to the script into native PHP data
2252      *
2253      * @param array $params such as $_REQUEST or $_POST
2254      * @return array data passed for this class
2255      */
2256     protected function params_to_data(array $params) {
2258         if (empty($params)) {
2259             return array();
2260         }
2262         $data = array();
2263         foreach ($params as $name => $value) {
2264             if (strpos($name, self::HTTP_PARAM_PREFIX) === 0) {
2265                 $realname = substr($name, strlen(self::HTTP_PARAM_PREFIX));
2266                 $realvalue = json_decode($value);
2267                 $data[$realname] = $realvalue;
2268             }
2269         }
2271         return $data;
2272     }
2274     /**
2275      * Returns a random string to be used as a filename of the password storage.
2276      *
2277      * @return string
2278      */
2279     protected function generate_passfile() {
2280         return clean_param(uniqid('mdeploy_', true), PARAM_FILE);
2281     }
2283     /**
2284      * Returns a random string to be used as the authorization token
2285      *
2286      * @return string
2287      */
2288     protected function generate_password() {
2289         return complex_random_string();
2290     }
2292     /**
2293      * Checks if the given component's directory is writable
2294      *
2295      * For the purpose of the deployment, the web server process has to have
2296      * write access to all files in the component's directory (recursively) and for the
2297      * directory itself.
2298      *
2299      * @see worker::move_directory_source_precheck()
2300      * @param string $component normalized component name
2301      * @return boolean
2302      */
2303     protected function component_writable($component) {
2305         list($plugintype, $pluginname) = normalize_component($component);
2307         $directory = get_plugin_directory($plugintype, $pluginname);
2309         if (is_null($directory)) {
2310             throw new coding_exception('Unknown component location', $component);
2311         }
2313         return $this->directory_writable($directory);
2314     }
2316     /**
2317      * Checks if the mdeploy.php will be able to fetch the ZIP from the given URL
2318      *
2319      * This is mainly supposed to check if the transmission over HTTPS would
2320      * work. That is, if the CA certificates are present at the server.
2321      *
2322      * @param string $downloadurl the URL of the ZIP package to download
2323      * @return bool
2324      */
2325     protected function update_downloadable($downloadurl) {
2326         global $CFG;
2328         $curloptions = array(
2329             'CURLOPT_SSL_VERIFYHOST' => 2,      // this is the default in {@link curl} class but just in case
2330             'CURLOPT_SSL_VERIFYPEER' => true,
2331         );
2333         $curl = new curl(array('proxy' => true));
2334         $result = $curl->head($downloadurl, $curloptions);
2335         $errno = $curl->get_errno();
2336         if (empty($errno)) {
2337             return true;
2338         } else {
2339             return false;
2340         }
2341     }
2343     /**
2344      * Checks if the directory and all its contents (recursively) is writable
2345      *
2346      * @param string $path full path to a directory
2347      * @return boolean
2348      */
2349     private function directory_writable($path) {
2351         if (!is_writable($path)) {
2352             return false;
2353         }
2355         if (is_dir($path)) {
2356             $handle = opendir($path);
2357         } else {
2358             return false;
2359         }
2361         $result = true;
2363         while ($filename = readdir($handle)) {
2364             $filepath = $path.'/'.$filename;
2366             if ($filename === '.' or $filename === '..') {
2367                 continue;
2368             }
2370             if (is_dir($filepath)) {
2371                 $result = $result && $this->directory_writable($filepath);
2373             } else {
2374                 $result = $result && is_writable($filepath);
2375             }
2376         }
2378         closedir($handle);
2380         return $result;
2381     }
2385 /**
2386  * Factory class producing required subclasses of {@link plugininfo_base}
2387  */
2388 class plugininfo_default_factory {
2390     /**
2391      * Makes a new instance of the plugininfo class
2392      *
2393      * @param string $type the plugin type, eg. 'mod'
2394      * @param string $typerootdir full path to the location of all the plugins of this type
2395      * @param string $name the plugin name, eg. 'workshop'
2396      * @param string $namerootdir full path to the location of the plugin
2397      * @param string $typeclass the name of class that holds the info about the plugin
2398      * @return plugininfo_base the instance of $typeclass
2399      */
2400     public static function make($type, $typerootdir, $name, $namerootdir, $typeclass) {
2401         $plugin              = new $typeclass();
2402         $plugin->type        = $type;
2403         $plugin->typerootdir = $typerootdir;
2404         $plugin->name        = $name;
2405         $plugin->rootdir     = $namerootdir;
2407         $plugin->init_display_name();
2408         $plugin->load_disk_version();
2409         $plugin->load_db_version();
2410         $plugin->load_required_main_version();
2411         $plugin->init_is_standard();
2413         return $plugin;
2414     }
2418 /**
2419  * Base class providing access to the information about a plugin
2420  *
2421  * @property-read string component the component name, type_name
2422  */
2423 abstract class plugininfo_base {
2425     /** @var string the plugintype name, eg. mod, auth or workshopform */
2426     public $type;
2427     /** @var string full path to the location of all the plugins of this type */
2428     public $typerootdir;
2429     /** @var string the plugin name, eg. assignment, ldap */
2430     public $name;
2431     /** @var string the localized plugin name */
2432     public $displayname;
2433     /** @var string the plugin source, one of plugin_manager::PLUGIN_SOURCE_xxx constants */
2434     public $source;
2435     /** @var fullpath to the location of this plugin */
2436     public $rootdir;
2437     /** @var int|string the version of the plugin's source code */
2438     public $versiondisk;
2439     /** @var int|string the version of the installed plugin */
2440     public $versiondb;
2441     /** @var int|float|string required version of Moodle core  */
2442     public $versionrequires;
2443     /** @var array other plugins that this one depends on, lazy-loaded by {@link get_other_required_plugins()} */
2444     public $dependencies;
2445     /** @var int number of instances of the plugin - not supported yet */
2446     public $instances;
2447     /** @var int order of the plugin among other plugins of the same type - not supported yet */
2448     public $sortorder;
2449     /** @var array|null array of {@link available_update_info} for this plugin */
2450     public $availableupdates;
2452     /**
2453      * Gathers and returns the information about all plugins of the given type
2454      *
2455      * @param string $type the name of the plugintype, eg. mod, auth or workshopform
2456      * @param string $typerootdir full path to the location of the plugin dir
2457      * @param string $typeclass the name of the actually called class
2458      * @return array of plugintype classes, indexed by the plugin name
2459      */
2460     public static function get_plugins($type, $typerootdir, $typeclass) {
2462         // get the information about plugins at the disk
2463         $plugins = get_plugin_list($type);
2464         $ondisk = array();
2465         foreach ($plugins as $pluginname => $pluginrootdir) {
2466             $ondisk[$pluginname] = plugininfo_default_factory::make($type, $typerootdir,
2467                 $pluginname, $pluginrootdir, $typeclass);
2468         }
2469         return $ondisk;
2470     }
2472     /**
2473      * Sets {@link $displayname} property to a localized name of the plugin
2474      */
2475     public function init_display_name() {
2476         if (!get_string_manager()->string_exists('pluginname', $this->component)) {
2477             $this->displayname = '[pluginname,' . $this->component . ']';
2478         } else {
2479             $this->displayname = get_string('pluginname', $this->component);
2480         }
2481     }
2483     /**
2484      * Magic method getter, redirects to read only values.
2485      *
2486      * @param string $name
2487      * @return mixed
2488      */
2489     public function __get($name) {
2490         switch ($name) {
2491             case 'component': return $this->type . '_' . $this->name;
2493             default:
2494                 debugging('Invalid plugin property accessed! '.$name);
2495                 return null;
2496         }
2497     }
2499     /**
2500      * Return the full path name of a file within the plugin.
2501      *
2502      * No check is made to see if the file exists.
2503      *
2504      * @param string $relativepath e.g. 'version.php'.
2505      * @return string e.g. $CFG->dirroot . '/mod/quiz/version.php'.
2506      */
2507     public function full_path($relativepath) {
2508         if (empty($this->rootdir)) {
2509             return '';
2510         }
2511         return $this->rootdir . '/' . $relativepath;
2512     }
2514     /**
2515      * Load the data from version.php.
2516      *
2517      * @param bool $disablecache do not attempt to obtain data from the cache
2518      * @return stdClass the object called $plugin defined in version.php
2519      */
2520     protected function load_version_php($disablecache=false) {
2522         $cache = cache::make('core', 'plugininfo_base');
2524         $versionsphp = $cache->get('versions_php');
2526         if (!$disablecache and $versionsphp !== false and isset($versionsphp[$this->component])) {
2527             return $versionsphp[$this->component];
2528         }
2530         $versionfile = $this->full_path('version.php');
2532         $plugin = new stdClass();
2533         if (is_readable($versionfile)) {
2534             include($versionfile);
2535         }
2536         $versionsphp[$this->component] = $plugin;
2537         $cache->set('versions_php', $versionsphp);
2539         return $plugin;
2540     }
2542     /**
2543      * Sets {@link $versiondisk} property to a numerical value representing the
2544      * version of the plugin's source code.
2545      *
2546      * If the value is null after calling this method, either the plugin
2547      * does not use versioning (typically does not have any database
2548      * data) or is missing from disk.
2549      */
2550     public function load_disk_version() {
2551         $plugin = $this->load_version_php();
2552         if (isset($plugin->version)) {
2553             $this->versiondisk = $plugin->version;
2554         }
2555     }
2557     /**
2558      * Sets {@link $versionrequires} property to a numerical value representing
2559      * the version of Moodle core that this plugin requires.
2560      */
2561     public function load_required_main_version() {
2562         $plugin = $this->load_version_php();
2563         if (isset($plugin->requires)) {
2564             $this->versionrequires = $plugin->requires;
2565         }
2566     }
2568     /**
2569      * Initialise {@link $dependencies} to the list of other plugins (in any)
2570      * that this one requires to be installed.
2571      */
2572     protected function load_other_required_plugins() {
2573         $plugin = $this->load_version_php();
2574         if (!empty($plugin->dependencies)) {
2575             $this->dependencies = $plugin->dependencies;
2576         } else {
2577             $this->dependencies = array(); // By default, no dependencies.
2578         }
2579     }
2581     /**
2582      * Get the list of other plugins that this plugin requires to be installed.
2583      *
2584      * @return array with keys the frankenstyle plugin name, and values either
2585      *      a version string (like '2011101700') or the constant ANY_VERSION.
2586      */
2587     public function get_other_required_plugins() {
2588         if (is_null($this->dependencies)) {
2589             $this->load_other_required_plugins();
2590         }
2591         return $this->dependencies;
2592     }
2594     /**
2595      * Is this is a subplugin?
2596      *
2597      * @return boolean
2598      */
2599     public function is_subplugin() {
2600         return ($this->get_parent_plugin() !== false);
2601     }
2603     /**
2604      * If I am a subplugin, return the name of my parent plugin.
2605      *
2606      * @return string|bool false if not a subplugin, name of the parent otherwise
2607      */
2608     public function get_parent_plugin() {
2609         return $this->get_plugin_manager()->get_parent_of_subplugin($this->type);
2610     }
2612     /**
2613      * Sets {@link $versiondb} property to a numerical value representing the
2614      * currently installed version of the plugin.
2615      *
2616      * If the value is null after calling this method, either the plugin
2617      * does not use versioning (typically does not have any database
2618      * data) or has not been installed yet.
2619      */
2620     public function load_db_version() {
2621         if ($ver = self::get_version_from_config_plugins($this->component)) {
2622             $this->versiondb = $ver;
2623         }
2624     }
2626     /**
2627      * Sets {@link $source} property to one of plugin_manager::PLUGIN_SOURCE_xxx
2628      * constants.
2629      *
2630      * If the property's value is null after calling this method, then
2631      * the type of the plugin has not been recognized and you should throw
2632      * an exception.
2633      */
2634     public function init_is_standard() {
2636         $standard = plugin_manager::standard_plugins_list($this->type);
2638         if ($standard !== false) {
2639             $standard = array_flip($standard);
2640             if (isset($standard[$this->name])) {
2641                 $this->source = plugin_manager::PLUGIN_SOURCE_STANDARD;
2642             } else if (!is_null($this->versiondb) and is_null($this->versiondisk)
2643                     and plugin_manager::is_deleted_standard_plugin($this->type, $this->name)) {
2644                 $this->source = plugin_manager::PLUGIN_SOURCE_STANDARD; // to be deleted
2645             } else {
2646                 $this->source = plugin_manager::PLUGIN_SOURCE_EXTENSION;
2647             }
2648         }
2649     }
2651     /**
2652      * Returns true if the plugin is shipped with the official distribution
2653      * of the current Moodle version, false otherwise.
2654      *
2655      * @return bool
2656      */
2657     public function is_standard() {
2658         return $this->source === plugin_manager::PLUGIN_SOURCE_STANDARD;
2659     }
2661     /**
2662      * Returns true if the the given Moodle version is enough to run this plugin
2663      *
2664      * @param string|int|double $moodleversion
2665      * @return bool
2666      */
2667     public function is_core_dependency_satisfied($moodleversion) {
2669         if (empty($this->versionrequires)) {
2670             return true;
2672         } else {
2673             return (double)$this->versionrequires <= (double)$moodleversion;
2674         }
2675     }
2677     /**
2678      * Returns the status of the plugin
2679      *
2680      * @return string one of plugin_manager::PLUGIN_STATUS_xxx constants
2681      */
2682     public function get_status() {
2684         if (is_null($this->versiondb) and is_null($this->versiondisk)) {
2685             return plugin_manager::PLUGIN_STATUS_NODB;
2687         } else if (is_null($this->versiondb) and !is_null($this->versiondisk)) {
2688             return plugin_manager::PLUGIN_STATUS_NEW;
2690         } else if (!is_null($this->versiondb) and is_null($this->versiondisk)) {
2691             if (plugin_manager::is_deleted_standard_plugin($this->type, $this->name)) {
2692                 return plugin_manager::PLUGIN_STATUS_DELETE;
2693             } else {
2694                 return plugin_manager::PLUGIN_STATUS_MISSING;
2695             }
2697         } else if ((string)$this->versiondb === (string)$this->versiondisk) {
2698             return plugin_manager::PLUGIN_STATUS_UPTODATE;
2700         } else if ($this->versiondb < $this->versiondisk) {
2701             return plugin_manager::PLUGIN_STATUS_UPGRADE;
2703         } else if ($this->versiondb > $this->versiondisk) {
2704             return plugin_manager::PLUGIN_STATUS_DOWNGRADE;
2706         } else {
2707             // $version = pi(); and similar funny jokes - hopefully Donald E. Knuth will never contribute to Moodle ;-)
2708             throw new coding_exception('Unable to determine plugin state, check the plugin versions');
2709         }
2710     }
2712     /**
2713      * Returns the information about plugin availability
2714      *
2715      * True means that the plugin is enabled. False means that the plugin is
2716      * disabled. Null means that the information is not available, or the
2717      * plugin does not support configurable availability or the availability
2718      * can not be changed.
2719      *
2720      * @return null|bool
2721      */
2722     public function is_enabled() {
2723         return null;
2724     }
2726     /**
2727      * Populates the property {@link $availableupdates} with the information provided by
2728      * available update checker
2729      *
2730      * @param available_update_checker $provider the class providing the available update info
2731      */
2732     public function check_available_updates(available_update_checker $provider) {
2733         global $CFG;
2735         if (isset($CFG->updateminmaturity)) {
2736             $minmaturity = $CFG->updateminmaturity;
2737         } else {
2738             // this can happen during the very first upgrade to 2.3
2739             $minmaturity = MATURITY_STABLE;
2740         }
2742         $this->availableupdates = $provider->get_update_info($this->component,
2743             array('minmaturity' => $minmaturity));
2744     }
2746     /**
2747      * If there are updates for this plugin available, returns them.
2748      *
2749      * Returns array of {@link available_update_info} objects, if some update
2750      * is available. Returns null if there is no update available or if the update
2751      * availability is unknown.
2752      *
2753      * @return array|null
2754      */
2755     public function available_updates() {
2757         if (empty($this->availableupdates) or !is_array($this->availableupdates)) {
2758             return null;
2759         }
2761         $updates = array();
2763         foreach ($this->availableupdates as $availableupdate) {
2764             if ($availableupdate->version > $this->versiondisk) {
2765                 $updates[] = $availableupdate;
2766             }
2767         }
2769         if (empty($updates)) {
2770             return null;
2771         }
2773         return $updates;
2774     }
2776     /**
2777      * Returns the node name used in admin settings menu for this plugin settings (if applicable)
2778      *
2779      * @return null|string node name or null if plugin does not create settings node (default)
2780      */
2781     public function get_settings_section_name() {
2782         return null;
2783     }
2785     /**
2786      * Returns the URL of the plugin settings screen
2787      *
2788      * Null value means that the plugin either does not have the settings screen
2789      * or its location is not available via this library.
2790      *
2791      * @return null|moodle_url
2792      */
2793     public function get_settings_url() {
2794         $section = $this->get_settings_section_name();
2795         if ($section === null) {
2796             return null;
2797         }
2798         $settings = admin_get_root()->locate($section);
2799         if ($settings && $settings instanceof admin_settingpage) {
2800             return new moodle_url('/admin/settings.php', array('section' => $section));
2801         } else if ($settings && $settings instanceof admin_externalpage) {
2802             return new moodle_url($settings->url);
2803         } else {
2804             return null;
2805         }
2806     }
2808     /**
2809      * Loads plugin settings to the settings tree
2810      *
2811      * This function usually includes settings.php file in plugins folder.
2812      * Alternatively it can create a link to some settings page (instance of admin_externalpage)
2813      *
2814      * @param part_of_admin_tree $adminroot
2815      * @param string $parentnodename
2816      * @param bool $hassiteconfig whether the current user has moodle/site:config capability
2817      */
2818     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
2819     }
2821     /**
2822      * Should there be a way to uninstall the plugin via the administration UI
2823      *
2824      * By default, uninstallation is allowed for all non-standard add-ons. Subclasses
2825      * may want to override this to allow uninstallation of all plugins (simply by
2826      * returning true unconditionally). Subplugins follow their parent plugin's
2827      * decision by default.
2828      *
2829      * Note that even if true is returned, the core may still prohibit the uninstallation,
2830      * e.g. in case there are other plugins that depend on this one.
2831      *
2832      * @return boolean
2833      */
2834     public function is_uninstall_allowed() {
2836         if ($this->is_subplugin()) {
2837             return $this->get_plugin_manager()->get_plugin_info($this->get_parent_plugin())->is_uninstall_allowed();
2838         }
2840         if ($this->is_standard()) {
2841             return false;
2842         }
2844         return true;
2845     }
2847     /**
2848      * Optional extra warning before uninstallation, for example number of uses in courses.
2849      *
2850      * @return string
2851      */
2852     public function get_uninstall_extra_warning() {
2853         return '';
2854     }
2856     /**
2857      * Returns the URL of the screen where this plugin can be uninstalled
2858      *
2859      * Visiting that URL must be safe, that is a manual confirmation is needed
2860      * for actual uninstallation of the plugin. By default, URL to a common
2861      * uninstalling tool is returned.
2862      *
2863      * @return moodle_url
2864      */
2865     public function get_uninstall_url() {
2866         return $this->get_default_uninstall_url();
2867     }
2869     /**
2870      * Returns relative directory of the plugin with heading '/'
2871      *
2872      * @return string
2873      */
2874     public function get_dir() {
2875         global $CFG;
2877         return substr($this->rootdir, strlen($CFG->dirroot));
2878     }
2880     /**
2881      * Hook method to implement certain steps when uninstalling the plugin.
2882      *
2883      * This hook is called by {@link plugin_manager::uninstall_plugin()} so
2884      * it is basically usable only for those plugin types that use the default
2885      * uninstall tool provided by {@link self::get_default_uninstall_url()}.
2886      *
2887      * @param progress_trace $progress traces the process
2888      * @return bool true on success, false on failure
2889      */
2890     public function uninstall(progress_trace $progress) {
2891         return true;
2892     }
2894     /**
2895      * Returns URL to a script that handles common plugin uninstall procedure.
2896      *
2897      * This URL is suitable for plugins that do not have their own UI
2898      * for uninstalling.
2899      *
2900      * @return moodle_url
2901      */
2902     protected final function get_default_uninstall_url() {
2903         return new moodle_url('/admin/plugins.php', array(
2904             'sesskey' => sesskey(),
2905             'uninstall' => $this->component,
2906             'confirm' => 0,
2907         ));
2908     }
2910     /**
2911      * Provides access to plugin versions from the {config_plugins} table
2912      *
2913      * @param string $plugin plugin name
2914      * @param bool $disablecache do not attempt to obtain data from the cache
2915      * @return int|bool the stored value or false if not found
2916      */
2917     protected function get_version_from_config_plugins($plugin, $disablecache=false) {
2918         global $DB;
2920         $cache = cache::make('core', 'plugininfo_base');
2922         $pluginversions = $cache->get('versions_db');
2924         if ($pluginversions === false or $disablecache) {
2925             try {
2926                 $pluginversions = $DB->get_records_menu('config_plugins', array('name' => 'version'), 'plugin', 'plugin,value');
2927             } catch (dml_exception $e) {
2928                 // before install
2929                 $pluginversions = array();
2930             }
2931             $cache->set('versions_db', $pluginversions);
2932         }
2934         if (isset($pluginversions[$plugin])) {
2935             return $pluginversions[$plugin];
2936         } else {
2937             return false;
2938         }
2939     }
2941     /**
2942      * Provides access to the plugin_manager singleton.
2943      *
2944      * @return plugin_manmager
2945      */
2946     protected function get_plugin_manager() {
2947         return plugin_manager::instance();
2948     }
2952 /**
2953  * General class for all plugin types that do not have their own class
2954  */
2955 class plugininfo_general extends plugininfo_base {
2959 /**
2960  * Class for page side blocks
2961  */
2962 class plugininfo_block extends plugininfo_base {
2964     public static function get_plugins($type, $typerootdir, $typeclass) {
2966         // get the information about blocks at the disk
2967         $blocks = parent::get_plugins($type, $typerootdir, $typeclass);
2969         // add blocks missing from disk
2970         $blocksinfo = self::get_blocks_info();
2971         foreach ($blocksinfo as $blockname => $blockinfo) {
2972             if (isset($blocks[$blockname])) {
2973                 continue;
2974             }
2975             $plugin                 = new $typeclass();
2976             $plugin->type           = $type;
2977             $plugin->typerootdir    = $typerootdir;
2978             $plugin->name           = $blockname;
2979             $plugin->rootdir        = null;
2980             $plugin->displayname    = $blockname;
2981             $plugin->versiondb      = $blockinfo->version;
2982             $plugin->init_is_standard();
2984             $blocks[$blockname]   = $plugin;
2985         }
2987         return $blocks;
2988     }
2990     /**
2991      * Magic method getter, redirects to read only values.
2992      *
2993      * For block plugins pretends the object has 'visible' property for compatibility
2994      * with plugins developed for Moodle version below 2.4
2995      *
2996      * @param string $name
2997      * @return mixed
2998      */
2999     public function __get($name) {
3000         if ($name === 'visible') {
3001             debugging('This is now an instance of plugininfo_block, please use $block->is_enabled() instead of $block->visible', DEBUG_DEVELOPER);
3002             return ($this->is_enabled() !== false);
3003         }
3004         return parent::__get($name);
3005     }
3007     public function init_display_name() {
3009         if (get_string_manager()->string_exists('pluginname', 'block_' . $this->name)) {
3010             $this->displayname = get_string('pluginname', 'block_' . $this->name);
3012         } else if (($block = block_instance($this->name)) !== false) {
3013             $this->displayname = $block->get_title();
3015         } else {
3016             parent::init_display_name();
3017         }
3018     }
3020     public function load_db_version() {
3021         global $DB;
3023         $blocksinfo = self::get_blocks_info();
3024         if (isset($blocksinfo[$this->name]->version)) {
3025             $this->versiondb = $blocksinfo[$this->name]->version;
3026         }
3027     }
3029     public function is_enabled() {
3031         $blocksinfo = self::get_blocks_info();
3032         if (isset($blocksinfo[$this->name]->visible)) {
3033             if ($blocksinfo[$this->name]->visible) {
3034                 return true;
3035             } else {
3036                 return false;
3037             }
3038         } else {
3039             return parent::is_enabled();
3040         }
3041     }
3043     public function get_settings_section_name() {
3044         return 'blocksetting' . $this->name;
3045     }
3047     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3048         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3049         $ADMIN = $adminroot; // may be used in settings.php
3050         $block = $this; // also can be used inside settings.php
3051         $section = $this->get_settings_section_name();
3053         if (!$hassiteconfig || (($blockinstance = block_instance($this->name)) === false)) {
3054             return;
3055         }
3057         $settings = null;
3058         if ($blockinstance->has_config()) {
3059             if (file_exists($this->full_path('settings.php'))) {
3060                 $settings = new admin_settingpage($section, $this->displayname,
3061                         'moodle/site:config', $this->is_enabled() === false);
3062                 include($this->full_path('settings.php')); // this may also set $settings to null
3063             } else {
3064                 $blocksinfo = self::get_blocks_info();
3065                 $settingsurl = new moodle_url('/admin/block.php', array('block' => $blocksinfo[$this->name]->id));
3066                 $settings = new admin_externalpage($section, $this->displayname,
3067                         $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
3068             }
3069         }
3070         if ($settings) {
3071             $ADMIN->add($parentnodename, $settings);
3072         }
3073     }
3075     public function is_uninstall_allowed() {
3076         return true;
3077     }
3079     /**
3080      * Warnign with number of block instances.
3081      *
3082      * @return string
3083      */
3084     public function get_uninstall_extra_warning() {
3085         global $DB;
3087         if (!$count = $DB->count_records('block_instances', array('blockname'=>$this->name))) {
3088             return '';
3089         }
3091         return '<p>'.get_string('uninstallextraconfirmblock', 'core_plugin', array('instances'=>$count)).'</p>';
3092     }
3094     /**
3095      * Provides access to the records in {block} table
3096      *
3097      * @param bool $disablecache do not attempt to obtain data from the cache
3098      * @return array array of stdClasses
3099      */
3100     protected static function get_blocks_info($disablecache=false) {
3101         global $DB;
3103         $cache = cache::make('core', 'plugininfo_block');
3105         $blocktypes = $cache->get('blocktypes');
3107         if ($blocktypes === false or $disablecache) {
3108             try {
3109                 $blocktypes = $DB->get_records('block', null, 'name', 'name,id,version,visible');
3110             } catch (dml_exception $e) {
3111                 // before install
3112                 $blocktypes = array();
3113             }
3114             $cache->set('blocktypes', $blocktypes);
3115         }
3117         return $blocktypes;
3118     }
3122 /**
3123  * Class for text filters
3124  */
3125 class plugininfo_filter extends plugininfo_base {
3127     public static function get_plugins($type, $typerootdir, $typeclass) {
3128         global $CFG, $DB;
3130         $filters = array();
3132         // get the list of filters in /filter location
3133         $installed = filter_get_all_installed();
3135         foreach ($installed as $name => $displayname) {
3136             $plugin                 = new $typeclass();
3137             $plugin->type           = $type;
3138             $plugin->typerootdir    = $typerootdir;
3139             $plugin->name           = $name;
3140             $plugin->rootdir        = "$CFG->dirroot/filter/$name";
3141             $plugin->displayname    = $displayname;
3143             $plugin->load_disk_version();
3144             $plugin->load_db_version();
3145             $plugin->load_required_main_version();
3146             $plugin->init_is_standard();
3148             $filters[$plugin->name] = $plugin;
3149         }
3151         // Do not mess with filter registration here!
3153         $globalstates = self::get_global_states();
3155         // make sure that all registered filters are installed, just in case
3156         foreach ($globalstates as $name => $info) {
3157             if (!isset($filters[$name])) {
3158                 // oops, there is a record in filter_active but the filter is not installed
3159                 $plugin                 = new $typeclass();
3160                 $plugin->type           = $type;
3161                 $plugin->typerootdir    = $typerootdir;
3162                 $plugin->name           = $name;
3163                 $plugin->rootdir        = "$CFG->dirroot/filter/$name";
3164                 $plugin->displayname    = $name;
3166                 $plugin->load_db_version();
3168                 if (is_null($plugin->versiondb)) {
3169                     // this is a hack to stimulate 'Missing from disk' error
3170                     // because $plugin->versiondisk will be null !== false
3171                     $plugin->versiondb = false;
3172                 }
3174                 $filters[$plugin->name] = $plugin;
3175             }
3176         }
3178         return $filters;
3179     }
3181     public function init_display_name() {
3182         // do nothing, the name is set in self::get_plugins()
3183     }
3185     public function is_enabled() {
3187         $globalstates = self::get_global_states();
3189         foreach ($globalstates as $name => $info) {
3190             if ($name === $this->name) {
3191                 if ($info->active == TEXTFILTER_DISABLED) {
3192                     return false;
3193                 } else {
3194                     // it may be 'On' or 'Off, but available'
3195                     return null;
3196                 }
3197             }
3198         }
3200         return null;
3201     }
3203     public function get_settings_section_name() {
3204         return 'filtersetting' . $this->name;
3205     }
3207     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3208         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3209         $ADMIN = $adminroot; // may be used in settings.php
3210         $filter = $this; // also can be used inside settings.php
3212         $settings = null;
3213         if ($hassiteconfig && file_exists($this->full_path('filtersettings.php'))) {
3214             $section = $this->get_settings_section_name();
3215             $settings = new admin_settingpage($section, $this->displayname,
3216                     'moodle/site:config', $this->is_enabled() === false);
3217             include($this->full_path('filtersettings.php')); // this may also set $settings to null
3218         }
3219         if ($settings) {
3220             $ADMIN->add($parentnodename, $settings);
3221         }
3222     }
3224     public function is_uninstall_allowed() {
3225         return true;
3226     }
3228     public function get_uninstall_url() {
3229         return new moodle_url('/admin/filters.php', array('sesskey' => sesskey(), 'filterpath' => $this->name, 'action' => 'delete'));
3230     }
3232     /**
3233      * Provides access to the results of {@link filter_get_global_states()}
3234      * but indexed by the normalized filter name
3235      *
3236      * The legacy filter name is available as ->legacyname property.
3237      *
3238      * @param bool $disablecache do not attempt to obtain data from the cache
3239      * @return array
3240      */
3241     protected static function get_global_states($disablecache=false) {
3242         global $DB;
3244         $cache = cache::make('core', 'plugininfo_filter');
3246         $globalstates = $cache->get('globalstates');
3248         if ($globalstates === false or $disablecache) {
3250             if (!$DB->get_manager()->table_exists('filter_active')) {
3251                 // Not installed yet.
3252                 $cache->set('globalstates', array());
3253                 return array();
3254             }
3256             $globalstates = array();
3258             foreach (filter_get_global_states() as $name => $info) {
3259                 if (strpos($name, '/') !== false) {
3260                     // Skip existing before upgrade to new names.
3261                     continue;
3262                 }
3264                 $filterinfo = new stdClass();
3265                 $filterinfo->active = $info->active;
3266                 $filterinfo->sortorder = $info->sortorder;
3267                 $globalstates[$name] = $filterinfo;
3268             }
3270             $cache->set('globalstates', $globalstates);
3271         }
3273         return $globalstates;
3274     }
3278 /**
3279  * Class for activity modules
3280  */
3281 class plugininfo_mod extends plugininfo_base {
3283     public static function get_plugins($type, $typerootdir, $typeclass) {
3285         // get the information about plugins at the disk
3286         $modules = parent::get_plugins($type, $typerootdir, $typeclass);
3288         // add modules missing from disk
3289         $modulesinfo = self::get_modules_info();
3290         foreach ($modulesinfo as $modulename => $moduleinfo) {
3291             if (isset($modules[$modulename])) {
3292                 continue;
3293             }
3294             $plugin                 = new $typeclass();
3295             $plugin->type           = $type;
3296             $plugin->typerootdir    = $typerootdir;
3297             $plugin->name           = $modulename;
3298             $plugin->rootdir        = null;
3299             $plugin->displayname    = $modulename;
3300             $plugin->versiondb      = $moduleinfo->version;
3301             $plugin->init_is_standard();
3303             $modules[$modulename]   = $plugin;
3304         }
3306         return $modules;
3307     }
3309     /**
3310      * Magic method getter, redirects to read only values.
3311      *
3312      * For module plugins we pretend the object has 'visible' property for compatibility
3313      * with plugins developed for Moodle version below 2.4
3314      *
3315      * @param string $name
3316      * @return mixed
3317      */
3318     public function __get($name) {
3319         if ($name === 'visible') {
3320             debugging('This is now an instance of plugininfo_mod, please use $module->is_enabled() instead of $module->visible', DEBUG_DEVELOPER);
3321             return ($this->is_enabled() !== false);
3322         }
3323         return parent::__get($name);
3324     }
3326     public function init_display_name() {
3327         if (get_string_manager()->string_exists('pluginname', $this->component)) {
3328             $this->displayname = get_string('pluginname', $this->component);
3329         } else {
3330             $this->displayname = get_string('modulename', $this->component);
3331         }
3332     }
3334     /**
3335      * Load the data from version.php.
3336      *
3337      * @param bool $disablecache do not attempt to obtain data from the cache
3338      * @return object the data object defined in version.php.
3339      */
3340     protected function load_version_php($disablecache=false) {
3342         $cache = cache::make('core', 'plugininfo_base');
3344         $versionsphp = $cache->get('versions_php');
3346         if (!$disablecache and $versionsphp !== false and isset($versionsphp[$this->component])) {
3347             return $versionsphp[$this->component];
3348         }
3350         $versionfile = $this->full_path('version.php');
3352         $module = new stdClass();
3353         $plugin = new stdClass();
3354         if (is_readable($versionfile)) {
3355             include($versionfile);
3356         }
3357         if (!isset($module->version) and isset($plugin->version)) {
3358             $module = $plugin;
3359         }
3360         $versionsphp[$this->component] = $module;
3361         $cache->set('versions_php', $versionsphp);
3363         return $module;
3364     }
3366     public function load_db_version() {
3367         global $DB;
3369         $modulesinfo = self::get_modules_info();
3370         if (isset($modulesinfo[$this->name]->version)) {
3371             $this->versiondb = $modulesinfo[$this->name]->version;
3372         }
3373     }
3375     public function is_enabled() {
3377         $modulesinfo = self::get_modules_info();
3378         if (isset($modulesinfo[$this->name]->visible)) {
3379             if ($modulesinfo[$this->name]->visible) {
3380                 return true;
3381             } else {
3382                 return false;
3383             }
3384         } else {
3385             return parent::is_enabled();
3386         }
3387     }
3389     public function get_settings_section_name() {
3390         return 'modsetting' . $this->name;
3391     }
3393     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3394         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3395         $ADMIN = $adminroot; // may be used in settings.php
3396         $module = $this; // also can be used inside settings.php
3397         $section = $this->get_settings_section_name();
3399         $modulesinfo = self::get_modules_info();
3400         $settings = null;
3401         if ($hassiteconfig && isset($modulesinfo[$this->name]) && file_exists($this->full_path('settings.php'))) {
3402             $settings = new admin_settingpage($section, $this->displayname,
3403                     'moodle/site:config', $this->is_enabled() === false);
3404             include($this->full_path('settings.php')); // this may also set $settings to null
3405         }
3406         if ($settings) {
3407             $ADMIN->add($parentnodename, $settings);
3408         }
3409     }
3411     /**
3412      * Allow all activity modules but Forum to be uninstalled.
3414      * This exception for the Forum has been hard-coded in Moodle since ages,
3415      * we may want to re-think it one day.
3416      */
3417     public function is_uninstall_allowed() {
3418         if ($this->name === 'forum') {
3419             return false;
3420         } else {
3421             return true;
3422         }
3423     }
3425     /**
3426      * Return warning with number of activities and number of affected courses.
3427      *
3428      * @return string
3429      */
3430     public function get_uninstall_extra_warning() {
3431         global $DB;
3433         if (!$module = $DB->get_record('modules', array('name'=>$this->name))) {
3434             return '';
3435         }
3437         if (!$count = $DB->count_records('course_modules', array('module'=>$module->id))) {
3438             return '';
3439         }
3441         $sql = "SELECT COUNT('x')
3442                   FROM (
3443                     SELECT course
3444                       FROM {course_modules}
3445                      WHERE module = :mid
3446                   GROUP BY course
3447                   ) c";
3448         $courses = $DB->count_records_sql($sql, array('mid'=>$module->id));
3450         return '<p>'.get_string('uninstallextraconfirmmod', 'core_plugin', array('instances'=>$count, 'courses'=>$courses)).'</p>';
3451     }
3453     /**
3454      * Provides access to the records in {modules} table
3455      *
3456      * @param bool $disablecache do not attempt to obtain data from the cache
3457      * @return array array of stdClasses
3458      */
3459     protected static function get_modules_info($disablecache=false) {
3460         global $DB;
3462         $cache = cache::make('core', 'plugininfo_mod');
3464         $modulesinfo = $cache->get('modulesinfo');
3466         if ($modulesinfo === false or $disablecache) {
3467             try {
3468                 $modulesinfo = $DB->get_records('modules', null, 'name', 'name,id,version,visible');
3469             } catch (dml_exception $e) {
3470                 // before install
3471                 $modulesinfo = array();
3472             }
3473             $cache->set('modulesinfo', $modulesinfo);
3474         }
3476         return $modulesinfo;
3477     }
3481 /**
3482  * Class for question behaviours.
3483  */
3484 class plugininfo_qbehaviour extends plugininfo_base {
3486     public function is_uninstall_allowed() {
3487         return true;
3488     }
3490     public function get_uninstall_url() {
3491         return new moodle_url('/admin/qbehaviours.php',
3492                 array('delete' => $this->name, 'sesskey' => sesskey()));
3493     }
3497 /**
3498  * Class for question types
3499  */
3500 class plugininfo_qtype extends plugininfo_base {
3502     public function is_uninstall_allowed() {
3503         return true;
3504     }
3506     public function get_uninstall_url() {
3507         return new moodle_url('/admin/qtypes.php',
3508                 array('delete' => $this->name, 'sesskey' => sesskey()));
3509     }
3511     public function get_settings_section_name() {
3512         return 'qtypesetting' . $this->name;
3513     }
3515     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3516         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3517         $ADMIN = $adminroot; // may be used in settings.php
3518         $qtype = $this; // also can be used inside settings.php
3519         $section = $this->get_settings_section_name();
3521         $settings = null;
3522         $systemcontext = context_system::instance();
3523         if (($hassiteconfig || has_capability('moodle/question:config', $systemcontext)) &&
3524                 file_exists($this->full_path('settings.php'))) {
3525             $settings = new admin_settingpage($section, $this->displayname,
3526                     'moodle/question:config', $this->is_enabled() === false);
3527             include($this->full_path('settings.php')); // this may also set $settings to null
3528         }
3529         if ($settings) {
3530             $ADMIN->add($parentnodename, $settings);
3531         }
3532     }
3536 /**
3537  * Class for authentication plugins
3538  */
3539 class plugininfo_auth extends plugininfo_base {
3541     public function is_enabled() {
3542         global $CFG;
3544         if (in_array($this->name, array('nologin', 'manual'))) {
3545             // these two are always enabled and can't be disabled
3546             return null;
3547         }
3549         $enabled = array_flip(explode(',', $CFG->auth));
3551         return isset($enabled[$this->name]);
3552     }
3554     public function get_settings_section_name() {
3555         return 'authsetting' . $this->name;
3556     }
3558     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3559         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3560         $ADMIN = $adminroot; // may be used in settings.php
3561         $auth = $this; // also to be used inside settings.php
3562         $section = $this->get_settings_section_name();
3564         $settings = null;
3565         if ($hassiteconfig) {
3566             if (file_exists($this->full_path('settings.php'))) {
3567                 // TODO: finish implementation of common settings - locking, etc.
3568                 $settings = new admin_settingpage($section, $this->displayname,
3569                         'moodle/site:config', $this->is_enabled() === false);
3570                 include($this->full_path('settings.php')); // this may also set $settings to null
3571             } else {
3572                 $settingsurl = new moodle_url('/admin/auth_config.php', array('auth' => $this->name));
3573                 $settings = new admin_externalpage($section, $this->displayname,
3574                         $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
3575             }
3576         }
3577         if ($settings) {
3578             $ADMIN->add($parentnodename, $settings);
3579         }
3580     }
3584 /**
3585  * Class for enrolment plugins
3586  */
3587 class plugininfo_enrol extends plugininfo_base {
3589     public function is_enabled() {
3590         global $CFG;
3592         // We do not actually need whole enrolment classes here so we do not call
3593         // {@link enrol_get_plugins()}. Note that this may produce slightly different
3594         // results, for example if the enrolment plugin does not contain lib.php
3595         // but it is listed in $CFG->enrol_plugins_enabled