MDL-39358 Mark the new theme_clean as a standard plugin
[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(array('mod', 'editor') as $type) {
147                 foreach (get_plugin_list($type) as $dir) {
148                     if (file_exists("$dir/adminlib.php")) {
149                         include_once("$dir/adminlib.php");
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      * At the moment, only activity modules and editors can define subplugins.
225      *
226      * @param bool $disablecache force reload, cache can be used otherwise
227      * @return array with keys like 'mod_quiz', and values the data from the
228      *      corresponding db/subplugins.php file.
229      */
230     public function get_subplugins($disablecache=false) {
232         if ($disablecache or is_null($this->subpluginsinfo)) {
233             $this->subpluginsinfo = array();
234             foreach (array('mod', 'editor') as $type) {
235                 $owners = get_plugin_list($type);
236                 foreach ($owners as $component => $ownerdir) {
237                     $componentsubplugins = array();
238                     if (file_exists($ownerdir . '/db/subplugins.php')) {
239                         $subplugins = array();
240                         include($ownerdir . '/db/subplugins.php');
241                         foreach ($subplugins as $subplugintype => $subplugintyperootdir) {
242                             $subplugin = new stdClass();
243                             $subplugin->type = $subplugintype;
244                             $subplugin->typerootdir = $subplugintyperootdir;
245                             $componentsubplugins[$subplugintype] = $subplugin;
246                         }
247                         $this->subpluginsinfo[$type . '_' . $component] = $componentsubplugins;
248                     }
249                 }
250             }
251         }
253         return $this->subpluginsinfo;
254     }
256     /**
257      * Returns the name of the plugin that defines the given subplugin type
258      *
259      * If the given subplugin type is not actually a subplugin, returns false.
260      *
261      * @param string $subplugintype the name of subplugin type, eg. workshopform or quiz
262      * @return false|string the name of the parent plugin, eg. mod_workshop
263      */
264     public function get_parent_of_subplugin($subplugintype) {
266         $parent = false;
267         foreach ($this->get_subplugins() as $pluginname => $subplugintypes) {
268             if (isset($subplugintypes[$subplugintype])) {
269                 $parent = $pluginname;
270                 break;
271             }
272         }
274         return $parent;
275     }
277     /**
278      * Returns a localized name of a given plugin
279      *
280      * @param string $component name of the plugin, eg mod_workshop or auth_ldap
281      * @return string
282      */
283     public function plugin_name($component) {
285         $pluginfo = $this->get_plugin_info($component);
287         if (is_null($pluginfo)) {
288             throw new moodle_exception('err_unknown_plugin', 'core_plugin', '', array('plugin' => $component));
289         }
291         return $pluginfo->displayname;
292     }
294     /**
295      * Returns a localized name of a plugin typed in singular form
296      *
297      * Most plugin types define their names in core_plugin lang file. In case of subplugins,
298      * we try to ask the parent plugin for the name. In the worst case, we will return
299      * the value of the passed $type parameter.
300      *
301      * @param string $type the type of the plugin, e.g. mod or workshopform
302      * @return string
303      */
304     public function plugintype_name($type) {
306         if (get_string_manager()->string_exists('type_' . $type, 'core_plugin')) {
307             // for most plugin types, their names are defined in core_plugin lang file
308             return get_string('type_' . $type, 'core_plugin');
310         } else if ($parent = $this->get_parent_of_subplugin($type)) {
311             // if this is a subplugin, try to ask the parent plugin for the name
312             if (get_string_manager()->string_exists('subplugintype_' . $type, $parent)) {
313                 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type, $parent);
314             } else {
315                 return $this->plugin_name($parent) . ' / ' . $type;
316             }
318         } else {
319             return $type;
320         }
321     }
323     /**
324      * Returns a localized name of a plugin type in plural form
325      *
326      * Most plugin types define their names in core_plugin lang file. In case of subplugins,
327      * we try to ask the parent plugin for the name. In the worst case, we will return
328      * the value of the passed $type parameter.
329      *
330      * @param string $type the type of the plugin, e.g. mod or workshopform
331      * @return string
332      */
333     public function plugintype_name_plural($type) {
335         if (get_string_manager()->string_exists('type_' . $type . '_plural', 'core_plugin')) {
336             // for most plugin types, their names are defined in core_plugin lang file
337             return get_string('type_' . $type . '_plural', 'core_plugin');
339         } else if ($parent = $this->get_parent_of_subplugin($type)) {
340             // if this is a subplugin, try to ask the parent plugin for the name
341             if (get_string_manager()->string_exists('subplugintype_' . $type . '_plural', $parent)) {
342                 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type . '_plural', $parent);
343             } else {
344                 return $this->plugin_name($parent) . ' / ' . $type;
345             }
347         } else {
348             return $type;
349         }
350     }
352     /**
353      * Returns information about the known plugin, or null
354      *
355      * @param string $component frankenstyle component name.
356      * @param bool $disablecache force reload, cache can be used otherwise
357      * @return plugininfo_base|null the corresponding plugin information.
358      */
359     public function get_plugin_info($component, $disablecache=false) {
360         list($type, $name) = $this->normalize_component($component);
361         $plugins = $this->get_plugins($disablecache);
362         if (isset($plugins[$type][$name])) {
363             return $plugins[$type][$name];
364         } else {
365             return null;
366         }
367     }
369     /**
370      * Check to see if the current version of the plugin seems to be a checkout of an external repository.
371      *
372      * @see available_update_deployer::plugin_external_source()
373      * @param string $component frankenstyle component name
374      * @return false|string
375      */
376     public function plugin_external_source($component) {
378         $plugininfo = $this->get_plugin_info($component);
380         if (is_null($plugininfo)) {
381             return false;
382         }
384         $pluginroot = $plugininfo->rootdir;
386         if (is_dir($pluginroot.'/.git')) {
387             return 'git';
388         }
390         if (is_dir($pluginroot.'/CVS')) {
391             return 'cvs';
392         }
394         if (is_dir($pluginroot.'/.svn')) {
395             return 'svn';
396         }
398         return false;
399     }
401     /**
402      * Get a list of any other plugins that require this one.
403      * @param string $component frankenstyle component name.
404      * @return array of frankensyle component names that require this one.
405      */
406     public function other_plugins_that_require($component) {
407         $others = array();
408         foreach ($this->get_plugins() as $type => $plugins) {
409             foreach ($plugins as $plugin) {
410                 $required = $plugin->get_other_required_plugins();
411                 if (isset($required[$component])) {
412                     $others[] = $plugin->component;
413                 }
414             }
415         }
416         return $others;
417     }
419     /**
420      * Check a dependencies list against the list of installed plugins.
421      * @param array $dependencies compenent name to required version or ANY_VERSION.
422      * @return bool true if all the dependencies are satisfied.
423      */
424     public function are_dependencies_satisfied($dependencies) {
425         foreach ($dependencies as $component => $requiredversion) {
426             $otherplugin = $this->get_plugin_info($component);
427             if (is_null($otherplugin)) {
428                 return false;
429             }
431             if ($requiredversion != ANY_VERSION and $otherplugin->versiondisk < $requiredversion) {
432                 return false;
433             }
434         }
436         return true;
437     }
439     /**
440      * Checks all dependencies for all installed plugins
441      *
442      * This is used by install and upgrade. The array passed by reference as the second
443      * argument is populated with the list of plugins that have failed dependencies (note that
444      * a single plugin can appear multiple times in the $failedplugins).
445      *
446      * @param int $moodleversion the version from version.php.
447      * @param array $failedplugins to return the list of plugins with non-satisfied dependencies
448      * @return bool true if all the dependencies are satisfied for all plugins.
449      */
450     public function all_plugins_ok($moodleversion, &$failedplugins = array()) {
452         $return = true;
453         foreach ($this->get_plugins() as $type => $plugins) {
454             foreach ($plugins as $plugin) {
456                 if (!$plugin->is_core_dependency_satisfied($moodleversion)) {
457                     $return = false;
458                     $failedplugins[] = $plugin->component;
459                 }
461                 if (!$this->are_dependencies_satisfied($plugin->get_other_required_plugins())) {
462                     $return = false;
463                     $failedplugins[] = $plugin->component;
464                 }
465             }
466         }
468         return $return;
469     }
471     /**
472      * Is it possible to uninstall the given plugin?
473      *
474      * False is returned if the plugininfo subclass declares the uninstall should
475      * not be allowed via {@link plugininfo_base::is_uninstall_allowed()} or if the
476      * core vetoes it (e.g. becase the plugin or some of its subplugins is required
477      * by some other installed plugin).
478      *
479      * @param string $component full frankenstyle name, e.g. mod_foobar
480      * @return bool
481      */
482     public function can_uninstall_plugin($component) {
484         $pluginfo = $this->get_plugin_info($component);
486         if (is_null($pluginfo)) {
487             return false;
488         }
490         if (!$this->common_uninstall_check($pluginfo)) {
491             return false;
492         }
494         // If it has subplugins, check they can be uninstalled too.
495         $subplugins = $this->get_subplugins_of_plugin($pluginfo->component);
496         foreach ($subplugins as $subpluginfo) {
497             if (!$this->common_uninstall_check($subpluginfo)) {
498                 return false;
499             }
500             // Check if there are some other plugins requiring this subplugin
501             // (but the parent and siblings).
502             foreach ($this->other_plugins_that_require($subpluginfo->component) as $requiresme) {
503                 $ismyparent = ($pluginfo->component === $requiresme);
504                 $ismysibling = in_array($requiresme, array_keys($subplugins));
505                 if (!$ismyparent and !$ismysibling) {
506                     return false;
507                 }
508             }
509         }
511         // Check if there are some other plugins requiring this plugin
512         // (but its subplugins).
513         foreach ($this->other_plugins_that_require($pluginfo->component) as $requiresme) {
514             $ismysubplugin = in_array($requiresme, array_keys($subplugins));
515             if (!$ismysubplugin) {
516                 return false;
517             }
518         }
520         return true;
521     }
523     /**
524      * Uninstall the given plugin.
525      *
526      * Automatically cleans-up all remaining configuration data, log records, events,
527      * files from the file pool etc.
528      *
529      * In the future, the functionality of {@link uninstall_plugin()} function may be moved
530      * into this method and all the code should be refactored to use it. At the moment, we
531      * mimic this future behaviour by wrapping that function call.
532      *
533      * @param string $component
534      * @param progress_trace $progress traces the process
535      * @return bool true on success, false on errors/problems
536      */
537     public function uninstall_plugin($component, progress_trace $progress) {
539         $pluginfo = $this->get_plugin_info($component);
541         if (is_null($pluginfo)) {
542             return false;
543         }
545         // Give the pluginfo class a chance to execute some steps.
546         $result = $pluginfo->uninstall($progress);
547         if (!$result) {
548             return false;
549         }
551         // Call the legacy core function to uninstall the plugin.
552         ob_start();
553         uninstall_plugin($pluginfo->type, $pluginfo->name);
554         $progress->output(ob_get_clean());
556         return true;
557     }
559     /**
560      * Checks if there are some plugins with a known available update
561      *
562      * @return bool true if there is at least one available update
563      */
564     public function some_plugins_updatable() {
565         foreach ($this->get_plugins() as $type => $plugins) {
566             foreach ($plugins as $plugin) {
567                 if ($plugin->available_updates()) {
568                     return true;
569                 }
570             }
571         }
573         return false;
574     }
576     /**
577      * Check to see if the given plugin folder can be removed by the web server process.
578      *
579      * @param string $component full frankenstyle component
580      * @return bool
581      */
582     public function is_plugin_folder_removable($component) {
584         $pluginfo = $this->get_plugin_info($component);
586         if (is_null($pluginfo)) {
587             return false;
588         }
590         // To be able to remove the plugin folder, its parent must be writable, too.
591         if (!is_writable(dirname($pluginfo->rootdir))) {
592             return false;
593         }
595         // Check that the folder and all its content is writable (thence removable).
596         return $this->is_directory_removable($pluginfo->rootdir);
597     }
599     /**
600      * Defines a list of all plugins that were originally shipped in the standard Moodle distribution,
601      * but are not anymore and are deleted during upgrades.
602      *
603      * The main purpose of this list is to hide missing plugins during upgrade.
604      *
605      * @param string $type plugin type
606      * @param string $name plugin name
607      * @return bool
608      */
609     public static function is_deleted_standard_plugin($type, $name) {
611         // Example of the array structure:
612         // $plugins = array(
613         //     'block' => array('admin', 'admin_tree'),
614         //     'mod' => array('assignment'),
615         // );
616         // Do not include plugins that were removed during upgrades to versions that are
617         // not supported as source versions for upgrade any more. For example, at MOODLE_23_STABLE
618         // branch, listed should be no plugins that were removed at 1.9.x - 2.1.x versions as
619         // Moodle 2.3 supports upgrades from 2.2.x only.
620         $plugins = array(
621             'qformat' => array('blackboard'),
622         );
624         if (!isset($plugins[$type])) {
625             return false;
626         }
627         return in_array($name, $plugins[$type]);
628     }
630     /**
631      * Defines a white list of all plugins shipped in the standard Moodle distribution
632      *
633      * @param string $type
634      * @return false|array array of standard plugins or false if the type is unknown
635      */
636     public static function standard_plugins_list($type) {
637         $standard_plugins = array(
639             'assignment' => array(
640                 'offline', 'online', 'upload', 'uploadsingle'
641             ),
643             'assignsubmission' => array(
644                 'comments', 'file', 'onlinetext'
645             ),
647             'assignfeedback' => array(
648                 'comments', 'file', 'offline'
649             ),
651             'auth' => array(
652                 'cas', 'db', 'email', 'fc', 'imap', 'ldap', 'manual', 'mnet',
653                 'nntp', 'nologin', 'none', 'pam', 'pop3', 'radius',
654                 'shibboleth', 'webservice'
655             ),
657             'block' => array(
658                 'activity_modules', 'admin_bookmarks', 'badges', 'blog_menu',
659                 'blog_recent', 'blog_tags', 'calendar_month',
660                 'calendar_upcoming', 'comments', 'community',
661                 'completionstatus', 'course_list', 'course_overview',
662                 'course_summary', 'feedback', 'glossary_random', 'html',
663                 'login', 'mentees', 'messages', 'mnet_hosts', 'myprofile',
664                 'navigation', 'news_items', 'online_users', 'participants',
665                 'private_files', 'quiz_results', 'recent_activity',
666                 'rss_client', 'search_forums', 'section_links',
667                 'selfcompletion', 'settings', 'site_main_menu',
668                 'social_activities', 'tag_flickr', 'tag_youtube', 'tags'
669             ),
671             'booktool' => array(
672                 'exportimscp', 'importhtml', 'print'
673             ),
675             'cachelock' => array(
676                 'file'
677             ),
679             'cachestore' => array(
680                 'file', 'memcache', 'memcached', 'mongodb', 'session', 'static'
681             ),
683             'coursereport' => array(
684                 //deprecated!
685             ),
687             'datafield' => array(
688                 'checkbox', 'date', 'file', 'latlong', 'menu', 'multimenu',
689                 'number', 'picture', 'radiobutton', 'text', 'textarea', 'url'
690             ),
692             'datapreset' => array(
693                 'imagegallery'
694             ),
696             'editor' => array(
697                 'textarea', 'tinymce'
698             ),
700             'enrol' => array(
701                 'authorize', 'category', 'cohort', 'database', 'flatfile',
702                 'guest', 'imsenterprise', 'ldap', 'manual', 'meta', 'mnet',
703                 'paypal', 'self'
704             ),
706             'filter' => array(
707                 'activitynames', 'algebra', 'censor', 'emailprotect',
708                 'emoticon', 'mediaplugin', 'multilang', 'tex', 'tidy',
709                 'urltolink', 'data', 'glossary'
710             ),
712             'format' => array(
713                 'scorm', 'social', 'topics', 'weeks'
714             ),
716             'gradeexport' => array(
717                 'ods', 'txt', 'xls', 'xml'
718             ),
720             'gradeimport' => array(
721                 'csv', 'xml'
722             ),
724             'gradereport' => array(
725                 'grader', 'outcomes', 'overview', 'user'
726             ),
728             'gradingform' => array(
729                 'rubric', 'guide'
730             ),
732             'local' => array(
733             ),
735             'message' => array(
736                 'email', 'jabber', 'popup'
737             ),
739             'mnetservice' => array(
740                 'enrol'
741             ),
743             'mod' => array(
744                 'assign', 'assignment', 'book', 'chat', 'choice', 'data', 'feedback', 'folder',
745                 'forum', 'glossary', 'imscp', 'label', 'lesson', 'lti', 'page',
746                 'quiz', 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop'
747             ),
749             'plagiarism' => array(
750             ),
752             'portfolio' => array(
753                 'boxnet', 'download', 'flickr', 'googledocs', 'mahara', 'picasa'
754             ),
756             'profilefield' => array(
757                 'checkbox', 'datetime', 'menu', 'text', 'textarea'
758             ),
760             'qbehaviour' => array(
761                 'adaptive', 'adaptivenopenalty', 'deferredcbm',
762                 'deferredfeedback', 'immediatecbm', 'immediatefeedback',
763                 'informationitem', 'interactive', 'interactivecountback',
764                 'manualgraded', 'missing'
765             ),
767             'qformat' => array(
768                 'aiken', 'blackboard_six', 'examview', 'gift',
769                 'learnwise', 'missingword', 'multianswer', 'webct',
770                 'xhtml', 'xml'
771             ),
773             'qtype' => array(
774                 'calculated', 'calculatedmulti', 'calculatedsimple',
775                 'description', 'essay', 'match', 'missingtype', 'multianswer',
776                 'multichoice', 'numerical', 'random', 'randomsamatch',
777                 'shortanswer', 'truefalse'
778             ),
780             'quiz' => array(
781                 'grading', 'overview', 'responses', 'statistics'
782             ),
784             'quizaccess' => array(
785                 'delaybetweenattempts', 'ipaddress', 'numattempts', 'openclosedate',
786                 'password', 'safebrowser', 'securewindow', 'timelimit'
787             ),
789             'report' => array(
790                 'backups', 'completion', 'configlog', 'courseoverview',
791                 'log', 'loglive', 'outline', 'participation', 'progress', 'questioninstances', 'security', 'stats', 'performance'
792             ),
794             'repository' => array(
795                 'alfresco', 'boxnet', 'coursefiles', 'dropbox', 'equella', 'filesystem',
796                 'flickr', 'flickr_public', 'googledocs', 'local', 'merlot',
797                 'picasa', 'recent', 's3', 'upload', 'url', 'user', 'webdav',
798                 'wikimedia', 'youtube'
799             ),
801             'scormreport' => array(
802                 'basic',
803                 'interactions',
804                 'graphs'
805             ),
807             'tinymce' => array(
808                 'ctrlhelp', 'dragmath', 'moodleemoticon', 'moodleimage', 'moodlemedia', 'moodlenolink', 'spellchecker',
809             ),
811             'theme' => array(
812                 'afterburner', 'anomaly', 'arialist', 'base', 'binarius', 'bootstrap',
813                 'boxxie', 'brick', 'canvas', 'clean', 'formal_white', 'formfactor',
814                 'fusion', 'leatherbound', 'magazine', 'mymobile', 'nimble',
815                 'nonzero', 'overlay', 'serenity', 'sky_high', 'splash',
816                 'standard', 'standardold'
817             ),
819             'tool' => array(
820                 'assignmentupgrade', 'behat', 'capability', 'customlang',
821                 'dbtransfer', 'generator', 'health', 'innodb', 'installaddon',
822                 'langimport', 'multilangupgrade', 'phpunit', 'profiling',
823                 'qeupgradehelper', 'replace', 'spamcleaner', 'timezoneimport',
824                 'unittest', 'uploaduser', 'unsuproles', 'xmldb'
825             ),
827             'webservice' => array(
828                 'amf', 'rest', 'soap', 'xmlrpc'
829             ),
831             'workshopallocation' => array(
832                 'manual', 'random', 'scheduled'
833             ),
835             'workshopeval' => array(
836                 'best'
837             ),
839             'workshopform' => array(
840                 'accumulative', 'comments', 'numerrors', 'rubric'
841             )
842         );
844         if (isset($standard_plugins[$type])) {
845             return $standard_plugins[$type];
846         } else {
847             return false;
848         }
849     }
851     /**
852      * Wrapper for the core function {@link normalize_component()}.
853      *
854      * This is here just to make it possible to mock it in unit tests.
855      *
856      * @param string $component
857      * @return array
858      */
859     protected function normalize_component($component) {
860         return normalize_component($component);
861     }
863     /**
864      * Reorders plugin types into a sequence to be displayed
865      *
866      * For technical reasons, plugin types returned by {@link get_plugin_types()} are
867      * in a certain order that does not need to fit the expected order for the display.
868      * Particularly, activity modules should be displayed first as they represent the
869      * real heart of Moodle. They should be followed by other plugin types that are
870      * used to build the courses (as that is what one expects from LMS). After that,
871      * other supportive plugin types follow.
872      *
873      * @param array $types associative array
874      * @return array same array with altered order of items
875      */
876     protected function reorder_plugin_types(array $types) {
877         $fix = array(
878             'mod'        => $types['mod'],
879             'block'      => $types['block'],
880             'qtype'      => $types['qtype'],
881             'qbehaviour' => $types['qbehaviour'],
882             'qformat'    => $types['qformat'],
883             'filter'     => $types['filter'],
884             'enrol'      => $types['enrol'],
885         );
886         foreach ($types as $type => $path) {
887             if (!isset($fix[$type])) {
888                 $fix[$type] = $path;
889             }
890         }
891         return $fix;
892     }
894     /**
895      * Check if the given directory can be removed by the web server process.
896      *
897      * This recursively checks that the given directory and all its contents
898      * it writable.
899      *
900      * @param string $fullpath
901      * @return boolean
902      */
903     protected function is_directory_removable($fullpath) {
905         if (!is_writable($fullpath)) {
906             return false;
907         }
909         if (is_dir($fullpath)) {
910             $handle = opendir($fullpath);
911         } else {
912             return false;
913         }
915         $result = true;
917         while ($filename = readdir($handle)) {
919             if ($filename === '.' or $filename === '..') {
920                 continue;
921             }
923             $subfilepath = $fullpath.'/'.$filename;
925             if (is_dir($subfilepath)) {
926                 $result = $result && $this->is_directory_removable($subfilepath);
928             } else {
929                 $result = $result && is_writable($subfilepath);
930             }
931         }
933         closedir($handle);
935         return $result;
936     }
938     /**
939      * Helper method that implements common uninstall prerequisities
940      *
941      * @param plugininfo_base $pluginfo
942      * @return bool
943      */
944     protected function common_uninstall_check(plugininfo_base $pluginfo) {
946         if (!$pluginfo->is_uninstall_allowed()) {
947             // The plugin's plugininfo class declares it should not be uninstalled.
948             return false;
949         }
951         if ($pluginfo->get_status() === plugin_manager::PLUGIN_STATUS_NEW) {
952             // The plugin is not installed. It should be either installed or removed from the disk.
953             // Relying on this temporary state may be tricky.
954             return false;
955         }
957         if (is_null($pluginfo->get_uninstall_url())) {
958             // Backwards compatibility.
959             debugging('plugininfo_base subclasses should use is_uninstall_allowed() instead of returning null in get_uninstall_url()',
960                 DEBUG_DEVELOPER);
961             return false;
962         }
964         return true;
965     }
969 /**
970  * General exception thrown by the {@link available_update_checker} class
971  */
972 class available_update_checker_exception extends moodle_exception {
974     /**
975      * @param string $errorcode exception description identifier
976      * @param mixed $debuginfo debugging data to display
977      */
978     public function __construct($errorcode, $debuginfo=null) {
979         parent::__construct($errorcode, 'core_plugin', '', null, print_r($debuginfo, true));
980     }
984 /**
985  * Singleton class that handles checking for available updates
986  */
987 class available_update_checker {
989     /** @var available_update_checker holds the singleton instance */
990     protected static $singletoninstance;
991     /** @var null|int the timestamp of when the most recent response was fetched */
992     protected $recentfetch = null;
993     /** @var null|array the recent response from the update notification provider */
994     protected $recentresponse = null;
995     /** @var null|string the numerical version of the local Moodle code */
996     protected $currentversion = null;
997     /** @var null|string the release info of the local Moodle code */
998     protected $currentrelease = null;
999     /** @var null|string branch of the local Moodle code */
1000     protected $currentbranch = null;
1001     /** @var array of (string)frankestyle => (string)version list of additional plugins deployed at this site */
1002     protected $currentplugins = array();
1004     /**
1005      * Direct initiation not allowed, use the factory method {@link self::instance()}
1006      */
1007     protected function __construct() {
1008     }
1010     /**
1011      * Sorry, this is singleton
1012      */
1013     protected function __clone() {
1014     }
1016     /**
1017      * Factory method for this class
1018      *
1019      * @return available_update_checker the singleton instance
1020      */
1021     public static function instance() {
1022         if (is_null(self::$singletoninstance)) {
1023             self::$singletoninstance = new self();
1024         }
1025         return self::$singletoninstance;
1026     }
1028     /**
1029      * Reset any caches
1030      * @param bool $phpunitreset
1031      */
1032     public static function reset_caches($phpunitreset = false) {
1033         if ($phpunitreset) {
1034             self::$singletoninstance = null;
1035         }
1036     }
1038     /**
1039      * Returns the timestamp of the last execution of {@link fetch()}
1040      *
1041      * @return int|null null if it has never been executed or we don't known
1042      */
1043     public function get_last_timefetched() {
1045         $this->restore_response();
1047         if (!empty($this->recentfetch)) {
1048             return $this->recentfetch;
1050         } else {
1051             return null;
1052         }
1053     }
1055     /**
1056      * Fetches the available update status from the remote site
1057      *
1058      * @throws available_update_checker_exception
1059      */
1060     public function fetch() {
1061         $response = $this->get_response();
1062         $this->validate_response($response);
1063         $this->store_response($response);
1064     }
1066     /**
1067      * Returns the available update information for the given component
1068      *
1069      * This method returns null if the most recent response does not contain any information
1070      * about it. The returned structure is an array of available updates for the given
1071      * component. Each update info is an object with at least one property called
1072      * 'version'. Other possible properties are 'release', 'maturity', 'url' and 'downloadurl'.
1073      *
1074      * For the 'core' component, the method returns real updates only (those with higher version).
1075      * For all other components, the list of all known remote updates is returned and the caller
1076      * (usually the {@link plugin_manager}) is supposed to make the actual comparison of versions.
1077      *
1078      * @param string $component frankenstyle
1079      * @param array $options with supported keys 'minmaturity' and/or 'notifybuilds'
1080      * @return null|array null or array of available_update_info objects
1081      */
1082     public function get_update_info($component, array $options = array()) {
1084         if (!isset($options['minmaturity'])) {
1085             $options['minmaturity'] = 0;
1086         }
1088         if (!isset($options['notifybuilds'])) {
1089             $options['notifybuilds'] = false;
1090         }
1092         if ($component == 'core') {
1093             $this->load_current_environment();
1094         }
1096         $this->restore_response();
1098         if (empty($this->recentresponse['updates'][$component])) {
1099             return null;
1100         }
1102         $updates = array();
1103         foreach ($this->recentresponse['updates'][$component] as $info) {
1104             $update = new available_update_info($component, $info);
1105             if (isset($update->maturity) and ($update->maturity < $options['minmaturity'])) {
1106                 continue;
1107             }
1108             if ($component == 'core') {
1109                 if ($update->version <= $this->currentversion) {
1110                     continue;
1111                 }
1112                 if (empty($options['notifybuilds']) and $this->is_same_release($update->release)) {
1113                     continue;
1114                 }
1115             }
1116             $updates[] = $update;
1117         }
1119         if (empty($updates)) {
1120             return null;
1121         }
1123         return $updates;
1124     }
1126     /**
1127      * The method being run via cron.php
1128      */
1129     public function cron() {
1130         global $CFG;
1132         if (!$this->cron_autocheck_enabled()) {
1133             $this->cron_mtrace('Automatic check for available updates not enabled, skipping.');
1134             return;
1135         }
1137         $now = $this->cron_current_timestamp();
1139         if ($this->cron_has_fresh_fetch($now)) {
1140             $this->cron_mtrace('Recently fetched info about available updates is still fresh enough, skipping.');
1141             return;
1142         }
1144         if ($this->cron_has_outdated_fetch($now)) {
1145             $this->cron_mtrace('Outdated or missing info about available updates, forced fetching ... ', '');
1146             $this->cron_execute();
1147             return;
1148         }
1150         $offset = $this->cron_execution_offset();
1151         $start = mktime(1, 0, 0, date('n', $now), date('j', $now), date('Y', $now)); // 01:00 AM today local time
1152         if ($now > $start + $offset) {
1153             $this->cron_mtrace('Regular daily check for available updates ... ', '');
1154             $this->cron_execute();
1155             return;
1156         }
1157     }
1159     /// end of public API //////////////////////////////////////////////////////
1161     /**
1162      * Makes cURL request to get data from the remote site
1163      *
1164      * @return string raw request result
1165      * @throws available_update_checker_exception
1166      */
1167     protected function get_response() {
1168         global $CFG;
1169         require_once($CFG->libdir.'/filelib.php');
1171         $curl = new curl(array('proxy' => true));
1172         $response = $curl->post($this->prepare_request_url(), $this->prepare_request_params(), $this->prepare_request_options());
1173         $curlerrno = $curl->get_errno();
1174         if (!empty($curlerrno)) {
1175             throw new available_update_checker_exception('err_response_curl', 'cURL error '.$curlerrno.': '.$curl->error);
1176         }
1177         $curlinfo = $curl->get_info();
1178         if ($curlinfo['http_code'] != 200) {
1179             throw new available_update_checker_exception('err_response_http_code', $curlinfo['http_code']);
1180         }
1181         return $response;
1182     }
1184     /**
1185      * Makes sure the response is valid, has correct API format etc.
1186      *
1187      * @param string $response raw response as returned by the {@link self::get_response()}
1188      * @throws available_update_checker_exception
1189      */
1190     protected function validate_response($response) {
1192         $response = $this->decode_response($response);
1194         if (empty($response)) {
1195             throw new available_update_checker_exception('err_response_empty');
1196         }
1198         if (empty($response['status']) or $response['status'] !== 'OK') {
1199             throw new available_update_checker_exception('err_response_status', $response['status']);
1200         }
1202         if (empty($response['apiver']) or $response['apiver'] !== '1.2') {
1203             throw new available_update_checker_exception('err_response_format_version', $response['apiver']);
1204         }
1206         if (empty($response['forbranch']) or $response['forbranch'] !== moodle_major_version(true)) {
1207             throw new available_update_checker_exception('err_response_target_version', $response['forbranch']);
1208         }
1209     }
1211     /**
1212      * Decodes the raw string response from the update notifications provider
1213      *
1214      * @param string $response as returned by {@link self::get_response()}
1215      * @return array decoded response structure
1216      */
1217     protected function decode_response($response) {
1218         return json_decode($response, true);
1219     }
1221     /**
1222      * Stores the valid fetched response for later usage
1223      *
1224      * This implementation uses the config_plugins table as the permanent storage.
1225      *
1226      * @param string $response raw valid data returned by {@link self::get_response()}
1227      */
1228     protected function store_response($response) {
1230         set_config('recentfetch', time(), 'core_plugin');
1231         set_config('recentresponse', $response, 'core_plugin');
1233         $this->restore_response(true);
1234     }
1236     /**
1237      * Loads the most recent raw response record we have fetched
1238      *
1239      * After this method is called, $this->recentresponse is set to an array. If the
1240      * array is empty, then either no data have been fetched yet or the fetched data
1241      * do not have expected format (and thence they are ignored and a debugging
1242      * message is displayed).
1243      *
1244      * This implementation uses the config_plugins table as the permanent storage.
1245      *
1246      * @param bool $forcereload reload even if it was already loaded
1247      */
1248     protected function restore_response($forcereload = false) {
1250         if (!$forcereload and !is_null($this->recentresponse)) {
1251             // we already have it, nothing to do
1252             return;
1253         }
1255         $config = get_config('core_plugin');
1257         if (!empty($config->recentresponse) and !empty($config->recentfetch)) {
1258             try {
1259                 $this->validate_response($config->recentresponse);
1260                 $this->recentfetch = $config->recentfetch;
1261                 $this->recentresponse = $this->decode_response($config->recentresponse);
1262             } catch (available_update_checker_exception $e) {
1263                 // The server response is not valid. Behave as if no data were fetched yet.
1264                 // This may happen when the most recent update info (cached locally) has been
1265                 // fetched with the previous branch of Moodle (like during an upgrade from 2.x
1266                 // to 2.y) or when the API of the response has changed.
1267                 $this->recentresponse = array();
1268             }
1270         } else {
1271             $this->recentresponse = array();
1272         }
1273     }
1275     /**
1276      * Compares two raw {@link $recentresponse} records and returns the list of changed updates
1277      *
1278      * This method is used to populate potential update info to be sent to site admins.
1279      *
1280      * @param array $old
1281      * @param array $new
1282      * @throws available_update_checker_exception
1283      * @return array parts of $new['updates'] that have changed
1284      */
1285     protected function compare_responses(array $old, array $new) {
1287         if (empty($new)) {
1288             return array();
1289         }
1291         if (!array_key_exists('updates', $new)) {
1292             throw new available_update_checker_exception('err_response_format');
1293         }
1295         if (empty($old)) {
1296             return $new['updates'];
1297         }
1299         if (!array_key_exists('updates', $old)) {
1300             throw new available_update_checker_exception('err_response_format');
1301         }
1303         $changes = array();
1305         foreach ($new['updates'] as $newcomponent => $newcomponentupdates) {
1306             if (empty($old['updates'][$newcomponent])) {
1307                 $changes[$newcomponent] = $newcomponentupdates;
1308                 continue;
1309             }
1310             foreach ($newcomponentupdates as $newcomponentupdate) {
1311                 $inold = false;
1312                 foreach ($old['updates'][$newcomponent] as $oldcomponentupdate) {
1313                     if ($newcomponentupdate['version'] == $oldcomponentupdate['version']) {
1314                         $inold = true;
1315                     }
1316                 }
1317                 if (!$inold) {
1318                     if (!isset($changes[$newcomponent])) {
1319                         $changes[$newcomponent] = array();
1320                     }
1321                     $changes[$newcomponent][] = $newcomponentupdate;
1322                 }
1323             }
1324         }
1326         return $changes;
1327     }
1329     /**
1330      * Returns the URL to send update requests to
1331      *
1332      * During the development or testing, you can set $CFG->alternativeupdateproviderurl
1333      * to a custom URL that will be used. Otherwise the standard URL will be returned.
1334      *
1335      * @return string URL
1336      */
1337     protected function prepare_request_url() {
1338         global $CFG;
1340         if (!empty($CFG->config_php_settings['alternativeupdateproviderurl'])) {
1341             return $CFG->config_php_settings['alternativeupdateproviderurl'];
1342         } else {
1343             return 'https://download.moodle.org/api/1.2/updates.php';
1344         }
1345     }
1347     /**
1348      * Sets the properties currentversion, currentrelease, currentbranch and currentplugins
1349      *
1350      * @param bool $forcereload
1351      */
1352     protected function load_current_environment($forcereload=false) {
1353         global $CFG;
1355         if (!is_null($this->currentversion) and !$forcereload) {
1356             // nothing to do
1357             return;
1358         }
1360         $version = null;
1361         $release = null;
1363         require($CFG->dirroot.'/version.php');
1364         $this->currentversion = $version;
1365         $this->currentrelease = $release;
1366         $this->currentbranch = moodle_major_version(true);
1368         $pluginman = plugin_manager::instance();
1369         foreach ($pluginman->get_plugins() as $type => $plugins) {
1370             foreach ($plugins as $plugin) {
1371                 if (!$plugin->is_standard()) {
1372                     $this->currentplugins[$plugin->component] = $plugin->versiondisk;
1373                 }
1374             }
1375         }
1376     }
1378     /**
1379      * Returns the list of HTTP params to be sent to the updates provider URL
1380      *
1381      * @return array of (string)param => (string)value
1382      */
1383     protected function prepare_request_params() {
1384         global $CFG;
1386         $this->load_current_environment();
1387         $this->restore_response();
1389         $params = array();
1390         $params['format'] = 'json';
1392         if (isset($this->recentresponse['ticket'])) {
1393             $params['ticket'] = $this->recentresponse['ticket'];
1394         }
1396         if (isset($this->currentversion)) {
1397             $params['version'] = $this->currentversion;
1398         } else {
1399             throw new coding_exception('Main Moodle version must be already known here');
1400         }
1402         if (isset($this->currentbranch)) {
1403             $params['branch'] = $this->currentbranch;
1404         } else {
1405             throw new coding_exception('Moodle release must be already known here');
1406         }
1408         $plugins = array();
1409         foreach ($this->currentplugins as $plugin => $version) {
1410             $plugins[] = $plugin.'@'.$version;
1411         }
1412         if (!empty($plugins)) {
1413             $params['plugins'] = implode(',', $plugins);
1414         }
1416         return $params;
1417     }
1419     /**
1420      * Returns the list of cURL options to use when fetching available updates data
1421      *
1422      * @return array of (string)param => (string)value
1423      */
1424     protected function prepare_request_options() {
1425         global $CFG;
1427         $options = array(
1428             'CURLOPT_SSL_VERIFYHOST' => 2,      // this is the default in {@link curl} class but just in case
1429             'CURLOPT_SSL_VERIFYPEER' => true,
1430         );
1432         $cacertfile = $CFG->dataroot.'/moodleorgca.crt';
1433         if (is_readable($cacertfile)) {
1434             // Do not use CA certs provided by the operating system. Instead,
1435             // use this CA cert to verify the updates provider.
1436             $options['CURLOPT_CAINFO'] = $cacertfile;
1437         }
1439         return $options;
1440     }
1442     /**
1443      * Returns the current timestamp
1444      *
1445      * @return int the timestamp
1446      */
1447     protected function cron_current_timestamp() {
1448         return time();
1449     }
1451     /**
1452      * Output cron debugging info
1453      *
1454      * @see mtrace()
1455      * @param string $msg output message
1456      * @param string $eol end of line
1457      */
1458     protected function cron_mtrace($msg, $eol = PHP_EOL) {
1459         mtrace($msg, $eol);
1460     }
1462     /**
1463      * Decide if the autocheck feature is disabled in the server setting
1464      *
1465      * @return bool true if autocheck enabled, false if disabled
1466      */
1467     protected function cron_autocheck_enabled() {
1468         global $CFG;
1470         if (empty($CFG->updateautocheck)) {
1471             return false;
1472         } else {
1473             return true;
1474         }
1475     }
1477     /**
1478      * Decide if the recently fetched data are still fresh enough
1479      *
1480      * @param int $now current timestamp
1481      * @return bool true if no need to re-fetch, false otherwise
1482      */
1483     protected function cron_has_fresh_fetch($now) {
1484         $recent = $this->get_last_timefetched();
1486         if (empty($recent)) {
1487             return false;
1488         }
1490         if ($now < $recent) {
1491             $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1492             return true;
1493         }
1495         if ($now - $recent > 24 * HOURSECS) {
1496             return false;
1497         }
1499         return true;
1500     }
1502     /**
1503      * Decide if the fetch is outadated or even missing
1504      *
1505      * @param int $now current timestamp
1506      * @return bool false if no need to re-fetch, true otherwise
1507      */
1508     protected function cron_has_outdated_fetch($now) {
1509         $recent = $this->get_last_timefetched();
1511         if (empty($recent)) {
1512             return true;
1513         }
1515         if ($now < $recent) {
1516             $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1517             return false;
1518         }
1520         if ($now - $recent > 48 * HOURSECS) {
1521             return true;
1522         }
1524         return false;
1525     }
1527     /**
1528      * Returns the cron execution offset for this site
1529      *
1530      * The main {@link self::cron()} is supposed to run every night in some random time
1531      * between 01:00 and 06:00 AM (local time). The exact moment is defined by so called
1532      * execution offset, that is the amount of time after 01:00 AM. The offset value is
1533      * initially generated randomly and then used consistently at the site. This way, the
1534      * regular checks against the download.moodle.org server are spread in time.
1535      *
1536      * @return int the offset number of seconds from range 1 sec to 5 hours
1537      */
1538     protected function cron_execution_offset() {
1539         global $CFG;
1541         if (empty($CFG->updatecronoffset)) {
1542             set_config('updatecronoffset', rand(1, 5 * HOURSECS));
1543         }
1545         return $CFG->updatecronoffset;
1546     }
1548     /**
1549      * Fetch available updates info and eventually send notification to site admins
1550      */
1551     protected function cron_execute() {
1553         try {
1554             $this->restore_response();
1555             $previous = $this->recentresponse;
1556             $this->fetch();
1557             $this->restore_response(true);
1558             $current = $this->recentresponse;
1559             $changes = $this->compare_responses($previous, $current);
1560             $notifications = $this->cron_notifications($changes);
1561             $this->cron_notify($notifications);
1562             $this->cron_mtrace('done');
1563         } catch (available_update_checker_exception $e) {
1564             $this->cron_mtrace('FAILED!');
1565         }
1566     }
1568     /**
1569      * Given the list of changes in available updates, pick those to send to site admins
1570      *
1571      * @param array $changes as returned by {@link self::compare_responses()}
1572      * @return array of available_update_info objects to send to site admins
1573      */
1574     protected function cron_notifications(array $changes) {
1575         global $CFG;
1577         $notifications = array();
1578         $pluginman = plugin_manager::instance();
1579         $plugins = $pluginman->get_plugins(true);
1581         foreach ($changes as $component => $componentchanges) {
1582             if (empty($componentchanges)) {
1583                 continue;
1584             }
1585             $componentupdates = $this->get_update_info($component,
1586                 array('minmaturity' => $CFG->updateminmaturity, 'notifybuilds' => $CFG->updatenotifybuilds));
1587             if (empty($componentupdates)) {
1588                 continue;
1589             }
1590             // notify only about those $componentchanges that are present in $componentupdates
1591             // to respect the preferences
1592             foreach ($componentchanges as $componentchange) {
1593                 foreach ($componentupdates as $componentupdate) {
1594                     if ($componentupdate->version == $componentchange['version']) {
1595                         if ($component == 'core') {
1596                             // In case of 'core', we already know that the $componentupdate
1597                             // is a real update with higher version ({@see self::get_update_info()}).
1598                             // We just perform additional check for the release property as there
1599                             // can be two Moodle releases having the same version (e.g. 2.4.0 and 2.5dev shortly
1600                             // after the release). We can do that because we have the release info
1601                             // always available for the core.
1602                             if ((string)$componentupdate->release === (string)$componentchange['release']) {
1603                                 $notifications[] = $componentupdate;
1604                             }
1605                         } else {
1606                             // Use the plugin_manager to check if the detected $componentchange
1607                             // is a real update with higher version. That is, the $componentchange
1608                             // is present in the array of {@link available_update_info} objects
1609                             // returned by the plugin's available_updates() method.
1610                             list($plugintype, $pluginname) = normalize_component($component);
1611                             if (!empty($plugins[$plugintype][$pluginname])) {
1612                                 $availableupdates = $plugins[$plugintype][$pluginname]->available_updates();
1613                                 if (!empty($availableupdates)) {
1614                                     foreach ($availableupdates as $availableupdate) {
1615                                         if ($availableupdate->version == $componentchange['version']) {
1616                                             $notifications[] = $componentupdate;
1617                                         }
1618                                     }
1619                                 }
1620                             }
1621                         }
1622                     }
1623                 }
1624             }
1625         }
1627         return $notifications;
1628     }
1630     /**
1631      * Sends the given notifications to site admins via messaging API
1632      *
1633      * @param array $notifications array of available_update_info objects to send
1634      */
1635     protected function cron_notify(array $notifications) {
1636         global $CFG;
1638         if (empty($notifications)) {
1639             return;
1640         }
1642         $admins = get_admins();
1644         if (empty($admins)) {
1645             return;
1646         }
1648         $this->cron_mtrace('sending notifications ... ', '');
1650         $text = get_string('updatenotifications', 'core_admin') . PHP_EOL;
1651         $html = html_writer::tag('h1', get_string('updatenotifications', 'core_admin')) . PHP_EOL;
1653         $coreupdates = array();
1654         $pluginupdates = array();
1656         foreach ($notifications as $notification) {
1657             if ($notification->component == 'core') {
1658                 $coreupdates[] = $notification;
1659             } else {
1660                 $pluginupdates[] = $notification;
1661             }
1662         }
1664         if (!empty($coreupdates)) {
1665             $text .= PHP_EOL . get_string('updateavailable', 'core_admin') . PHP_EOL;
1666             $html .= html_writer::tag('h2', get_string('updateavailable', 'core_admin')) . PHP_EOL;
1667             $html .= html_writer::start_tag('ul') . PHP_EOL;
1668             foreach ($coreupdates as $coreupdate) {
1669                 $html .= html_writer::start_tag('li');
1670                 if (isset($coreupdate->release)) {
1671                     $text .= get_string('updateavailable_release', 'core_admin', $coreupdate->release);
1672                     $html .= html_writer::tag('strong', get_string('updateavailable_release', 'core_admin', $coreupdate->release));
1673                 }
1674                 if (isset($coreupdate->version)) {
1675                     $text .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
1676                     $html .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
1677                 }
1678                 if (isset($coreupdate->maturity)) {
1679                     $text .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
1680                     $html .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
1681                 }
1682                 $text .= PHP_EOL;
1683                 $html .= html_writer::end_tag('li') . PHP_EOL;
1684             }
1685             $text .= PHP_EOL;
1686             $html .= html_writer::end_tag('ul') . PHP_EOL;
1688             $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/index.php');
1689             $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
1690             $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/index.php', $CFG->wwwroot.'/'.$CFG->admin.'/index.php'));
1691             $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
1692         }
1694         if (!empty($pluginupdates)) {
1695             $text .= PHP_EOL . get_string('updateavailableforplugin', 'core_admin') . PHP_EOL;
1696             $html .= html_writer::tag('h2', get_string('updateavailableforplugin', 'core_admin')) . PHP_EOL;
1698             $html .= html_writer::start_tag('ul') . PHP_EOL;
1699             foreach ($pluginupdates as $pluginupdate) {
1700                 $html .= html_writer::start_tag('li');
1701                 $text .= get_string('pluginname', $pluginupdate->component);
1702                 $html .= html_writer::tag('strong', get_string('pluginname', $pluginupdate->component));
1704                 $text .= ' ('.$pluginupdate->component.')';
1705                 $html .= ' ('.$pluginupdate->component.')';
1707                 $text .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
1708                 $html .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
1710                 $text .= PHP_EOL;
1711                 $html .= html_writer::end_tag('li') . PHP_EOL;
1712             }
1713             $text .= PHP_EOL;
1714             $html .= html_writer::end_tag('ul') . PHP_EOL;
1716             $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php');
1717             $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
1718             $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/plugins.php', $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php'));
1719             $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
1720         }
1722         $a = array('siteurl' => $CFG->wwwroot);
1723         $text .= get_string('updatenotificationfooter', 'core_admin', $a) . PHP_EOL;
1724         $a = array('siteurl' => html_writer::link($CFG->wwwroot, $CFG->wwwroot));
1725         $html .= html_writer::tag('footer', html_writer::tag('p', get_string('updatenotificationfooter', 'core_admin', $a),
1726             array('style' => 'font-size:smaller; color:#333;')));
1728         foreach ($admins as $admin) {
1729             $message = new stdClass();
1730             $message->component         = 'moodle';
1731             $message->name              = 'availableupdate';
1732             $message->userfrom          = get_admin();
1733             $message->userto            = $admin;
1734             $message->subject           = get_string('updatenotificationsubject', 'core_admin', array('siteurl' => $CFG->wwwroot));
1735             $message->fullmessage       = $text;
1736             $message->fullmessageformat = FORMAT_PLAIN;
1737             $message->fullmessagehtml   = $html;
1738             $message->smallmessage      = get_string('updatenotifications', 'core_admin');
1739             $message->notification      = 1;
1740             message_send($message);
1741         }
1742     }
1744     /**
1745      * Compare two release labels and decide if they are the same
1746      *
1747      * @param string $remote release info of the available update
1748      * @param null|string $local release info of the local code, defaults to $release defined in version.php
1749      * @return boolean true if the releases declare the same minor+major version
1750      */
1751     protected function is_same_release($remote, $local=null) {
1753         if (is_null($local)) {
1754             $this->load_current_environment();
1755             $local = $this->currentrelease;
1756         }
1758         $pattern = '/^([0-9\.\+]+)([^(]*)/';
1760         preg_match($pattern, $remote, $remotematches);
1761         preg_match($pattern, $local, $localmatches);
1763         $remotematches[1] = str_replace('+', '', $remotematches[1]);
1764         $localmatches[1] = str_replace('+', '', $localmatches[1]);
1766         if ($remotematches[1] === $localmatches[1] and rtrim($remotematches[2]) === rtrim($localmatches[2])) {
1767             return true;
1768         } else {
1769             return false;
1770         }
1771     }
1775 /**
1776  * Defines the structure of objects returned by {@link available_update_checker::get_update_info()}
1777  */
1778 class available_update_info {
1780     /** @var string frankenstyle component name */
1781     public $component;
1782     /** @var int the available version of the component */
1783     public $version;
1784     /** @var string|null optional release name */
1785     public $release = null;
1786     /** @var int|null optional maturity info, eg {@link MATURITY_STABLE} */
1787     public $maturity = null;
1788     /** @var string|null optional URL of a page with more info about the update */
1789     public $url = null;
1790     /** @var string|null optional URL of a ZIP package that can be downloaded and installed */
1791     public $download = null;
1792     /** @var string|null of self::download is set, then this must be the MD5 hash of the ZIP */
1793     public $downloadmd5 = null;
1795     /**
1796      * Creates new instance of the class
1797      *
1798      * The $info array must provide at least the 'version' value and optionally all other
1799      * values to populate the object's properties.
1800      *
1801      * @param string $name the frankenstyle component name
1802      * @param array $info associative array with other properties
1803      */
1804     public function __construct($name, array $info) {
1805         $this->component = $name;
1806         foreach ($info as $k => $v) {
1807             if (property_exists('available_update_info', $k) and $k != 'component') {
1808                 $this->$k = $v;
1809             }
1810         }
1811     }
1815 /**
1816  * Implements a communication bridge to the mdeploy.php utility
1817  */
1818 class available_update_deployer {
1820     const HTTP_PARAM_PREFIX     = 'updteautodpldata_';  // Hey, even Google has not heard of such a prefix! So it MUST be safe :-p
1821     const HTTP_PARAM_CHECKER    = 'datapackagesize';    // Name of the parameter that holds the number of items in the received data items
1823     /** @var available_update_deployer holds the singleton instance */
1824     protected static $singletoninstance;
1825     /** @var moodle_url URL of a page that includes the deployer UI */
1826     protected $callerurl;
1827     /** @var moodle_url URL to return after the deployment */
1828     protected $returnurl;
1830     /**
1831      * Direct instantiation not allowed, use the factory method {@link self::instance()}
1832      */
1833     protected function __construct() {
1834     }
1836     /**
1837      * Sorry, this is singleton
1838      */
1839     protected function __clone() {
1840     }
1842     /**
1843      * Factory method for this class
1844      *
1845      * @return available_update_deployer the singleton instance
1846      */
1847     public static function instance() {
1848         if (is_null(self::$singletoninstance)) {
1849             self::$singletoninstance = new self();
1850         }
1851         return self::$singletoninstance;
1852     }
1854     /**
1855      * Reset caches used by this script
1856      *
1857      * @param bool $phpunitreset is this called as a part of PHPUnit reset?
1858      */
1859     public static function reset_caches($phpunitreset = false) {
1860         if ($phpunitreset) {
1861             self::$singletoninstance = null;
1862         }
1863     }
1865     /**
1866      * Is automatic deployment enabled?
1867      *
1868      * @return bool
1869      */
1870     public function enabled() {
1871         global $CFG;
1873         if (!empty($CFG->disableupdateautodeploy)) {
1874             // The feature is prohibited via config.php
1875             return false;
1876         }
1878         return get_config('updateautodeploy');
1879     }
1881     /**
1882      * Sets some base properties of the class to make it usable.
1883      *
1884      * @param moodle_url $callerurl the base URL of a script that will handle the class'es form data
1885      * @param moodle_url $returnurl the final URL to return to when the deployment is finished
1886      */
1887     public function initialize(moodle_url $callerurl, moodle_url $returnurl) {
1889         if (!$this->enabled()) {
1890             throw new coding_exception('Unable to initialize the deployer, the feature is not enabled.');
1891         }
1893         $this->callerurl = $callerurl;
1894         $this->returnurl = $returnurl;
1895     }
1897     /**
1898      * Has the deployer been initialized?
1899      *
1900      * Initialized deployer means that the following properties were set:
1901      * callerurl, returnurl
1902      *
1903      * @return bool
1904      */
1905     public function initialized() {
1907         if (!$this->enabled()) {
1908             return false;
1909         }
1911         if (empty($this->callerurl)) {
1912             return false;
1913         }
1915         if (empty($this->returnurl)) {
1916             return false;
1917         }
1919         return true;
1920     }
1922     /**
1923      * Returns a list of reasons why the deployment can not happen
1924      *
1925      * If the returned array is empty, the deployment seems to be possible. The returned
1926      * structure is an associative array with keys representing individual impediments.
1927      * Possible keys are: missingdownloadurl, missingdownloadmd5, notwritable.
1928      *
1929      * @param available_update_info $info
1930      * @return array
1931      */
1932     public function deployment_impediments(available_update_info $info) {
1934         $impediments = array();
1936         if (empty($info->download)) {
1937             $impediments['missingdownloadurl'] = true;
1938         }
1940         if (empty($info->downloadmd5)) {
1941             $impediments['missingdownloadmd5'] = true;
1942         }
1944         if (!empty($info->download) and !$this->update_downloadable($info->download)) {
1945             $impediments['notdownloadable'] = true;
1946         }
1948         if (!$this->component_writable($info->component)) {
1949             $impediments['notwritable'] = true;
1950         }
1952         return $impediments;
1953     }
1955     /**
1956      * Check to see if the current version of the plugin seems to be a checkout of an external repository.
1957      *
1958      * @see plugin_manager::plugin_external_source()
1959      * @param available_update_info $info
1960      * @return false|string
1961      */
1962     public function plugin_external_source(available_update_info $info) {
1964         $paths = get_plugin_types(true);
1965         list($plugintype, $pluginname) = normalize_component($info->component);
1966         $pluginroot = $paths[$plugintype].'/'.$pluginname;
1968         if (is_dir($pluginroot.'/.git')) {
1969             return 'git';
1970         }
1972         if (is_dir($pluginroot.'/CVS')) {
1973             return 'cvs';
1974         }
1976         if (is_dir($pluginroot.'/.svn')) {
1977             return 'svn';
1978         }
1980         return false;
1981     }
1983     /**
1984      * Prepares a renderable widget to confirm installation of an available update.
1985      *
1986      * @param available_update_info $info component version to deploy
1987      * @return renderable
1988      */
1989     public function make_confirm_widget(available_update_info $info) {
1991         if (!$this->initialized()) {
1992             throw new coding_exception('Illegal method call - deployer not initialized.');
1993         }
1995         $params = $this->data_to_params(array(
1996             'updateinfo' => (array)$info,   // see http://www.php.net/manual/en/language.types.array.php#language.types.array.casting
1997         ));
1999         $widget = new single_button(
2000             new moodle_url($this->callerurl, $params),
2001             get_string('updateavailableinstall', 'core_admin'),
2002             'post'
2003         );
2005         return $widget;
2006     }
2008     /**
2009      * Prepares a renderable widget to execute installation of an available update.
2010      *
2011      * @param available_update_info $info component version to deploy
2012      * @param moodle_url $returnurl URL to return after the installation execution
2013      * @return renderable
2014      */
2015     public function make_execution_widget(available_update_info $info, moodle_url $returnurl = null) {
2016         global $CFG;
2018         if (!$this->initialized()) {
2019             throw new coding_exception('Illegal method call - deployer not initialized.');
2020         }
2022         $pluginrootpaths = get_plugin_types(true);
2024         list($plugintype, $pluginname) = normalize_component($info->component);
2026         if (empty($pluginrootpaths[$plugintype])) {
2027             throw new coding_exception('Unknown plugin type root location', $plugintype);
2028         }
2030         list($passfile, $password) = $this->prepare_authorization();
2032         if (is_null($returnurl)) {
2033             $returnurl = new moodle_url('/admin');
2034         } else {
2035             $returnurl = $returnurl;
2036         }
2038         $params = array(
2039             'upgrade' => true,
2040             'type' => $plugintype,
2041             'name' => $pluginname,
2042             'typeroot' => $pluginrootpaths[$plugintype],
2043             'package' => $info->download,
2044             'md5' => $info->downloadmd5,
2045             'dataroot' => $CFG->dataroot,
2046             'dirroot' => $CFG->dirroot,
2047             'passfile' => $passfile,
2048             'password' => $password,
2049             'returnurl' => $returnurl->out(false),
2050         );
2052         if (!empty($CFG->proxyhost)) {
2053             // MDL-36973 - Beware - we should call just !is_proxybypass() here. But currently, our
2054             // cURL wrapper class does not do it. So, to have consistent behaviour, we pass proxy
2055             // setting regardless the $CFG->proxybypass setting. Once the {@link curl} class is
2056             // fixed, the condition should be amended.
2057             if (true or !is_proxybypass($info->download)) {
2058                 if (empty($CFG->proxyport)) {
2059                     $params['proxy'] = $CFG->proxyhost;
2060                 } else {
2061                     $params['proxy'] = $CFG->proxyhost.':'.$CFG->proxyport;
2062                 }
2064                 if (!empty($CFG->proxyuser) and !empty($CFG->proxypassword)) {
2065                     $params['proxyuserpwd'] = $CFG->proxyuser.':'.$CFG->proxypassword;
2066                 }
2068                 if (!empty($CFG->proxytype)) {
2069                     $params['proxytype'] = $CFG->proxytype;
2070                 }
2071             }
2072         }
2074         $widget = new single_button(
2075             new moodle_url('/mdeploy.php', $params),
2076             get_string('updateavailableinstall', 'core_admin'),
2077             'post'
2078         );
2080         return $widget;
2081     }
2083     /**
2084      * Returns array of data objects passed to this tool.
2085      *
2086      * @return array
2087      */
2088     public function submitted_data() {
2090         $data = $this->params_to_data($_POST);
2092         if (empty($data) or empty($data[self::HTTP_PARAM_CHECKER])) {
2093             return false;
2094         }
2096         if (!empty($data['updateinfo']) and is_object($data['updateinfo'])) {
2097             $updateinfo = $data['updateinfo'];
2098             if (!empty($updateinfo->component) and !empty($updateinfo->version)) {
2099                 $data['updateinfo'] = new available_update_info($updateinfo->component, (array)$updateinfo);
2100             }
2101         }
2103         if (!empty($data['callerurl'])) {
2104             $data['callerurl'] = new moodle_url($data['callerurl']);
2105         }
2107         if (!empty($data['returnurl'])) {
2108             $data['returnurl'] = new moodle_url($data['returnurl']);
2109         }
2111         return $data;
2112     }
2114     /**
2115      * Handles magic getters and setters for protected properties.
2116      *
2117      * @param string $name method name, e.g. set_returnurl()
2118      * @param array $arguments arguments to be passed to the array
2119      */
2120     public function __call($name, array $arguments = array()) {
2122         if (substr($name, 0, 4) === 'set_') {
2123             $property = substr($name, 4);
2124             if (empty($property)) {
2125                 throw new coding_exception('Invalid property name (empty)');
2126             }
2127             if (empty($arguments)) {
2128                 $arguments = array(true); // Default value for flag-like properties.
2129             }
2130             // Make sure it is a protected property.
2131             $isprotected = false;
2132             $reflection = new ReflectionObject($this);
2133             foreach ($reflection->getProperties(ReflectionProperty::IS_PROTECTED) as $reflectionproperty) {
2134                 if ($reflectionproperty->getName() === $property) {
2135                     $isprotected = true;
2136                     break;
2137                 }
2138             }
2139             if (!$isprotected) {
2140                 throw new coding_exception('Unable to set property - it does not exist or it is not protected');
2141             }
2142             $value = reset($arguments);
2143             $this->$property = $value;
2144             return;
2145         }
2147         if (substr($name, 0, 4) === 'get_') {
2148             $property = substr($name, 4);
2149             if (empty($property)) {
2150                 throw new coding_exception('Invalid property name (empty)');
2151             }
2152             if (!empty($arguments)) {
2153                 throw new coding_exception('No parameter expected');
2154             }
2155             // Make sure it is a protected property.
2156             $isprotected = false;
2157             $reflection = new ReflectionObject($this);
2158             foreach ($reflection->getProperties(ReflectionProperty::IS_PROTECTED) as $reflectionproperty) {
2159                 if ($reflectionproperty->getName() === $property) {
2160                     $isprotected = true;
2161                     break;
2162                 }
2163             }
2164             if (!$isprotected) {
2165                 throw new coding_exception('Unable to get property - it does not exist or it is not protected');
2166             }
2167             return $this->$property;
2168         }
2169     }
2171     /**
2172      * Generates a random token and stores it in a file in moodledata directory.
2173      *
2174      * @return array of the (string)filename and (string)password in this order
2175      */
2176     public function prepare_authorization() {
2177         global $CFG;
2179         make_upload_directory('mdeploy/auth/');
2181         $attempts = 0;
2182         $success = false;
2184         while (!$success and $attempts < 5) {
2185             $attempts++;
2187             $passfile = $this->generate_passfile();
2188             $password = $this->generate_password();
2189             $now = time();
2191             $filepath = $CFG->dataroot.'/mdeploy/auth/'.$passfile;
2193             if (!file_exists($filepath)) {
2194                 $success = file_put_contents($filepath, $password . PHP_EOL . $now . PHP_EOL, LOCK_EX);
2195             }
2196         }
2198         if ($success) {
2199             return array($passfile, $password);
2201         } else {
2202             throw new moodle_exception('unable_prepare_authorization', 'core_plugin');
2203         }
2204     }
2206     // End of external API
2208     /**
2209      * Prepares an array of HTTP parameters that can be passed to another page.
2210      *
2211      * @param array|object $data associative array or an object holding the data, data JSON-able
2212      * @return array suitable as a param for moodle_url
2213      */
2214     protected function data_to_params($data) {
2216         // Append some our own data
2217         if (!empty($this->callerurl)) {
2218             $data['callerurl'] = $this->callerurl->out(false);
2219         }
2220         if (!empty($this->returnurl)) {
2221             $data['returnurl'] = $this->returnurl->out(false);
2222         }
2224         // Finally append the count of items in the package.
2225         $data[self::HTTP_PARAM_CHECKER] = count($data);
2227         // Generate params
2228         $params = array();
2229         foreach ($data as $name => $value) {
2230             $transname = self::HTTP_PARAM_PREFIX.$name;
2231             $transvalue = json_encode($value);
2232             $params[$transname] = $transvalue;
2233         }
2235         return $params;
2236     }
2238     /**
2239      * Converts HTTP parameters passed to the script into native PHP data
2240      *
2241      * @param array $params such as $_REQUEST or $_POST
2242      * @return array data passed for this class
2243      */
2244     protected function params_to_data(array $params) {
2246         if (empty($params)) {
2247             return array();
2248         }
2250         $data = array();
2251         foreach ($params as $name => $value) {
2252             if (strpos($name, self::HTTP_PARAM_PREFIX) === 0) {
2253                 $realname = substr($name, strlen(self::HTTP_PARAM_PREFIX));
2254                 $realvalue = json_decode($value);
2255                 $data[$realname] = $realvalue;
2256             }
2257         }
2259         return $data;
2260     }
2262     /**
2263      * Returns a random string to be used as a filename of the password storage.
2264      *
2265      * @return string
2266      */
2267     protected function generate_passfile() {
2268         return clean_param(uniqid('mdeploy_', true), PARAM_FILE);
2269     }
2271     /**
2272      * Returns a random string to be used as the authorization token
2273      *
2274      * @return string
2275      */
2276     protected function generate_password() {
2277         return complex_random_string();
2278     }
2280     /**
2281      * Checks if the given component's directory is writable
2282      *
2283      * For the purpose of the deployment, the web server process has to have
2284      * write access to all files in the component's directory (recursively) and for the
2285      * directory itself.
2286      *
2287      * @see worker::move_directory_source_precheck()
2288      * @param string $component normalized component name
2289      * @return boolean
2290      */
2291     protected function component_writable($component) {
2293         list($plugintype, $pluginname) = normalize_component($component);
2295         $directory = get_plugin_directory($plugintype, $pluginname);
2297         if (is_null($directory)) {
2298             throw new coding_exception('Unknown component location', $component);
2299         }
2301         return $this->directory_writable($directory);
2302     }
2304     /**
2305      * Checks if the mdeploy.php will be able to fetch the ZIP from the given URL
2306      *
2307      * This is mainly supposed to check if the transmission over HTTPS would
2308      * work. That is, if the CA certificates are present at the server.
2309      *
2310      * @param string $downloadurl the URL of the ZIP package to download
2311      * @return bool
2312      */
2313     protected function update_downloadable($downloadurl) {
2314         global $CFG;
2316         $curloptions = array(
2317             'CURLOPT_SSL_VERIFYHOST' => 2,      // this is the default in {@link curl} class but just in case
2318             'CURLOPT_SSL_VERIFYPEER' => true,
2319         );
2321         $cacertfile = $CFG->dataroot.'/moodleorgca.crt';
2322         if (is_readable($cacertfile)) {
2323             // Do not use CA certs provided by the operating system. Instead,
2324             // use this CA cert to verify the updates provider.
2325             $curloptions['CURLOPT_CAINFO'] = $cacertfile;
2326         }
2328         $curl = new curl(array('proxy' => true));
2329         $result = $curl->head($downloadurl, $curloptions);
2330         $errno = $curl->get_errno();
2331         if (empty($errno)) {
2332             return true;
2333         } else {
2334             return false;
2335         }
2336     }
2338     /**
2339      * Checks if the directory and all its contents (recursively) is writable
2340      *
2341      * @param string $path full path to a directory
2342      * @return boolean
2343      */
2344     private function directory_writable($path) {
2346         if (!is_writable($path)) {
2347             return false;
2348         }
2350         if (is_dir($path)) {
2351             $handle = opendir($path);
2352         } else {
2353             return false;
2354         }
2356         $result = true;
2358         while ($filename = readdir($handle)) {
2359             $filepath = $path.'/'.$filename;
2361             if ($filename === '.' or $filename === '..') {
2362                 continue;
2363             }
2365             if (is_dir($filepath)) {
2366                 $result = $result && $this->directory_writable($filepath);
2368             } else {
2369                 $result = $result && is_writable($filepath);
2370             }
2371         }
2373         closedir($handle);
2375         return $result;
2376     }
2380 /**
2381  * Factory class producing required subclasses of {@link plugininfo_base}
2382  */
2383 class plugininfo_default_factory {
2385     /**
2386      * Makes a new instance of the plugininfo class
2387      *
2388      * @param string $type the plugin type, eg. 'mod'
2389      * @param string $typerootdir full path to the location of all the plugins of this type
2390      * @param string $name the plugin name, eg. 'workshop'
2391      * @param string $namerootdir full path to the location of the plugin
2392      * @param string $typeclass the name of class that holds the info about the plugin
2393      * @return plugininfo_base the instance of $typeclass
2394      */
2395     public static function make($type, $typerootdir, $name, $namerootdir, $typeclass) {
2396         $plugin              = new $typeclass();
2397         $plugin->type        = $type;
2398         $plugin->typerootdir = $typerootdir;
2399         $plugin->name        = $name;
2400         $plugin->rootdir     = $namerootdir;
2402         $plugin->init_display_name();
2403         $plugin->load_disk_version();
2404         $plugin->load_db_version();
2405         $plugin->load_required_main_version();
2406         $plugin->init_is_standard();
2408         return $plugin;
2409     }
2413 /**
2414  * Base class providing access to the information about a plugin
2415  *
2416  * @property-read string component the component name, type_name
2417  */
2418 abstract class plugininfo_base {
2420     /** @var string the plugintype name, eg. mod, auth or workshopform */
2421     public $type;
2422     /** @var string full path to the location of all the plugins of this type */
2423     public $typerootdir;
2424     /** @var string the plugin name, eg. assignment, ldap */
2425     public $name;
2426     /** @var string the localized plugin name */
2427     public $displayname;
2428     /** @var string the plugin source, one of plugin_manager::PLUGIN_SOURCE_xxx constants */
2429     public $source;
2430     /** @var fullpath to the location of this plugin */
2431     public $rootdir;
2432     /** @var int|string the version of the plugin's source code */
2433     public $versiondisk;
2434     /** @var int|string the version of the installed plugin */
2435     public $versiondb;
2436     /** @var int|float|string required version of Moodle core  */
2437     public $versionrequires;
2438     /** @var array other plugins that this one depends on, lazy-loaded by {@link get_other_required_plugins()} */
2439     public $dependencies;
2440     /** @var int number of instances of the plugin - not supported yet */
2441     public $instances;
2442     /** @var int order of the plugin among other plugins of the same type - not supported yet */
2443     public $sortorder;
2444     /** @var array|null array of {@link available_update_info} for this plugin */
2445     public $availableupdates;
2447     /**
2448      * Gathers and returns the information about all plugins of the given type
2449      *
2450      * @param string $type the name of the plugintype, eg. mod, auth or workshopform
2451      * @param string $typerootdir full path to the location of the plugin dir
2452      * @param string $typeclass the name of the actually called class
2453      * @return array of plugintype classes, indexed by the plugin name
2454      */
2455     public static function get_plugins($type, $typerootdir, $typeclass) {
2457         // get the information about plugins at the disk
2458         $plugins = get_plugin_list($type);
2459         $ondisk = array();
2460         foreach ($plugins as $pluginname => $pluginrootdir) {
2461             $ondisk[$pluginname] = plugininfo_default_factory::make($type, $typerootdir,
2462                 $pluginname, $pluginrootdir, $typeclass);
2463         }
2464         return $ondisk;
2465     }
2467     /**
2468      * Sets {@link $displayname} property to a localized name of the plugin
2469      */
2470     public function init_display_name() {
2471         if (!get_string_manager()->string_exists('pluginname', $this->component)) {
2472             $this->displayname = '[pluginname,' . $this->component . ']';
2473         } else {
2474             $this->displayname = get_string('pluginname', $this->component);
2475         }
2476     }
2478     /**
2479      * Magic method getter, redirects to read only values.
2480      *
2481      * @param string $name
2482      * @return mixed
2483      */
2484     public function __get($name) {
2485         switch ($name) {
2486             case 'component': return $this->type . '_' . $this->name;
2488             default:
2489                 debugging('Invalid plugin property accessed! '.$name);
2490                 return null;
2491         }
2492     }
2494     /**
2495      * Return the full path name of a file within the plugin.
2496      *
2497      * No check is made to see if the file exists.
2498      *
2499      * @param string $relativepath e.g. 'version.php'.
2500      * @return string e.g. $CFG->dirroot . '/mod/quiz/version.php'.
2501      */
2502     public function full_path($relativepath) {
2503         if (empty($this->rootdir)) {
2504             return '';
2505         }
2506         return $this->rootdir . '/' . $relativepath;
2507     }
2509     /**
2510      * Load the data from version.php.
2511      *
2512      * @param bool $disablecache do not attempt to obtain data from the cache
2513      * @return stdClass the object called $plugin defined in version.php
2514      */
2515     protected function load_version_php($disablecache=false) {
2517         $cache = cache::make('core', 'plugininfo_base');
2519         $versionsphp = $cache->get('versions_php');
2521         if (!$disablecache and $versionsphp !== false and isset($versionsphp[$this->component])) {
2522             return $versionsphp[$this->component];
2523         }
2525         $versionfile = $this->full_path('version.php');
2527         $plugin = new stdClass();
2528         if (is_readable($versionfile)) {
2529             include($versionfile);
2530         }
2531         $versionsphp[$this->component] = $plugin;
2532         $cache->set('versions_php', $versionsphp);
2534         return $plugin;
2535     }
2537     /**
2538      * Sets {@link $versiondisk} property to a numerical value representing the
2539      * version of the plugin's source code.
2540      *
2541      * If the value is null after calling this method, either the plugin
2542      * does not use versioning (typically does not have any database
2543      * data) or is missing from disk.
2544      */
2545     public function load_disk_version() {
2546         $plugin = $this->load_version_php();
2547         if (isset($plugin->version)) {
2548             $this->versiondisk = $plugin->version;
2549         }
2550     }
2552     /**
2553      * Sets {@link $versionrequires} property to a numerical value representing
2554      * the version of Moodle core that this plugin requires.
2555      */
2556     public function load_required_main_version() {
2557         $plugin = $this->load_version_php();
2558         if (isset($plugin->requires)) {
2559             $this->versionrequires = $plugin->requires;
2560         }
2561     }
2563     /**
2564      * Initialise {@link $dependencies} to the list of other plugins (in any)
2565      * that this one requires to be installed.
2566      */
2567     protected function load_other_required_plugins() {
2568         $plugin = $this->load_version_php();
2569         if (!empty($plugin->dependencies)) {
2570             $this->dependencies = $plugin->dependencies;
2571         } else {
2572             $this->dependencies = array(); // By default, no dependencies.
2573         }
2574     }
2576     /**
2577      * Get the list of other plugins that this plugin requires to be installed.
2578      *
2579      * @return array with keys the frankenstyle plugin name, and values either
2580      *      a version string (like '2011101700') or the constant ANY_VERSION.
2581      */
2582     public function get_other_required_plugins() {
2583         if (is_null($this->dependencies)) {
2584             $this->load_other_required_plugins();
2585         }
2586         return $this->dependencies;
2587     }
2589     /**
2590      * Is this is a subplugin?
2591      *
2592      * @return boolean
2593      */
2594     public function is_subplugin() {
2595         return ($this->get_parent_plugin() !== false);
2596     }
2598     /**
2599      * If I am a subplugin, return the name of my parent plugin.
2600      *
2601      * @return string|bool false if not a subplugin, name of the parent otherwise
2602      */
2603     public function get_parent_plugin() {
2604         return $this->get_plugin_manager()->get_parent_of_subplugin($this->type);
2605     }
2607     /**
2608      * Sets {@link $versiondb} property to a numerical value representing the
2609      * currently installed version of the plugin.
2610      *
2611      * If the value is null after calling this method, either the plugin
2612      * does not use versioning (typically does not have any database
2613      * data) or has not been installed yet.
2614      */
2615     public function load_db_version() {
2616         if ($ver = self::get_version_from_config_plugins($this->component)) {
2617             $this->versiondb = $ver;
2618         }
2619     }
2621     /**
2622      * Sets {@link $source} property to one of plugin_manager::PLUGIN_SOURCE_xxx
2623      * constants.
2624      *
2625      * If the property's value is null after calling this method, then
2626      * the type of the plugin has not been recognized and you should throw
2627      * an exception.
2628      */
2629     public function init_is_standard() {
2631         $standard = plugin_manager::standard_plugins_list($this->type);
2633         if ($standard !== false) {
2634             $standard = array_flip($standard);
2635             if (isset($standard[$this->name])) {
2636                 $this->source = plugin_manager::PLUGIN_SOURCE_STANDARD;
2637             } else if (!is_null($this->versiondb) and is_null($this->versiondisk)
2638                     and plugin_manager::is_deleted_standard_plugin($this->type, $this->name)) {
2639                 $this->source = plugin_manager::PLUGIN_SOURCE_STANDARD; // to be deleted
2640             } else {
2641                 $this->source = plugin_manager::PLUGIN_SOURCE_EXTENSION;
2642             }
2643         }
2644     }
2646     /**
2647      * Returns true if the plugin is shipped with the official distribution
2648      * of the current Moodle version, false otherwise.
2649      *
2650      * @return bool
2651      */
2652     public function is_standard() {
2653         return $this->source === plugin_manager::PLUGIN_SOURCE_STANDARD;
2654     }
2656     /**
2657      * Returns true if the the given Moodle version is enough to run this plugin
2658      *
2659      * @param string|int|double $moodleversion
2660      * @return bool
2661      */
2662     public function is_core_dependency_satisfied($moodleversion) {
2664         if (empty($this->versionrequires)) {
2665             return true;
2667         } else {
2668             return (double)$this->versionrequires <= (double)$moodleversion;
2669         }
2670     }
2672     /**
2673      * Returns the status of the plugin
2674      *
2675      * @return string one of plugin_manager::PLUGIN_STATUS_xxx constants
2676      */
2677     public function get_status() {
2679         if (is_null($this->versiondb) and is_null($this->versiondisk)) {
2680             return plugin_manager::PLUGIN_STATUS_NODB;
2682         } else if (is_null($this->versiondb) and !is_null($this->versiondisk)) {
2683             return plugin_manager::PLUGIN_STATUS_NEW;
2685         } else if (!is_null($this->versiondb) and is_null($this->versiondisk)) {
2686             if (plugin_manager::is_deleted_standard_plugin($this->type, $this->name)) {
2687                 return plugin_manager::PLUGIN_STATUS_DELETE;
2688             } else {
2689                 return plugin_manager::PLUGIN_STATUS_MISSING;
2690             }
2692         } else if ((string)$this->versiondb === (string)$this->versiondisk) {
2693             return plugin_manager::PLUGIN_STATUS_UPTODATE;
2695         } else if ($this->versiondb < $this->versiondisk) {
2696             return plugin_manager::PLUGIN_STATUS_UPGRADE;
2698         } else if ($this->versiondb > $this->versiondisk) {
2699             return plugin_manager::PLUGIN_STATUS_DOWNGRADE;
2701         } else {
2702             // $version = pi(); and similar funny jokes - hopefully Donald E. Knuth will never contribute to Moodle ;-)
2703             throw new coding_exception('Unable to determine plugin state, check the plugin versions');
2704         }
2705     }
2707     /**
2708      * Returns the information about plugin availability
2709      *
2710      * True means that the plugin is enabled. False means that the plugin is
2711      * disabled. Null means that the information is not available, or the
2712      * plugin does not support configurable availability or the availability
2713      * can not be changed.
2714      *
2715      * @return null|bool
2716      */
2717     public function is_enabled() {
2718         return null;
2719     }
2721     /**
2722      * Populates the property {@link $availableupdates} with the information provided by
2723      * available update checker
2724      *
2725      * @param available_update_checker $provider the class providing the available update info
2726      */
2727     public function check_available_updates(available_update_checker $provider) {
2728         global $CFG;
2730         if (isset($CFG->updateminmaturity)) {
2731             $minmaturity = $CFG->updateminmaturity;
2732         } else {
2733             // this can happen during the very first upgrade to 2.3
2734             $minmaturity = MATURITY_STABLE;
2735         }
2737         $this->availableupdates = $provider->get_update_info($this->component,
2738             array('minmaturity' => $minmaturity));
2739     }
2741     /**
2742      * If there are updates for this plugin available, returns them.
2743      *
2744      * Returns array of {@link available_update_info} objects, if some update
2745      * is available. Returns null if there is no update available or if the update
2746      * availability is unknown.
2747      *
2748      * @return array|null
2749      */
2750     public function available_updates() {
2752         if (empty($this->availableupdates) or !is_array($this->availableupdates)) {
2753             return null;
2754         }
2756         $updates = array();
2758         foreach ($this->availableupdates as $availableupdate) {
2759             if ($availableupdate->version > $this->versiondisk) {
2760                 $updates[] = $availableupdate;
2761             }
2762         }
2764         if (empty($updates)) {
2765             return null;
2766         }
2768         return $updates;
2769     }
2771     /**
2772      * Returns the node name used in admin settings menu for this plugin settings (if applicable)
2773      *
2774      * @return null|string node name or null if plugin does not create settings node (default)
2775      */
2776     public function get_settings_section_name() {
2777         return null;
2778     }
2780     /**
2781      * Returns the URL of the plugin settings screen
2782      *
2783      * Null value means that the plugin either does not have the settings screen
2784      * or its location is not available via this library.
2785      *
2786      * @return null|moodle_url
2787      */
2788     public function get_settings_url() {
2789         $section = $this->get_settings_section_name();
2790         if ($section === null) {
2791             return null;
2792         }
2793         $settings = admin_get_root()->locate($section);
2794         if ($settings && $settings instanceof admin_settingpage) {
2795             return new moodle_url('/admin/settings.php', array('section' => $section));
2796         } else if ($settings && $settings instanceof admin_externalpage) {
2797             return new moodle_url($settings->url);
2798         } else {
2799             return null;
2800         }
2801     }
2803     /**
2804      * Loads plugin settings to the settings tree
2805      *
2806      * This function usually includes settings.php file in plugins folder.
2807      * Alternatively it can create a link to some settings page (instance of admin_externalpage)
2808      *
2809      * @param part_of_admin_tree $adminroot
2810      * @param string $parentnodename
2811      * @param bool $hassiteconfig whether the current user has moodle/site:config capability
2812      */
2813     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
2814     }
2816     /**
2817      * Should there be a way to uninstall the plugin via the administration UI
2818      *
2819      * By default, uninstallation is allowed for all non-standard add-ons. Subclasses
2820      * may want to override this to allow uninstallation of all plugins (simply by
2821      * returning true unconditionally). Subplugins follow their parent plugin's
2822      * decision by default.
2823      *
2824      * Note that even if true is returned, the core may still prohibit the uninstallation,
2825      * e.g. in case there are other plugins that depend on this one.
2826      *
2827      * @return boolean
2828      */
2829     public function is_uninstall_allowed() {
2831         if ($this->is_subplugin()) {
2832             return $this->get_plugin_manager()->get_plugin_info($this->get_parent_plugin())->is_uninstall_allowed();
2833         }
2835         if ($this->is_standard()) {
2836             return false;
2837         }
2839         return true;
2840     }
2842     /**
2843      * Returns the URL of the screen where this plugin can be uninstalled
2844      *
2845      * Visiting that URL must be safe, that is a manual confirmation is needed
2846      * for actual uninstallation of the plugin. By default, URL to a common
2847      * uninstalling tool is returned.
2848      *
2849      * @return moodle_url
2850      */
2851     public function get_uninstall_url() {
2852         return $this->get_default_uninstall_url();
2853     }
2855     /**
2856      * Returns relative directory of the plugin with heading '/'
2857      *
2858      * @return string
2859      */
2860     public function get_dir() {
2861         global $CFG;
2863         return substr($this->rootdir, strlen($CFG->dirroot));
2864     }
2866     /**
2867      * Hook method to implement certain steps when uninstalling the plugin.
2868      *
2869      * This hook is called by {@link plugin_manager::uninstall_plugin()} so
2870      * it is basically usable only for those plugin types that use the default
2871      * uninstall tool provided by {@link self::get_default_uninstall_url()}.
2872      *
2873      * @param progress_trace $progress traces the process
2874      * @return bool true on success, false on failure
2875      */
2876     public function uninstall(progress_trace $progress) {
2877         return true;
2878     }
2880     /**
2881      * Returns URL to a script that handles common plugin uninstall procedure.
2882      *
2883      * This URL is suitable for plugins that do not have their own UI
2884      * for uninstalling.
2885      *
2886      * @return moodle_url
2887      */
2888     protected final function get_default_uninstall_url() {
2889         return new moodle_url('/admin/plugins.php', array(
2890             'sesskey' => sesskey(),
2891             'uninstall' => $this->component,
2892             'confirm' => 0,
2893         ));
2894     }
2896     /**
2897      * Provides access to plugin versions from the {config_plugins} table
2898      *
2899      * @param string $plugin plugin name
2900      * @param bool $disablecache do not attempt to obtain data from the cache
2901      * @return int|bool the stored value or false if not found
2902      */
2903     protected function get_version_from_config_plugins($plugin, $disablecache=false) {
2904         global $DB;
2906         $cache = cache::make('core', 'plugininfo_base');
2908         $pluginversions = $cache->get('versions_db');
2910         if ($pluginversions === false or $disablecache) {
2911             try {
2912                 $pluginversions = $DB->get_records_menu('config_plugins', array('name' => 'version'), 'plugin', 'plugin,value');
2913             } catch (dml_exception $e) {
2914                 // before install
2915                 $pluginversions = array();
2916             }
2917             $cache->set('versions_db', $pluginversions);
2918         }
2920         if (isset($pluginversions[$plugin])) {
2921             return $pluginversions[$plugin];
2922         } else {
2923             return false;
2924         }
2925     }
2927     /**
2928      * Provides access to the plugin_manager singleton.
2929      *
2930      * @return plugin_manmager
2931      */
2932     protected function get_plugin_manager() {
2933         return plugin_manager::instance();
2934     }
2938 /**
2939  * General class for all plugin types that do not have their own class
2940  */
2941 class plugininfo_general extends plugininfo_base {
2945 /**
2946  * Class for page side blocks
2947  */
2948 class plugininfo_block extends plugininfo_base {
2950     public static function get_plugins($type, $typerootdir, $typeclass) {
2952         // get the information about blocks at the disk
2953         $blocks = parent::get_plugins($type, $typerootdir, $typeclass);
2955         // add blocks missing from disk
2956         $blocksinfo = self::get_blocks_info();
2957         foreach ($blocksinfo as $blockname => $blockinfo) {
2958             if (isset($blocks[$blockname])) {
2959                 continue;
2960             }
2961             $plugin                 = new $typeclass();
2962             $plugin->type           = $type;
2963             $plugin->typerootdir    = $typerootdir;
2964             $plugin->name           = $blockname;
2965             $plugin->rootdir        = null;
2966             $plugin->displayname    = $blockname;
2967             $plugin->versiondb      = $blockinfo->version;
2968             $plugin->init_is_standard();
2970             $blocks[$blockname]   = $plugin;
2971         }
2973         return $blocks;
2974     }
2976     /**
2977      * Magic method getter, redirects to read only values.
2978      *
2979      * For block plugins pretends the object has 'visible' property for compatibility
2980      * with plugins developed for Moodle version below 2.4
2981      *
2982      * @param string $name
2983      * @return mixed
2984      */
2985     public function __get($name) {
2986         if ($name === 'visible') {
2987             debugging('This is now an instance of plugininfo_block, please use $block->is_enabled() instead of $block->visible', DEBUG_DEVELOPER);
2988             return ($this->is_enabled() !== false);
2989         }
2990         return parent::__get($name);
2991     }
2993     public function init_display_name() {
2995         if (get_string_manager()->string_exists('pluginname', 'block_' . $this->name)) {
2996             $this->displayname = get_string('pluginname', 'block_' . $this->name);
2998         } else if (($block = block_instance($this->name)) !== false) {
2999             $this->displayname = $block->get_title();
3001         } else {
3002             parent::init_display_name();
3003         }
3004     }
3006     public function load_db_version() {
3007         global $DB;
3009         $blocksinfo = self::get_blocks_info();
3010         if (isset($blocksinfo[$this->name]->version)) {
3011             $this->versiondb = $blocksinfo[$this->name]->version;
3012         }
3013     }
3015     public function is_enabled() {
3017         $blocksinfo = self::get_blocks_info();
3018         if (isset($blocksinfo[$this->name]->visible)) {
3019             if ($blocksinfo[$this->name]->visible) {
3020                 return true;
3021             } else {
3022                 return false;
3023             }
3024         } else {
3025             return parent::is_enabled();
3026         }
3027     }
3029     public function get_settings_section_name() {
3030         return 'blocksetting' . $this->name;
3031     }
3033     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3034         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3035         $ADMIN = $adminroot; // may be used in settings.php
3036         $block = $this; // also can be used inside settings.php
3037         $section = $this->get_settings_section_name();
3039         if (!$hassiteconfig || (($blockinstance = block_instance($this->name)) === false)) {
3040             return;
3041         }
3043         $settings = null;
3044         if ($blockinstance->has_config()) {
3045             if (file_exists($this->full_path('settings.php'))) {
3046                 $settings = new admin_settingpage($section, $this->displayname,
3047                         'moodle/site:config', $this->is_enabled() === false);
3048                 include($this->full_path('settings.php')); // this may also set $settings to null
3049             } else {
3050                 $blocksinfo = self::get_blocks_info();
3051                 $settingsurl = new moodle_url('/admin/block.php', array('block' => $blocksinfo[$this->name]->id));
3052                 $settings = new admin_externalpage($section, $this->displayname,
3053                         $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
3054             }
3055         }
3056         if ($settings) {
3057             $ADMIN->add($parentnodename, $settings);
3058         }
3059     }
3061     public function is_uninstall_allowed() {
3062         return true;
3063     }
3065     public function get_uninstall_url() {
3066         $blocksinfo = self::get_blocks_info();
3067         return new moodle_url('/admin/blocks.php', array('delete' => $blocksinfo[$this->name]->id, 'sesskey' => sesskey()));
3068     }
3070     /**
3071      * Provides access to the records in {block} table
3072      *
3073      * @param bool $disablecache do not attempt to obtain data from the cache
3074      * @return array array of stdClasses
3075      */
3076     protected static function get_blocks_info($disablecache=false) {
3077         global $DB;
3079         $cache = cache::make('core', 'plugininfo_block');
3081         $blocktypes = $cache->get('blocktypes');
3083         if ($blocktypes === false or $disablecache) {
3084             try {
3085                 $blocktypes = $DB->get_records('block', null, 'name', 'name,id,version,visible');
3086             } catch (dml_exception $e) {
3087                 // before install
3088                 $blocktypes = array();
3089             }
3090             $cache->set('blocktypes', $blocktypes);
3091         }
3093         return $blocktypes;
3094     }
3098 /**
3099  * Class for text filters
3100  */
3101 class plugininfo_filter extends plugininfo_base {
3103     public static function get_plugins($type, $typerootdir, $typeclass) {
3104         global $CFG, $DB;
3106         $filters = array();
3108         // get the list of filters in /filter location
3109         $installed = filter_get_all_installed();
3111         foreach ($installed as $name => $displayname) {
3112             $plugin                 = new $typeclass();
3113             $plugin->type           = $type;
3114             $plugin->typerootdir    = $typerootdir;
3115             $plugin->name           = $name;
3116             $plugin->rootdir        = "$CFG->dirroot/filter/$name";
3117             $plugin->displayname    = $displayname;
3119             $plugin->load_disk_version();
3120             $plugin->load_db_version();
3121             $plugin->load_required_main_version();
3122             $plugin->init_is_standard();
3124             $filters[$plugin->name] = $plugin;
3125         }
3127         // Do not mess with filter registration here!
3129         $globalstates = self::get_global_states();
3131         // make sure that all registered filters are installed, just in case
3132         foreach ($globalstates as $name => $info) {
3133             if (!isset($filters[$name])) {
3134                 // oops, there is a record in filter_active but the filter is not installed
3135                 $plugin                 = new $typeclass();
3136                 $plugin->type           = $type;
3137                 $plugin->typerootdir    = $typerootdir;
3138                 $plugin->name           = $name;
3139                 $plugin->rootdir        = "$CFG->dirroot/filter/$name";
3140                 $plugin->displayname    = $name;
3142                 $plugin->load_db_version();
3144                 if (is_null($plugin->versiondb)) {
3145                     // this is a hack to stimulate 'Missing from disk' error
3146                     // because $plugin->versiondisk will be null !== false
3147                     $plugin->versiondb = false;
3148                 }
3150                 $filters[$plugin->name] = $plugin;
3151             }
3152         }
3154         return $filters;
3155     }
3157     public function init_display_name() {
3158         // do nothing, the name is set in self::get_plugins()
3159     }
3161     public function is_enabled() {
3163         $globalstates = self::get_global_states();
3165         foreach ($globalstates as $name => $info) {
3166             if ($name === $this->name) {
3167                 if ($info->active == TEXTFILTER_DISABLED) {
3168                     return false;
3169                 } else {
3170                     // it may be 'On' or 'Off, but available'
3171                     return null;
3172                 }
3173             }
3174         }
3176         return null;
3177     }
3179     public function get_settings_section_name() {
3180         return 'filtersetting' . $this->name;
3181     }
3183     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3184         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3185         $ADMIN = $adminroot; // may be used in settings.php
3186         $filter = $this; // also can be used inside settings.php
3188         $settings = null;
3189         if ($hassiteconfig && file_exists($this->full_path('filtersettings.php'))) {
3190             $section = $this->get_settings_section_name();
3191             $settings = new admin_settingpage($section, $this->displayname,
3192                     'moodle/site:config', $this->is_enabled() === false);
3193             include($this->full_path('filtersettings.php')); // this may also set $settings to null
3194         }
3195         if ($settings) {
3196             $ADMIN->add($parentnodename, $settings);
3197         }
3198     }
3200     public function is_uninstall_allowed() {
3201         return true;
3202     }
3204     public function get_uninstall_url() {
3205         return new moodle_url('/admin/filters.php', array('sesskey' => sesskey(), 'filterpath' => $this->name, 'action' => 'delete'));
3206     }
3208     /**
3209      * Provides access to the results of {@link filter_get_global_states()}
3210      * but indexed by the normalized filter name
3211      *
3212      * The legacy filter name is available as ->legacyname property.
3213      *
3214      * @param bool $disablecache do not attempt to obtain data from the cache
3215      * @return array
3216      */
3217     protected static function get_global_states($disablecache=false) {
3218         global $DB;
3220         $cache = cache::make('core', 'plugininfo_filter');
3222         $globalstates = $cache->get('globalstates');
3224         if ($globalstates === false or $disablecache) {
3226             if (!$DB->get_manager()->table_exists('filter_active')) {
3227                 // Not installed yet.
3228                 $cache->set('globalstates', array());
3229                 return array();
3230             }
3232             $globalstates = array();
3234             foreach (filter_get_global_states() as $name => $info) {
3235                 if (strpos($name, '/') !== false) {
3236                     // Skip existing before upgrade to new names.
3237                     continue;
3238                 }
3240                 $filterinfo = new stdClass();
3241                 $filterinfo->active = $info->active;
3242                 $filterinfo->sortorder = $info->sortorder;
3243                 $globalstates[$name] = $filterinfo;
3244             }
3246             $cache->set('globalstates', $globalstates);
3247         }
3249         return $globalstates;
3250     }
3254 /**
3255  * Class for activity modules
3256  */
3257 class plugininfo_mod extends plugininfo_base {
3259     public static function get_plugins($type, $typerootdir, $typeclass) {
3261         // get the information about plugins at the disk
3262         $modules = parent::get_plugins($type, $typerootdir, $typeclass);
3264         // add modules missing from disk
3265         $modulesinfo = self::get_modules_info();
3266         foreach ($modulesinfo as $modulename => $moduleinfo) {
3267             if (isset($modules[$modulename])) {
3268                 continue;
3269             }
3270             $plugin                 = new $typeclass();
3271             $plugin->type           = $type;
3272             $plugin->typerootdir    = $typerootdir;
3273             $plugin->name           = $modulename;
3274             $plugin->rootdir        = null;
3275             $plugin->displayname    = $modulename;
3276             $plugin->versiondb      = $moduleinfo->version;
3277             $plugin->init_is_standard();
3279             $modules[$modulename]   = $plugin;
3280         }
3282         return $modules;
3283     }
3285     /**
3286      * Magic method getter, redirects to read only values.
3287      *
3288      * For module plugins we pretend the object has 'visible' property for compatibility
3289      * with plugins developed for Moodle version below 2.4
3290      *
3291      * @param string $name
3292      * @return mixed
3293      */
3294     public function __get($name) {
3295         if ($name === 'visible') {
3296             debugging('This is now an instance of plugininfo_mod, please use $module->is_enabled() instead of $module->visible', DEBUG_DEVELOPER);
3297             return ($this->is_enabled() !== false);
3298         }
3299         return parent::__get($name);
3300     }
3302     public function init_display_name() {
3303         if (get_string_manager()->string_exists('pluginname', $this->component)) {
3304             $this->displayname = get_string('pluginname', $this->component);
3305         } else {
3306             $this->displayname = get_string('modulename', $this->component);
3307         }
3308     }
3310     /**
3311      * Load the data from version.php.
3312      *
3313      * @param bool $disablecache do not attempt to obtain data from the cache
3314      * @return object the data object defined in version.php.
3315      */
3316     protected function load_version_php($disablecache=false) {
3318         $cache = cache::make('core', 'plugininfo_base');
3320         $versionsphp = $cache->get('versions_php');
3322         if (!$disablecache and $versionsphp !== false and isset($versionsphp[$this->component])) {
3323             return $versionsphp[$this->component];
3324         }
3326         $versionfile = $this->full_path('version.php');
3328         $module = new stdClass();
3329         $plugin = new stdClass();
3330         if (is_readable($versionfile)) {
3331             include($versionfile);
3332         }
3333         if (!isset($module->version) and isset($plugin->version)) {
3334             $module = $plugin;
3335         }
3336         $versionsphp[$this->component] = $module;
3337         $cache->set('versions_php', $versionsphp);
3339         return $module;
3340     }
3342     public function load_db_version() {
3343         global $DB;
3345         $modulesinfo = self::get_modules_info();
3346         if (isset($modulesinfo[$this->name]->version)) {
3347             $this->versiondb = $modulesinfo[$this->name]->version;
3348         }
3349     }
3351     public function is_enabled() {
3353         $modulesinfo = self::get_modules_info();
3354         if (isset($modulesinfo[$this->name]->visible)) {
3355             if ($modulesinfo[$this->name]->visible) {
3356                 return true;
3357             } else {
3358                 return false;
3359             }
3360         } else {
3361             return parent::is_enabled();
3362         }
3363     }
3365     public function get_settings_section_name() {
3366         return 'modsetting' . $this->name;
3367     }
3369     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3370         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3371         $ADMIN = $adminroot; // may be used in settings.php
3372         $module = $this; // also can be used inside settings.php
3373         $section = $this->get_settings_section_name();
3375         $modulesinfo = self::get_modules_info();
3376         $settings = null;
3377         if ($hassiteconfig && isset($modulesinfo[$this->name]) && file_exists($this->full_path('settings.php'))) {
3378             $settings = new admin_settingpage($section, $this->displayname,
3379                     'moodle/site:config', $this->is_enabled() === false);
3380             include($this->full_path('settings.php')); // this may also set $settings to null
3381         }
3382         if ($settings) {
3383             $ADMIN->add($parentnodename, $settings);
3384         }
3385     }
3387     /**
3388      * Allow all activity modules but Forum to be uninstalled.
3390      * This exception for the Forum has been hard-coded in Moodle since ages,
3391      * we may want to re-think it one day.
3392      */
3393     public function is_uninstall_allowed() {
3394         if ($this->name === 'forum') {
3395             return false;
3396         } else {
3397             return true;
3398         }
3399     }
3401     public function get_uninstall_url() {
3402         return new moodle_url('/admin/modules.php', array('delete' => $this->name, 'sesskey' => sesskey()));
3403     }
3405     /**
3406      * Provides access to the records in {modules} table
3407      *
3408      * @param bool $disablecache do not attempt to obtain data from the cache
3409      * @return array array of stdClasses
3410      */
3411     protected static function get_modules_info($disablecache=false) {
3412         global $DB;
3414         $cache = cache::make('core', 'plugininfo_mod');
3416         $modulesinfo = $cache->get('modulesinfo');
3418         if ($modulesinfo === false or $disablecache) {
3419             try {
3420                 $modulesinfo = $DB->get_records('modules', null, 'name', 'name,id,version,visible');
3421             } catch (dml_exception $e) {
3422                 // before install
3423                 $modulesinfo = array();
3424             }
3425             $cache->set('modulesinfo', $modulesinfo);
3426         }
3428         return $modulesinfo;
3429     }
3433 /**
3434  * Class for question behaviours.
3435  */
3436 class plugininfo_qbehaviour extends plugininfo_base {
3438     public function is_uninstall_allowed() {
3439         return true;
3440     }
3442     public function get_uninstall_url() {
3443         return new moodle_url('/admin/qbehaviours.php',
3444                 array('delete' => $this->name, 'sesskey' => sesskey()));
3445     }
3449 /**
3450  * Class for question types
3451  */
3452 class plugininfo_qtype extends plugininfo_base {
3454     public function is_uninstall_allowed() {
3455         return true;
3456     }
3458     public function get_uninstall_url() {
3459         return new moodle_url('/admin/qtypes.php',
3460                 array('delete' => $this->name, 'sesskey' => sesskey()));
3461     }
3463     public function get_settings_section_name() {
3464         return 'qtypesetting' . $this->name;
3465     }
3467     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3468         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3469         $ADMIN = $adminroot; // may be used in settings.php
3470         $qtype = $this; // also can be used inside settings.php
3471         $section = $this->get_settings_section_name();
3473         $settings = null;
3474         $systemcontext = context_system::instance();
3475         if (($hassiteconfig || has_capability('moodle/question:config', $systemcontext)) &&
3476                 file_exists($this->full_path('settings.php'))) {
3477             $settings = new admin_settingpage($section, $this->displayname,
3478                     'moodle/question:config', $this->is_enabled() === false);
3479             include($this->full_path('settings.php')); // this may also set $settings to null
3480         }
3481         if ($settings) {
3482             $ADMIN->add($parentnodename, $settings);
3483         }
3484     }
3488 /**
3489  * Class for authentication plugins
3490  */
3491 class plugininfo_auth extends plugininfo_base {
3493     public function is_enabled() {
3494         global $CFG;
3496         if (in_array($this->name, array('nologin', 'manual'))) {
3497             // these two are always enabled and can't be disabled
3498             return null;
3499         }
3501         $enabled = array_flip(explode(',', $CFG->auth));
3503         return isset($enabled[$this->name]);
3504     }
3506     public function get_settings_section_name() {
3507         return 'authsetting' . $this->name;
3508     }
3510     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3511         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3512         $ADMIN = $adminroot; // may be used in settings.php
3513         $auth = $this; // also to be used inside settings.php
3514         $section = $this->get_settings_section_name();
3516         $settings = null;
3517         if ($hassiteconfig) {
3518             if (file_exists($this->full_path('settings.php'))) {
3519                 // TODO: finish implementation of common settings - locking, etc.
3520                 $settings = new admin_settingpage($section, $this->displayname,
3521                         'moodle/site:config', $this->is_enabled() === false);
3522                 include($this->full_path('settings.php')); // this may also set $settings to null
3523             } else {
3524                 $settingsurl = new moodle_url('/admin/auth_config.php', array('auth' => $this->name));
3525                 $settings = new admin_externalpage($section, $this->displayname,
3526                         $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
3527             }
3528         }
3529         if ($settings) {
3530             $ADMIN->add($parentnodename, $settings);
3531         }
3532     }
3536 /**
3537  * Class for enrolment plugins
3538  */
3539 class plugininfo_enrol extends plugininfo_base {
3541     public function is_enabled() {
3542         global $CFG;
3544         // We do not actually need whole enrolment classes here so we do not call
3545         // {@link enrol_get_plugins()}. Note that this may produce slightly different
3546         // results, for example if the enrolment plugin does not contain lib.php
3547         // but it is listed in $CFG->enrol_plugins_enabled
3549         $enabled = array_flip(explode(',', $CFG->enrol_plugins_enabled));
3551         return isset($enabled[$this->name]);
3552     }
3554     public function get_settings_section_name() {
3555         if (file_exists($this->full_path('settings.php'))) {
3556             return 'enrolsettings' . $this->name;
3557         } else {
3558             return null;
3559         }
3560     }
3562     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3563         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3565         if (!$hassiteconfig or !file_exists($this->full_path('settings.php'))) {
3566             return;
3567         }
3568         $section = $this->get_settings_section_name();
3570         $ADMIN = $adminroot; // may be used in settings.php
3571         $enrol = $this; // also can be used inside settings.php
3572         $settings = new admin_settingpage($section, $this->displayname,
3573                 'moodle/site:config', $this->is_enabled() === false);
3575         include($this->full_path('settings.php')); // This may also set $settings to null!
3577         if ($settings) {
3578             $ADMIN->add($parentnodename, $settings);
3579         }
3580     }
3582     public function is_uninstall_allowed() {
3583         return true;
3584     }
3586     public function get_uninstall_url() {
3587         return new moodle_url('/admin/enrol.php', array('action' => 'uninstall', 'enrol' => $this->name, 'sesskey' => sesskey()));
3588     }
3592 /**
3593  * Class for messaging processors