MDL-40658 Include tinymce_pdw and tinymce_wrap in the list of standard plugins
[moodle.git] / lib / pluginlib.php
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
18 /**
19  * Defines classes used for plugins management
20  *
21  * This library provides a unified interface to various plugin types in
22  * Moodle. It is mainly used by the plugins management admin page and the
23  * plugins check page during the upgrade.
24  *
25  * @package    core
26  * @subpackage admin
27  * @copyright  2011 David Mudrak <david@moodle.com>
28  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
29  */
31 defined('MOODLE_INTERNAL') || die();
33 /**
34  * Singleton class providing general plugins management functionality
35  */
36 class plugin_manager {
38     /** the plugin is shipped with standard Moodle distribution */
39     const PLUGIN_SOURCE_STANDARD    = 'std';
40     /** the plugin is added extension */
41     const PLUGIN_SOURCE_EXTENSION   = 'ext';
43     /** the plugin uses neither database nor capabilities, no versions */
44     const PLUGIN_STATUS_NODB        = 'nodb';
45     /** the plugin is up-to-date */
46     const PLUGIN_STATUS_UPTODATE    = 'uptodate';
47     /** the plugin is about to be installed */
48     const PLUGIN_STATUS_NEW         = 'new';
49     /** the plugin is about to be upgraded */
50     const PLUGIN_STATUS_UPGRADE     = 'upgrade';
51     /** the standard plugin is about to be deleted */
52     const PLUGIN_STATUS_DELETE     = 'delete';
53     /** the version at the disk is lower than the one already installed */
54     const PLUGIN_STATUS_DOWNGRADE   = 'downgrade';
55     /** the plugin is installed but missing from disk */
56     const PLUGIN_STATUS_MISSING     = 'missing';
58     /** @var plugin_manager holds the singleton instance */
59     protected static $singletoninstance;
60     /** @var array of raw plugins information */
61     protected $pluginsinfo = null;
62     /** @var array of raw subplugins information */
63     protected $subpluginsinfo = null;
65     /**
66      * Direct initiation not allowed, use the factory method {@link self::instance()}
67      */
68     protected function __construct() {
69     }
71     /**
72      * Sorry, this is singleton
73      */
74     protected function __clone() {
75     }
77     /**
78      * Factory method for this class
79      *
80      * @return plugin_manager the singleton instance
81      */
82     public static function instance() {
83         if (is_null(self::$singletoninstance)) {
84             self::$singletoninstance = new self();
85         }
86         return self::$singletoninstance;
87     }
89     /**
90      * Reset any caches
91      * @param bool $phpunitreset
92      */
93     public static function reset_caches($phpunitreset = false) {
94         if ($phpunitreset) {
95             self::$singletoninstance = null;
96         }
97     }
99     /**
100      * Returns the result of {@link get_plugin_types()} ordered for humans
101      *
102      * @see self::reorder_plugin_types()
103      * @param bool $fullpaths false means relative paths from dirroot
104      * @return array (string)name => (string)location
105      */
106     public function get_plugin_types($fullpaths = true) {
107         return $this->reorder_plugin_types(get_plugin_types($fullpaths));
108     }
110     /**
111      * Returns list of known plugins of the given type
112      *
113      * This method returns the subset of the tree returned by {@link self::get_plugins()}.
114      * If the given type is not known, empty array is returned.
115      *
116      * @param string $type plugin type, e.g. 'mod' or 'workshopallocation'
117      * @param bool $disablecache force reload, cache can be used otherwise
118      * @return array (string)plugin name (e.g. 'workshop') => corresponding subclass of {@link plugininfo_base}
119      */
120     public function get_plugins_of_type($type, $disablecache=false) {
122         $plugins = $this->get_plugins($disablecache);
124         if (!isset($plugins[$type])) {
125             return array();
126         }
128         return $plugins[$type];
129     }
131     /**
132      * Returns a tree of known plugins and information about them
133      *
134      * @param bool $disablecache force reload, cache can be used otherwise
135      * @return array 2D array. The first keys are plugin type names (e.g. qtype);
136      *      the second keys are the plugin local name (e.g. multichoice); and
137      *      the values are the corresponding objects extending {@link plugininfo_base}
138      */
139     public function get_plugins($disablecache=false) {
140         global $CFG;
142         if ($disablecache or is_null($this->pluginsinfo)) {
143             // Hack: include mod and editor subplugin management classes first,
144             //       the adminlib.php is supposed to contain extra admin settings too.
145             require_once($CFG->libdir.'/adminlib.php');
146             foreach (core_component::get_plugin_types_with_subplugins() as $type => $ignored) {
147                 foreach (core_component::get_plugin_list($type) as $dir) {
148                     if (file_exists("$dir/adminlib.php")) {
149                         include_once("$dir/adminlib.php");
150                     }
151                 }
152             }
153             $this->pluginsinfo = array();
154             $plugintypes = $this->get_plugin_types();
155             foreach ($plugintypes as $plugintype => $plugintyperootdir) {
156                 if (in_array($plugintype, array('base', 'general'))) {
157                     throw new coding_exception('Illegal usage of reserved word for plugin type');
158                 }
159                 if (class_exists('plugininfo_' . $plugintype)) {
160                     $plugintypeclass = 'plugininfo_' . $plugintype;
161                 } else {
162                     $plugintypeclass = 'plugininfo_general';
163                 }
164                 if (!in_array('plugininfo_base', class_parents($plugintypeclass))) {
165                     throw new coding_exception('Class ' . $plugintypeclass . ' must extend plugininfo_base');
166                 }
167                 $plugins = call_user_func(array($plugintypeclass, 'get_plugins'), $plugintype, $plugintyperootdir, $plugintypeclass);
168                 $this->pluginsinfo[$plugintype] = $plugins;
169             }
171             if (empty($CFG->disableupdatenotifications) and !during_initial_install()) {
172                 // append the information about available updates provided by {@link available_update_checker()}
173                 $provider = available_update_checker::instance();
174                 foreach ($this->pluginsinfo as $plugintype => $plugins) {
175                     foreach ($plugins as $plugininfoholder) {
176                         $plugininfoholder->check_available_updates($provider);
177                     }
178                 }
179             }
180         }
182         return $this->pluginsinfo;
183     }
185     /**
186      * Returns list of all known subplugins of the given plugin
187      *
188      * For plugins that do not provide subplugins (i.e. there is no support for it),
189      * empty array is returned.
190      *
191      * @param string $component full component name, e.g. 'mod_workshop'
192      * @param bool $disablecache force reload, cache can be used otherwise
193      * @return array (string) component name (e.g. 'workshopallocation_random') => subclass of {@link plugininfo_base}
194      */
195     public function get_subplugins_of_plugin($component, $disablecache=false) {
197         $pluginfo = $this->get_plugin_info($component, $disablecache);
199         if (is_null($pluginfo)) {
200             return array();
201         }
203         $subplugins = $this->get_subplugins($disablecache);
205         if (!isset($subplugins[$pluginfo->component])) {
206             return array();
207         }
209         $list = array();
211         foreach ($subplugins[$pluginfo->component] as $subdata) {
212             foreach ($this->get_plugins_of_type($subdata->type) as $subpluginfo) {
213                 $list[$subpluginfo->component] = $subpluginfo;
214             }
215         }
217         return $list;
218     }
220     /**
221      * Returns list of plugins that define their subplugins and the information
222      * about them from the db/subplugins.php file.
223      *
224      * @param bool $disablecache force reload, cache can be used otherwise
225      * @return array with keys like 'mod_quiz', and values the data from the
226      *      corresponding db/subplugins.php file.
227      */
228     public function get_subplugins($disablecache=false) {
230         if ($disablecache or is_null($this->subpluginsinfo)) {
231             $this->subpluginsinfo = array();
232             foreach (core_component::get_plugin_types_with_subplugins() as $type => $ignored) {
233                 foreach (core_component::get_plugin_list($type) as $component => $ownerdir) {
234                     $componentsubplugins = array();
235                     if (file_exists($ownerdir . '/db/subplugins.php')) {
236                         $subplugins = array();
237                         include($ownerdir . '/db/subplugins.php');
238                         foreach ($subplugins as $subplugintype => $subplugintyperootdir) {
239                             $subplugin = new stdClass();
240                             $subplugin->type = $subplugintype;
241                             $subplugin->typerootdir = $subplugintyperootdir;
242                             $componentsubplugins[$subplugintype] = $subplugin;
243                         }
244                         $this->subpluginsinfo[$type . '_' . $component] = $componentsubplugins;
245                     }
246                 }
247             }
248         }
250         return $this->subpluginsinfo;
251     }
253     /**
254      * Returns the name of the plugin that defines the given subplugin type
255      *
256      * If the given subplugin type is not actually a subplugin, returns false.
257      *
258      * @param string $subplugintype the name of subplugin type, eg. workshopform or quiz
259      * @return false|string the name of the parent plugin, eg. mod_workshop
260      */
261     public function get_parent_of_subplugin($subplugintype) {
263         $parent = false;
264         foreach ($this->get_subplugins() as $pluginname => $subplugintypes) {
265             if (isset($subplugintypes[$subplugintype])) {
266                 $parent = $pluginname;
267                 break;
268             }
269         }
271         return $parent;
272     }
274     /**
275      * Returns a localized name of a given plugin
276      *
277      * @param string $component name of the plugin, eg mod_workshop or auth_ldap
278      * @return string
279      */
280     public function plugin_name($component) {
282         $pluginfo = $this->get_plugin_info($component);
284         if (is_null($pluginfo)) {
285             throw new moodle_exception('err_unknown_plugin', 'core_plugin', '', array('plugin' => $component));
286         }
288         return $pluginfo->displayname;
289     }
291     /**
292      * Returns a localized name of a plugin typed in singular form
293      *
294      * Most plugin types define their names in core_plugin lang file. In case of subplugins,
295      * we try to ask the parent plugin for the name. In the worst case, we will return
296      * the value of the passed $type parameter.
297      *
298      * @param string $type the type of the plugin, e.g. mod or workshopform
299      * @return string
300      */
301     public function plugintype_name($type) {
303         if (get_string_manager()->string_exists('type_' . $type, 'core_plugin')) {
304             // for most plugin types, their names are defined in core_plugin lang file
305             return get_string('type_' . $type, 'core_plugin');
307         } else if ($parent = $this->get_parent_of_subplugin($type)) {
308             // if this is a subplugin, try to ask the parent plugin for the name
309             if (get_string_manager()->string_exists('subplugintype_' . $type, $parent)) {
310                 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type, $parent);
311             } else {
312                 return $this->plugin_name($parent) . ' / ' . $type;
313             }
315         } else {
316             return $type;
317         }
318     }
320     /**
321      * Returns a localized name of a plugin type in plural form
322      *
323      * Most plugin types define their names in core_plugin lang file. In case of subplugins,
324      * we try to ask the parent plugin for the name. In the worst case, we will return
325      * the value of the passed $type parameter.
326      *
327      * @param string $type the type of the plugin, e.g. mod or workshopform
328      * @return string
329      */
330     public function plugintype_name_plural($type) {
332         if (get_string_manager()->string_exists('type_' . $type . '_plural', 'core_plugin')) {
333             // for most plugin types, their names are defined in core_plugin lang file
334             return get_string('type_' . $type . '_plural', 'core_plugin');
336         } else if ($parent = $this->get_parent_of_subplugin($type)) {
337             // if this is a subplugin, try to ask the parent plugin for the name
338             if (get_string_manager()->string_exists('subplugintype_' . $type . '_plural', $parent)) {
339                 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type . '_plural', $parent);
340             } else {
341                 return $this->plugin_name($parent) . ' / ' . $type;
342             }
344         } else {
345             return $type;
346         }
347     }
349     /**
350      * Returns information about the known plugin, or null
351      *
352      * @param string $component frankenstyle component name.
353      * @param bool $disablecache force reload, cache can be used otherwise
354      * @return plugininfo_base|null the corresponding plugin information.
355      */
356     public function get_plugin_info($component, $disablecache=false) {
357         list($type, $name) = $this->normalize_component($component);
358         $plugins = $this->get_plugins($disablecache);
359         if (isset($plugins[$type][$name])) {
360             return $plugins[$type][$name];
361         } else {
362             return null;
363         }
364     }
366     /**
367      * Check to see if the current version of the plugin seems to be a checkout of an external repository.
368      *
369      * @see available_update_deployer::plugin_external_source()
370      * @param string $component frankenstyle component name
371      * @return false|string
372      */
373     public function plugin_external_source($component) {
375         $plugininfo = $this->get_plugin_info($component);
377         if (is_null($plugininfo)) {
378             return false;
379         }
381         $pluginroot = $plugininfo->rootdir;
383         if (is_dir($pluginroot.'/.git')) {
384             return 'git';
385         }
387         if (is_dir($pluginroot.'/CVS')) {
388             return 'cvs';
389         }
391         if (is_dir($pluginroot.'/.svn')) {
392             return 'svn';
393         }
395         return false;
396     }
398     /**
399      * Get a list of any other plugins that require this one.
400      * @param string $component frankenstyle component name.
401      * @return array of frankensyle component names that require this one.
402      */
403     public function other_plugins_that_require($component) {
404         $others = array();
405         foreach ($this->get_plugins() as $type => $plugins) {
406             foreach ($plugins as $plugin) {
407                 $required = $plugin->get_other_required_plugins();
408                 if (isset($required[$component])) {
409                     $others[] = $plugin->component;
410                 }
411             }
412         }
413         return $others;
414     }
416     /**
417      * Check a dependencies list against the list of installed plugins.
418      * @param array $dependencies compenent name to required version or ANY_VERSION.
419      * @return bool true if all the dependencies are satisfied.
420      */
421     public function are_dependencies_satisfied($dependencies) {
422         foreach ($dependencies as $component => $requiredversion) {
423             $otherplugin = $this->get_plugin_info($component);
424             if (is_null($otherplugin)) {
425                 return false;
426             }
428             if ($requiredversion != ANY_VERSION and $otherplugin->versiondisk < $requiredversion) {
429                 return false;
430             }
431         }
433         return true;
434     }
436     /**
437      * Checks all dependencies for all installed plugins
438      *
439      * This is used by install and upgrade. The array passed by reference as the second
440      * argument is populated with the list of plugins that have failed dependencies (note that
441      * a single plugin can appear multiple times in the $failedplugins).
442      *
443      * @param int $moodleversion the version from version.php.
444      * @param array $failedplugins to return the list of plugins with non-satisfied dependencies
445      * @return bool true if all the dependencies are satisfied for all plugins.
446      */
447     public function all_plugins_ok($moodleversion, &$failedplugins = array()) {
449         $return = true;
450         foreach ($this->get_plugins() as $type => $plugins) {
451             foreach ($plugins as $plugin) {
453                 if (!$plugin->is_core_dependency_satisfied($moodleversion)) {
454                     $return = false;
455                     $failedplugins[] = $plugin->component;
456                 }
458                 if (!$this->are_dependencies_satisfied($plugin->get_other_required_plugins())) {
459                     $return = false;
460                     $failedplugins[] = $plugin->component;
461                 }
462             }
463         }
465         return $return;
466     }
468     /**
469      * Is it possible to uninstall the given plugin?
470      *
471      * False is returned if the plugininfo subclass declares the uninstall should
472      * not be allowed via {@link plugininfo_base::is_uninstall_allowed()} or if the
473      * core vetoes it (e.g. becase the plugin or some of its subplugins is required
474      * by some other installed plugin).
475      *
476      * @param string $component full frankenstyle name, e.g. mod_foobar
477      * @return bool
478      */
479     public function can_uninstall_plugin($component) {
481         $pluginfo = $this->get_plugin_info($component);
483         if (is_null($pluginfo)) {
484             return false;
485         }
487         if (!$this->common_uninstall_check($pluginfo)) {
488             return false;
489         }
491         // If it has subplugins, check they can be uninstalled too.
492         $subplugins = $this->get_subplugins_of_plugin($pluginfo->component);
493         foreach ($subplugins as $subpluginfo) {
494             if (!$this->common_uninstall_check($subpluginfo)) {
495                 return false;
496             }
497             // Check if there are some other plugins requiring this subplugin
498             // (but the parent and siblings).
499             foreach ($this->other_plugins_that_require($subpluginfo->component) as $requiresme) {
500                 $ismyparent = ($pluginfo->component === $requiresme);
501                 $ismysibling = in_array($requiresme, array_keys($subplugins));
502                 if (!$ismyparent and !$ismysibling) {
503                     return false;
504                 }
505             }
506         }
508         // Check if there are some other plugins requiring this plugin
509         // (but its subplugins).
510         foreach ($this->other_plugins_that_require($pluginfo->component) as $requiresme) {
511             $ismysubplugin = in_array($requiresme, array_keys($subplugins));
512             if (!$ismysubplugin) {
513                 return false;
514             }
515         }
517         return true;
518     }
520     /**
521      * Returns uninstall URL if exists.
522      *
523      * @param string $component
524      * @return moodle_url uninstall URL, null if uninstall not supported
525      */
526     public function get_uninstall_url($component) {
527         if (!$this->can_uninstall_plugin($component)) {
528             return null;
529         }
531         $pluginfo = $this->get_plugin_info($component);
533         if (is_null($pluginfo)) {
534             return null;
535         }
537         return $pluginfo->get_uninstall_url();
538     }
540     /**
541      * Uninstall the given plugin.
542      *
543      * Automatically cleans-up all remaining configuration data, log records, events,
544      * files from the file pool etc.
545      *
546      * In the future, the functionality of {@link uninstall_plugin()} function may be moved
547      * into this method and all the code should be refactored to use it. At the moment, we
548      * mimic this future behaviour by wrapping that function call.
549      *
550      * @param string $component
551      * @param progress_trace $progress traces the process
552      * @return bool true on success, false on errors/problems
553      */
554     public function uninstall_plugin($component, progress_trace $progress) {
556         $pluginfo = $this->get_plugin_info($component);
558         if (is_null($pluginfo)) {
559             return false;
560         }
562         // Give the pluginfo class a chance to execute some steps.
563         $result = $pluginfo->uninstall($progress);
564         if (!$result) {
565             return false;
566         }
568         // Call the legacy core function to uninstall the plugin.
569         ob_start();
570         uninstall_plugin($pluginfo->type, $pluginfo->name);
571         $progress->output(ob_get_clean());
573         return true;
574     }
576     /**
577      * Checks if there are some plugins with a known available update
578      *
579      * @return bool true if there is at least one available update
580      */
581     public function some_plugins_updatable() {
582         foreach ($this->get_plugins() as $type => $plugins) {
583             foreach ($plugins as $plugin) {
584                 if ($plugin->available_updates()) {
585                     return true;
586                 }
587             }
588         }
590         return false;
591     }
593     /**
594      * Check to see if the given plugin folder can be removed by the web server process.
595      *
596      * @param string $component full frankenstyle component
597      * @return bool
598      */
599     public function is_plugin_folder_removable($component) {
601         $pluginfo = $this->get_plugin_info($component);
603         if (is_null($pluginfo)) {
604             return false;
605         }
607         // To be able to remove the plugin folder, its parent must be writable, too.
608         if (!is_writable(dirname($pluginfo->rootdir))) {
609             return false;
610         }
612         // Check that the folder and all its content is writable (thence removable).
613         return $this->is_directory_removable($pluginfo->rootdir);
614     }
616     /**
617      * Defines a list of all plugins that were originally shipped in the standard Moodle distribution,
618      * but are not anymore and are deleted during upgrades.
619      *
620      * The main purpose of this list is to hide missing plugins during upgrade.
621      *
622      * @param string $type plugin type
623      * @param string $name plugin name
624      * @return bool
625      */
626     public static function is_deleted_standard_plugin($type, $name) {
628         // Example of the array structure:
629         // $plugins = array(
630         //     'block' => array('admin', 'admin_tree'),
631         //     'mod' => array('assignment'),
632         // );
633         // Do not include plugins that were removed during upgrades to versions that are
634         // not supported as source versions for upgrade any more. For example, at MOODLE_23_STABLE
635         // branch, listed should be no plugins that were removed at 1.9.x - 2.1.x versions as
636         // Moodle 2.3 supports upgrades from 2.2.x only.
637         $plugins = array(
638             'qformat' => array('blackboard'),
639         );
641         if (!isset($plugins[$type])) {
642             return false;
643         }
644         return in_array($name, $plugins[$type]);
645     }
647     /**
648      * Defines a white list of all plugins shipped in the standard Moodle distribution
649      *
650      * @param string $type
651      * @return false|array array of standard plugins or false if the type is unknown
652      */
653     public static function standard_plugins_list($type) {
654         $standard_plugins = array(
656             'assignment' => array(
657                 'offline', 'online', 'upload', 'uploadsingle'
658             ),
660             'assignsubmission' => array(
661                 'comments', 'file', 'onlinetext'
662             ),
664             'assignfeedback' => array(
665                 'comments', 'file', 'offline'
666             ),
668             'auth' => array(
669                 'cas', 'db', 'email', 'fc', 'imap', 'ldap', 'manual', 'mnet',
670                 'nntp', 'nologin', 'none', 'pam', 'pop3', 'radius',
671                 'shibboleth', 'webservice'
672             ),
674             'block' => array(
675                 'activity_modules', 'admin_bookmarks', 'badges', 'blog_menu',
676                 'blog_recent', 'blog_tags', 'calendar_month',
677                 'calendar_upcoming', 'comments', 'community',
678                 'completionstatus', 'course_list', 'course_overview',
679                 'course_summary', 'feedback', 'glossary_random', 'html',
680                 'login', 'mentees', 'messages', 'mnet_hosts', 'myprofile',
681                 'navigation', 'news_items', 'online_users', 'participants',
682                 'private_files', 'quiz_results', 'recent_activity',
683                 'rss_client', 'search_forums', 'section_links',
684                 'selfcompletion', 'settings', 'site_main_menu',
685                 'social_activities', 'tag_flickr', 'tag_youtube', 'tags'
686             ),
688             'booktool' => array(
689                 'exportimscp', 'importhtml', 'print'
690             ),
692             'cachelock' => array(
693                 'file'
694             ),
696             'cachestore' => array(
697                 'file', 'memcache', 'memcached', 'mongodb', 'session', 'static'
698             ),
700             'coursereport' => array(
701                 //deprecated!
702             ),
704             'datafield' => array(
705                 'checkbox', 'date', 'file', 'latlong', 'menu', 'multimenu',
706                 'number', 'picture', 'radiobutton', 'text', 'textarea', 'url'
707             ),
709             'datapreset' => array(
710                 'imagegallery'
711             ),
713             'editor' => array(
714                 'textarea', 'tinymce'
715             ),
717             'enrol' => array(
718                 'authorize', 'category', 'cohort', 'database', 'flatfile',
719                 'guest', 'imsenterprise', 'ldap', 'manual', 'meta', 'mnet',
720                 'paypal', 'self'
721             ),
723             'filter' => array(
724                 'activitynames', 'algebra', 'censor', 'emailprotect',
725                 'emoticon', 'mediaplugin', 'multilang', 'tex', 'tidy',
726                 'urltolink', 'data', 'glossary'
727             ),
729             'format' => array(
730                 'scorm', 'social', 'topics', 'weeks'
731             ),
733             'gradeexport' => array(
734                 'ods', 'txt', 'xls', 'xml'
735             ),
737             'gradeimport' => array(
738                 'csv', 'xml'
739             ),
741             'gradereport' => array(
742                 'grader', 'outcomes', 'overview', 'user'
743             ),
745             'gradingform' => array(
746                 'rubric', 'guide'
747             ),
749             'local' => array(
750             ),
752             'message' => array(
753                 'email', 'jabber', 'popup'
754             ),
756             'mnetservice' => array(
757                 'enrol'
758             ),
760             'mod' => array(
761                 'assign', 'assignment', 'book', 'chat', 'choice', 'data', 'feedback', 'folder',
762                 'forum', 'glossary', 'imscp', 'label', 'lesson', 'lti', 'page',
763                 'quiz', 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop'
764             ),
766             'plagiarism' => array(
767             ),
769             'portfolio' => array(
770                 'boxnet', 'download', 'flickr', 'googledocs', 'mahara', 'picasa'
771             ),
773             'profilefield' => array(
774                 'checkbox', 'datetime', 'menu', 'text', 'textarea'
775             ),
777             'qbehaviour' => array(
778                 'adaptive', 'adaptivenopenalty', 'deferredcbm',
779                 'deferredfeedback', 'immediatecbm', 'immediatefeedback',
780                 'informationitem', 'interactive', 'interactivecountback',
781                 'manualgraded', 'missing'
782             ),
784             'qformat' => array(
785                 'aiken', 'blackboard_six', 'examview', 'gift',
786                 'learnwise', 'missingword', 'multianswer', 'webct',
787                 'xhtml', 'xml'
788             ),
790             'qtype' => array(
791                 'calculated', 'calculatedmulti', 'calculatedsimple',
792                 'description', 'essay', 'match', 'missingtype', 'multianswer',
793                 'multichoice', 'numerical', 'random', 'randomsamatch',
794                 'shortanswer', 'truefalse'
795             ),
797             'quiz' => array(
798                 'grading', 'overview', 'responses', 'statistics'
799             ),
801             'quizaccess' => array(
802                 'delaybetweenattempts', 'ipaddress', 'numattempts', 'openclosedate',
803                 'password', 'safebrowser', 'securewindow', 'timelimit'
804             ),
806             'report' => array(
807                 'backups', 'completion', 'configlog', 'courseoverview',
808                 'log', 'loglive', 'outline', 'participation', 'progress', 'questioninstances', 'security', 'stats', 'performance'
809             ),
811             'repository' => array(
812                 'alfresco', 'boxnet', 'coursefiles', 'dropbox', 'equella', 'filesystem',
813                 'flickr', 'flickr_public', 'googledocs', 'local', 'merlot',
814                 'picasa', 'recent', 's3', 'upload', 'url', 'user', 'webdav',
815                 'wikimedia', 'youtube'
816             ),
818             'scormreport' => array(
819                 'basic',
820                 'interactions',
821                 'graphs'
822             ),
824             'tinymce' => array(
825                 'ctrlhelp', 'dragmath', 'moodleemoticon', 'moodleimage', 'moodlemedia', 'moodlenolink', 'spellchecker',
826                 'pdw', 'wrap'
827             ),
829             'theme' => array(
830                 'afterburner', 'anomaly', 'arialist', 'base', 'binarius', 'bootstrapbase',
831                 'boxxie', 'brick', 'canvas', 'clean', 'formal_white', 'formfactor',
832                 'fusion', 'leatherbound', 'magazine', 'mymobile', 'nimble',
833                 'nonzero', 'overlay', 'serenity', 'sky_high', 'splash',
834                 'standard', 'standardold'
835             ),
837             'tool' => array(
838                 'assignmentupgrade', 'behat', 'capability', 'customlang',
839                 'dbtransfer', 'generator', 'health', 'innodb', 'installaddon',
840                 'langimport', 'multilangupgrade', 'phpunit', 'profiling',
841                 'qeupgradehelper', 'replace', 'spamcleaner', 'timezoneimport',
842                 'unittest', 'uploaduser', 'unsuproles', 'xmldb'
843             ),
845             'webservice' => array(
846                 'amf', 'rest', 'soap', 'xmlrpc'
847             ),
849             'workshopallocation' => array(
850                 'manual', 'random', 'scheduled'
851             ),
853             'workshopeval' => array(
854                 'best'
855             ),
857             'workshopform' => array(
858                 'accumulative', 'comments', 'numerrors', 'rubric'
859             )
860         );
862         if (isset($standard_plugins[$type])) {
863             return $standard_plugins[$type];
864         } else {
865             return false;
866         }
867     }
869     /**
870      * Wrapper for the core function {@link normalize_component()}.
871      *
872      * This is here just to make it possible to mock it in unit tests.
873      *
874      * @param string $component
875      * @return array
876      */
877     protected function normalize_component($component) {
878         return normalize_component($component);
879     }
881     /**
882      * Reorders plugin types into a sequence to be displayed
883      *
884      * For technical reasons, plugin types returned by {@link get_plugin_types()} are
885      * in a certain order that does not need to fit the expected order for the display.
886      * Particularly, activity modules should be displayed first as they represent the
887      * real heart of Moodle. They should be followed by other plugin types that are
888      * used to build the courses (as that is what one expects from LMS). After that,
889      * other supportive plugin types follow.
890      *
891      * @param array $types associative array
892      * @return array same array with altered order of items
893      */
894     protected function reorder_plugin_types(array $types) {
895         $fix = array(
896             'mod'        => $types['mod'],
897             'block'      => $types['block'],
898             'qtype'      => $types['qtype'],
899             'qbehaviour' => $types['qbehaviour'],
900             'qformat'    => $types['qformat'],
901             'filter'     => $types['filter'],
902             'enrol'      => $types['enrol'],
903         );
904         foreach ($types as $type => $path) {
905             if (!isset($fix[$type])) {
906                 $fix[$type] = $path;
907             }
908         }
909         return $fix;
910     }
912     /**
913      * Check if the given directory can be removed by the web server process.
914      *
915      * This recursively checks that the given directory and all its contents
916      * it writable.
917      *
918      * @param string $fullpath
919      * @return boolean
920      */
921     protected function is_directory_removable($fullpath) {
923         if (!is_writable($fullpath)) {
924             return false;
925         }
927         if (is_dir($fullpath)) {
928             $handle = opendir($fullpath);
929         } else {
930             return false;
931         }
933         $result = true;
935         while ($filename = readdir($handle)) {
937             if ($filename === '.' or $filename === '..') {
938                 continue;
939             }
941             $subfilepath = $fullpath.'/'.$filename;
943             if (is_dir($subfilepath)) {
944                 $result = $result && $this->is_directory_removable($subfilepath);
946             } else {
947                 $result = $result && is_writable($subfilepath);
948             }
949         }
951         closedir($handle);
953         return $result;
954     }
956     /**
957      * Helper method that implements common uninstall prerequisities
958      *
959      * @param plugininfo_base $pluginfo
960      * @return bool
961      */
962     protected function common_uninstall_check(plugininfo_base $pluginfo) {
964         if (!$pluginfo->is_uninstall_allowed()) {
965             // The plugin's plugininfo class declares it should not be uninstalled.
966             return false;
967         }
969         if ($pluginfo->get_status() === plugin_manager::PLUGIN_STATUS_NEW) {
970             // The plugin is not installed. It should be either installed or removed from the disk.
971             // Relying on this temporary state may be tricky.
972             return false;
973         }
975         if (is_null($pluginfo->get_uninstall_url())) {
976             // Backwards compatibility.
977             debugging('plugininfo_base subclasses should use is_uninstall_allowed() instead of returning null in get_uninstall_url()',
978                 DEBUG_DEVELOPER);
979             return false;
980         }
982         return true;
983     }
987 /**
988  * General exception thrown by the {@link available_update_checker} class
989  */
990 class available_update_checker_exception extends moodle_exception {
992     /**
993      * @param string $errorcode exception description identifier
994      * @param mixed $debuginfo debugging data to display
995      */
996     public function __construct($errorcode, $debuginfo=null) {
997         parent::__construct($errorcode, 'core_plugin', '', null, print_r($debuginfo, true));
998     }
1002 /**
1003  * Singleton class that handles checking for available updates
1004  */
1005 class available_update_checker {
1007     /** @var available_update_checker holds the singleton instance */
1008     protected static $singletoninstance;
1009     /** @var null|int the timestamp of when the most recent response was fetched */
1010     protected $recentfetch = null;
1011     /** @var null|array the recent response from the update notification provider */
1012     protected $recentresponse = null;
1013     /** @var null|string the numerical version of the local Moodle code */
1014     protected $currentversion = null;
1015     /** @var null|string the release info of the local Moodle code */
1016     protected $currentrelease = null;
1017     /** @var null|string branch of the local Moodle code */
1018     protected $currentbranch = null;
1019     /** @var array of (string)frankestyle => (string)version list of additional plugins deployed at this site */
1020     protected $currentplugins = array();
1022     /**
1023      * Direct initiation not allowed, use the factory method {@link self::instance()}
1024      */
1025     protected function __construct() {
1026     }
1028     /**
1029      * Sorry, this is singleton
1030      */
1031     protected function __clone() {
1032     }
1034     /**
1035      * Factory method for this class
1036      *
1037      * @return available_update_checker the singleton instance
1038      */
1039     public static function instance() {
1040         if (is_null(self::$singletoninstance)) {
1041             self::$singletoninstance = new self();
1042         }
1043         return self::$singletoninstance;
1044     }
1046     /**
1047      * Reset any caches
1048      * @param bool $phpunitreset
1049      */
1050     public static function reset_caches($phpunitreset = false) {
1051         if ($phpunitreset) {
1052             self::$singletoninstance = null;
1053         }
1054     }
1056     /**
1057      * Returns the timestamp of the last execution of {@link fetch()}
1058      *
1059      * @return int|null null if it has never been executed or we don't known
1060      */
1061     public function get_last_timefetched() {
1063         $this->restore_response();
1065         if (!empty($this->recentfetch)) {
1066             return $this->recentfetch;
1068         } else {
1069             return null;
1070         }
1071     }
1073     /**
1074      * Fetches the available update status from the remote site
1075      *
1076      * @throws available_update_checker_exception
1077      */
1078     public function fetch() {
1079         $response = $this->get_response();
1080         $this->validate_response($response);
1081         $this->store_response($response);
1082     }
1084     /**
1085      * Returns the available update information for the given component
1086      *
1087      * This method returns null if the most recent response does not contain any information
1088      * about it. The returned structure is an array of available updates for the given
1089      * component. Each update info is an object with at least one property called
1090      * 'version'. Other possible properties are 'release', 'maturity', 'url' and 'downloadurl'.
1091      *
1092      * For the 'core' component, the method returns real updates only (those with higher version).
1093      * For all other components, the list of all known remote updates is returned and the caller
1094      * (usually the {@link plugin_manager}) is supposed to make the actual comparison of versions.
1095      *
1096      * @param string $component frankenstyle
1097      * @param array $options with supported keys 'minmaturity' and/or 'notifybuilds'
1098      * @return null|array null or array of available_update_info objects
1099      */
1100     public function get_update_info($component, array $options = array()) {
1102         if (!isset($options['minmaturity'])) {
1103             $options['minmaturity'] = 0;
1104         }
1106         if (!isset($options['notifybuilds'])) {
1107             $options['notifybuilds'] = false;
1108         }
1110         if ($component == 'core') {
1111             $this->load_current_environment();
1112         }
1114         $this->restore_response();
1116         if (empty($this->recentresponse['updates'][$component])) {
1117             return null;
1118         }
1120         $updates = array();
1121         foreach ($this->recentresponse['updates'][$component] as $info) {
1122             $update = new available_update_info($component, $info);
1123             if (isset($update->maturity) and ($update->maturity < $options['minmaturity'])) {
1124                 continue;
1125             }
1126             if ($component == 'core') {
1127                 if ($update->version <= $this->currentversion) {
1128                     continue;
1129                 }
1130                 if (empty($options['notifybuilds']) and $this->is_same_release($update->release)) {
1131                     continue;
1132                 }
1133             }
1134             $updates[] = $update;
1135         }
1137         if (empty($updates)) {
1138             return null;
1139         }
1141         return $updates;
1142     }
1144     /**
1145      * The method being run via cron.php
1146      */
1147     public function cron() {
1148         global $CFG;
1150         if (!$this->cron_autocheck_enabled()) {
1151             $this->cron_mtrace('Automatic check for available updates not enabled, skipping.');
1152             return;
1153         }
1155         $now = $this->cron_current_timestamp();
1157         if ($this->cron_has_fresh_fetch($now)) {
1158             $this->cron_mtrace('Recently fetched info about available updates is still fresh enough, skipping.');
1159             return;
1160         }
1162         if ($this->cron_has_outdated_fetch($now)) {
1163             $this->cron_mtrace('Outdated or missing info about available updates, forced fetching ... ', '');
1164             $this->cron_execute();
1165             return;
1166         }
1168         $offset = $this->cron_execution_offset();
1169         $start = mktime(1, 0, 0, date('n', $now), date('j', $now), date('Y', $now)); // 01:00 AM today local time
1170         if ($now > $start + $offset) {
1171             $this->cron_mtrace('Regular daily check for available updates ... ', '');
1172             $this->cron_execute();
1173             return;
1174         }
1175     }
1177     /// end of public API //////////////////////////////////////////////////////
1179     /**
1180      * Makes cURL request to get data from the remote site
1181      *
1182      * @return string raw request result
1183      * @throws available_update_checker_exception
1184      */
1185     protected function get_response() {
1186         global $CFG;
1187         require_once($CFG->libdir.'/filelib.php');
1189         $curl = new curl(array('proxy' => true));
1190         $response = $curl->post($this->prepare_request_url(), $this->prepare_request_params(), $this->prepare_request_options());
1191         $curlerrno = $curl->get_errno();
1192         if (!empty($curlerrno)) {
1193             throw new available_update_checker_exception('err_response_curl', 'cURL error '.$curlerrno.': '.$curl->error);
1194         }
1195         $curlinfo = $curl->get_info();
1196         if ($curlinfo['http_code'] != 200) {
1197             throw new available_update_checker_exception('err_response_http_code', $curlinfo['http_code']);
1198         }
1199         return $response;
1200     }
1202     /**
1203      * Makes sure the response is valid, has correct API format etc.
1204      *
1205      * @param string $response raw response as returned by the {@link self::get_response()}
1206      * @throws available_update_checker_exception
1207      */
1208     protected function validate_response($response) {
1210         $response = $this->decode_response($response);
1212         if (empty($response)) {
1213             throw new available_update_checker_exception('err_response_empty');
1214         }
1216         if (empty($response['status']) or $response['status'] !== 'OK') {
1217             throw new available_update_checker_exception('err_response_status', $response['status']);
1218         }
1220         if (empty($response['apiver']) or $response['apiver'] !== '1.2') {
1221             throw new available_update_checker_exception('err_response_format_version', $response['apiver']);
1222         }
1224         if (empty($response['forbranch']) or $response['forbranch'] !== moodle_major_version(true)) {
1225             throw new available_update_checker_exception('err_response_target_version', $response['forbranch']);
1226         }
1227     }
1229     /**
1230      * Decodes the raw string response from the update notifications provider
1231      *
1232      * @param string $response as returned by {@link self::get_response()}
1233      * @return array decoded response structure
1234      */
1235     protected function decode_response($response) {
1236         return json_decode($response, true);
1237     }
1239     /**
1240      * Stores the valid fetched response for later usage
1241      *
1242      * This implementation uses the config_plugins table as the permanent storage.
1243      *
1244      * @param string $response raw valid data returned by {@link self::get_response()}
1245      */
1246     protected function store_response($response) {
1248         set_config('recentfetch', time(), 'core_plugin');
1249         set_config('recentresponse', $response, 'core_plugin');
1251         $this->restore_response(true);
1252     }
1254     /**
1255      * Loads the most recent raw response record we have fetched
1256      *
1257      * After this method is called, $this->recentresponse is set to an array. If the
1258      * array is empty, then either no data have been fetched yet or the fetched data
1259      * do not have expected format (and thence they are ignored and a debugging
1260      * message is displayed).
1261      *
1262      * This implementation uses the config_plugins table as the permanent storage.
1263      *
1264      * @param bool $forcereload reload even if it was already loaded
1265      */
1266     protected function restore_response($forcereload = false) {
1268         if (!$forcereload and !is_null($this->recentresponse)) {
1269             // we already have it, nothing to do
1270             return;
1271         }
1273         $config = get_config('core_plugin');
1275         if (!empty($config->recentresponse) and !empty($config->recentfetch)) {
1276             try {
1277                 $this->validate_response($config->recentresponse);
1278                 $this->recentfetch = $config->recentfetch;
1279                 $this->recentresponse = $this->decode_response($config->recentresponse);
1280             } catch (available_update_checker_exception $e) {
1281                 // The server response is not valid. Behave as if no data were fetched yet.
1282                 // This may happen when the most recent update info (cached locally) has been
1283                 // fetched with the previous branch of Moodle (like during an upgrade from 2.x
1284                 // to 2.y) or when the API of the response has changed.
1285                 $this->recentresponse = array();
1286             }
1288         } else {
1289             $this->recentresponse = array();
1290         }
1291     }
1293     /**
1294      * Compares two raw {@link $recentresponse} records and returns the list of changed updates
1295      *
1296      * This method is used to populate potential update info to be sent to site admins.
1297      *
1298      * @param array $old
1299      * @param array $new
1300      * @throws available_update_checker_exception
1301      * @return array parts of $new['updates'] that have changed
1302      */
1303     protected function compare_responses(array $old, array $new) {
1305         if (empty($new)) {
1306             return array();
1307         }
1309         if (!array_key_exists('updates', $new)) {
1310             throw new available_update_checker_exception('err_response_format');
1311         }
1313         if (empty($old)) {
1314             return $new['updates'];
1315         }
1317         if (!array_key_exists('updates', $old)) {
1318             throw new available_update_checker_exception('err_response_format');
1319         }
1321         $changes = array();
1323         foreach ($new['updates'] as $newcomponent => $newcomponentupdates) {
1324             if (empty($old['updates'][$newcomponent])) {
1325                 $changes[$newcomponent] = $newcomponentupdates;
1326                 continue;
1327             }
1328             foreach ($newcomponentupdates as $newcomponentupdate) {
1329                 $inold = false;
1330                 foreach ($old['updates'][$newcomponent] as $oldcomponentupdate) {
1331                     if ($newcomponentupdate['version'] == $oldcomponentupdate['version']) {
1332                         $inold = true;
1333                     }
1334                 }
1335                 if (!$inold) {
1336                     if (!isset($changes[$newcomponent])) {
1337                         $changes[$newcomponent] = array();
1338                     }
1339                     $changes[$newcomponent][] = $newcomponentupdate;
1340                 }
1341             }
1342         }
1344         return $changes;
1345     }
1347     /**
1348      * Returns the URL to send update requests to
1349      *
1350      * During the development or testing, you can set $CFG->alternativeupdateproviderurl
1351      * to a custom URL that will be used. Otherwise the standard URL will be returned.
1352      *
1353      * @return string URL
1354      */
1355     protected function prepare_request_url() {
1356         global $CFG;
1358         if (!empty($CFG->config_php_settings['alternativeupdateproviderurl'])) {
1359             return $CFG->config_php_settings['alternativeupdateproviderurl'];
1360         } else {
1361             return 'https://download.moodle.org/api/1.2/updates.php';
1362         }
1363     }
1365     /**
1366      * Sets the properties currentversion, currentrelease, currentbranch and currentplugins
1367      *
1368      * @param bool $forcereload
1369      */
1370     protected function load_current_environment($forcereload=false) {
1371         global $CFG;
1373         if (!is_null($this->currentversion) and !$forcereload) {
1374             // nothing to do
1375             return;
1376         }
1378         $version = null;
1379         $release = null;
1381         require($CFG->dirroot.'/version.php');
1382         $this->currentversion = $version;
1383         $this->currentrelease = $release;
1384         $this->currentbranch = moodle_major_version(true);
1386         $pluginman = plugin_manager::instance();
1387         foreach ($pluginman->get_plugins() as $type => $plugins) {
1388             foreach ($plugins as $plugin) {
1389                 if (!$plugin->is_standard()) {
1390                     $this->currentplugins[$plugin->component] = $plugin->versiondisk;
1391                 }
1392             }
1393         }
1394     }
1396     /**
1397      * Returns the list of HTTP params to be sent to the updates provider URL
1398      *
1399      * @return array of (string)param => (string)value
1400      */
1401     protected function prepare_request_params() {
1402         global $CFG;
1404         $this->load_current_environment();
1405         $this->restore_response();
1407         $params = array();
1408         $params['format'] = 'json';
1410         if (isset($this->recentresponse['ticket'])) {
1411             $params['ticket'] = $this->recentresponse['ticket'];
1412         }
1414         if (isset($this->currentversion)) {
1415             $params['version'] = $this->currentversion;
1416         } else {
1417             throw new coding_exception('Main Moodle version must be already known here');
1418         }
1420         if (isset($this->currentbranch)) {
1421             $params['branch'] = $this->currentbranch;
1422         } else {
1423             throw new coding_exception('Moodle release must be already known here');
1424         }
1426         $plugins = array();
1427         foreach ($this->currentplugins as $plugin => $version) {
1428             $plugins[] = $plugin.'@'.$version;
1429         }
1430         if (!empty($plugins)) {
1431             $params['plugins'] = implode(',', $plugins);
1432         }
1434         return $params;
1435     }
1437     /**
1438      * Returns the list of cURL options to use when fetching available updates data
1439      *
1440      * @return array of (string)param => (string)value
1441      */
1442     protected function prepare_request_options() {
1443         global $CFG;
1445         $options = array(
1446             'CURLOPT_SSL_VERIFYHOST' => 2,      // this is the default in {@link curl} class but just in case
1447             'CURLOPT_SSL_VERIFYPEER' => true,
1448         );
1450         return $options;
1451     }
1453     /**
1454      * Returns the current timestamp
1455      *
1456      * @return int the timestamp
1457      */
1458     protected function cron_current_timestamp() {
1459         return time();
1460     }
1462     /**
1463      * Output cron debugging info
1464      *
1465      * @see mtrace()
1466      * @param string $msg output message
1467      * @param string $eol end of line
1468      */
1469     protected function cron_mtrace($msg, $eol = PHP_EOL) {
1470         mtrace($msg, $eol);
1471     }
1473     /**
1474      * Decide if the autocheck feature is disabled in the server setting
1475      *
1476      * @return bool true if autocheck enabled, false if disabled
1477      */
1478     protected function cron_autocheck_enabled() {
1479         global $CFG;
1481         if (empty($CFG->updateautocheck)) {
1482             return false;
1483         } else {
1484             return true;
1485         }
1486     }
1488     /**
1489      * Decide if the recently fetched data are still fresh enough
1490      *
1491      * @param int $now current timestamp
1492      * @return bool true if no need to re-fetch, false otherwise
1493      */
1494     protected function cron_has_fresh_fetch($now) {
1495         $recent = $this->get_last_timefetched();
1497         if (empty($recent)) {
1498             return false;
1499         }
1501         if ($now < $recent) {
1502             $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1503             return true;
1504         }
1506         if ($now - $recent > 24 * HOURSECS) {
1507             return false;
1508         }
1510         return true;
1511     }
1513     /**
1514      * Decide if the fetch is outadated or even missing
1515      *
1516      * @param int $now current timestamp
1517      * @return bool false if no need to re-fetch, true otherwise
1518      */
1519     protected function cron_has_outdated_fetch($now) {
1520         $recent = $this->get_last_timefetched();
1522         if (empty($recent)) {
1523             return true;
1524         }
1526         if ($now < $recent) {
1527             $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1528             return false;
1529         }
1531         if ($now - $recent > 48 * HOURSECS) {
1532             return true;
1533         }
1535         return false;
1536     }
1538     /**
1539      * Returns the cron execution offset for this site
1540      *
1541      * The main {@link self::cron()} is supposed to run every night in some random time
1542      * between 01:00 and 06:00 AM (local time). The exact moment is defined by so called
1543      * execution offset, that is the amount of time after 01:00 AM. The offset value is
1544      * initially generated randomly and then used consistently at the site. This way, the
1545      * regular checks against the download.moodle.org server are spread in time.
1546      *
1547      * @return int the offset number of seconds from range 1 sec to 5 hours
1548      */
1549     protected function cron_execution_offset() {
1550         global $CFG;
1552         if (empty($CFG->updatecronoffset)) {
1553             set_config('updatecronoffset', rand(1, 5 * HOURSECS));
1554         }
1556         return $CFG->updatecronoffset;
1557     }
1559     /**
1560      * Fetch available updates info and eventually send notification to site admins
1561      */
1562     protected function cron_execute() {
1564         try {
1565             $this->restore_response();
1566             $previous = $this->recentresponse;
1567             $this->fetch();
1568             $this->restore_response(true);
1569             $current = $this->recentresponse;
1570             $changes = $this->compare_responses($previous, $current);
1571             $notifications = $this->cron_notifications($changes);
1572             $this->cron_notify($notifications);
1573             $this->cron_mtrace('done');
1574         } catch (available_update_checker_exception $e) {
1575             $this->cron_mtrace('FAILED!');
1576         }
1577     }
1579     /**
1580      * Given the list of changes in available updates, pick those to send to site admins
1581      *
1582      * @param array $changes as returned by {@link self::compare_responses()}
1583      * @return array of available_update_info objects to send to site admins
1584      */
1585     protected function cron_notifications(array $changes) {
1586         global $CFG;
1588         $notifications = array();
1589         $pluginman = plugin_manager::instance();
1590         $plugins = $pluginman->get_plugins(true);
1592         foreach ($changes as $component => $componentchanges) {
1593             if (empty($componentchanges)) {
1594                 continue;
1595             }
1596             $componentupdates = $this->get_update_info($component,
1597                 array('minmaturity' => $CFG->updateminmaturity, 'notifybuilds' => $CFG->updatenotifybuilds));
1598             if (empty($componentupdates)) {
1599                 continue;
1600             }
1601             // notify only about those $componentchanges that are present in $componentupdates
1602             // to respect the preferences
1603             foreach ($componentchanges as $componentchange) {
1604                 foreach ($componentupdates as $componentupdate) {
1605                     if ($componentupdate->version == $componentchange['version']) {
1606                         if ($component == 'core') {
1607                             // In case of 'core', we already know that the $componentupdate
1608                             // is a real update with higher version ({@see self::get_update_info()}).
1609                             // We just perform additional check for the release property as there
1610                             // can be two Moodle releases having the same version (e.g. 2.4.0 and 2.5dev shortly
1611                             // after the release). We can do that because we have the release info
1612                             // always available for the core.
1613                             if ((string)$componentupdate->release === (string)$componentchange['release']) {
1614                                 $notifications[] = $componentupdate;
1615                             }
1616                         } else {
1617                             // Use the plugin_manager to check if the detected $componentchange
1618                             // is a real update with higher version. That is, the $componentchange
1619                             // is present in the array of {@link available_update_info} objects
1620                             // returned by the plugin's available_updates() method.
1621                             list($plugintype, $pluginname) = normalize_component($component);
1622                             if (!empty($plugins[$plugintype][$pluginname])) {
1623                                 $availableupdates = $plugins[$plugintype][$pluginname]->available_updates();
1624                                 if (!empty($availableupdates)) {
1625                                     foreach ($availableupdates as $availableupdate) {
1626                                         if ($availableupdate->version == $componentchange['version']) {
1627                                             $notifications[] = $componentupdate;
1628                                         }
1629                                     }
1630                                 }
1631                             }
1632                         }
1633                     }
1634                 }
1635             }
1636         }
1638         return $notifications;
1639     }
1641     /**
1642      * Sends the given notifications to site admins via messaging API
1643      *
1644      * @param array $notifications array of available_update_info objects to send
1645      */
1646     protected function cron_notify(array $notifications) {
1647         global $CFG;
1649         if (empty($notifications)) {
1650             return;
1651         }
1653         $admins = get_admins();
1655         if (empty($admins)) {
1656             return;
1657         }
1659         $this->cron_mtrace('sending notifications ... ', '');
1661         $text = get_string('updatenotifications', 'core_admin') . PHP_EOL;
1662         $html = html_writer::tag('h1', get_string('updatenotifications', 'core_admin')) . PHP_EOL;
1664         $coreupdates = array();
1665         $pluginupdates = array();
1667         foreach ($notifications as $notification) {
1668             if ($notification->component == 'core') {
1669                 $coreupdates[] = $notification;
1670             } else {
1671                 $pluginupdates[] = $notification;
1672             }
1673         }
1675         if (!empty($coreupdates)) {
1676             $text .= PHP_EOL . get_string('updateavailable', 'core_admin') . PHP_EOL;
1677             $html .= html_writer::tag('h2', get_string('updateavailable', 'core_admin')) . PHP_EOL;
1678             $html .= html_writer::start_tag('ul') . PHP_EOL;
1679             foreach ($coreupdates as $coreupdate) {
1680                 $html .= html_writer::start_tag('li');
1681                 if (isset($coreupdate->release)) {
1682                     $text .= get_string('updateavailable_release', 'core_admin', $coreupdate->release);
1683                     $html .= html_writer::tag('strong', get_string('updateavailable_release', 'core_admin', $coreupdate->release));
1684                 }
1685                 if (isset($coreupdate->version)) {
1686                     $text .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
1687                     $html .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
1688                 }
1689                 if (isset($coreupdate->maturity)) {
1690                     $text .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
1691                     $html .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
1692                 }
1693                 $text .= PHP_EOL;
1694                 $html .= html_writer::end_tag('li') . PHP_EOL;
1695             }
1696             $text .= PHP_EOL;
1697             $html .= html_writer::end_tag('ul') . PHP_EOL;
1699             $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/index.php');
1700             $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
1701             $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/index.php', $CFG->wwwroot.'/'.$CFG->admin.'/index.php'));
1702             $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
1703         }
1705         if (!empty($pluginupdates)) {
1706             $text .= PHP_EOL . get_string('updateavailableforplugin', 'core_admin') . PHP_EOL;
1707             $html .= html_writer::tag('h2', get_string('updateavailableforplugin', 'core_admin')) . PHP_EOL;
1709             $html .= html_writer::start_tag('ul') . PHP_EOL;
1710             foreach ($pluginupdates as $pluginupdate) {
1711                 $html .= html_writer::start_tag('li');
1712                 $text .= get_string('pluginname', $pluginupdate->component);
1713                 $html .= html_writer::tag('strong', get_string('pluginname', $pluginupdate->component));
1715                 $text .= ' ('.$pluginupdate->component.')';
1716                 $html .= ' ('.$pluginupdate->component.')';
1718                 $text .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
1719                 $html .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
1721                 $text .= PHP_EOL;
1722                 $html .= html_writer::end_tag('li') . PHP_EOL;
1723             }
1724             $text .= PHP_EOL;
1725             $html .= html_writer::end_tag('ul') . PHP_EOL;
1727             $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php');
1728             $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
1729             $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/plugins.php', $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php'));
1730             $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
1731         }
1733         $a = array('siteurl' => $CFG->wwwroot);
1734         $text .= get_string('updatenotificationfooter', 'core_admin', $a) . PHP_EOL;
1735         $a = array('siteurl' => html_writer::link($CFG->wwwroot, $CFG->wwwroot));
1736         $html .= html_writer::tag('footer', html_writer::tag('p', get_string('updatenotificationfooter', 'core_admin', $a),
1737             array('style' => 'font-size:smaller; color:#333;')));
1739         foreach ($admins as $admin) {
1740             $message = new stdClass();
1741             $message->component         = 'moodle';
1742             $message->name              = 'availableupdate';
1743             $message->userfrom          = get_admin();
1744             $message->userto            = $admin;
1745             $message->subject           = get_string('updatenotificationsubject', 'core_admin', array('siteurl' => $CFG->wwwroot));
1746             $message->fullmessage       = $text;
1747             $message->fullmessageformat = FORMAT_PLAIN;
1748             $message->fullmessagehtml   = $html;
1749             $message->smallmessage      = get_string('updatenotifications', 'core_admin');
1750             $message->notification      = 1;
1751             message_send($message);
1752         }
1753     }
1755     /**
1756      * Compare two release labels and decide if they are the same
1757      *
1758      * @param string $remote release info of the available update
1759      * @param null|string $local release info of the local code, defaults to $release defined in version.php
1760      * @return boolean true if the releases declare the same minor+major version
1761      */
1762     protected function is_same_release($remote, $local=null) {
1764         if (is_null($local)) {
1765             $this->load_current_environment();
1766             $local = $this->currentrelease;
1767         }
1769         $pattern = '/^([0-9\.\+]+)([^(]*)/';
1771         preg_match($pattern, $remote, $remotematches);
1772         preg_match($pattern, $local, $localmatches);
1774         $remotematches[1] = str_replace('+', '', $remotematches[1]);
1775         $localmatches[1] = str_replace('+', '', $localmatches[1]);
1777         if ($remotematches[1] === $localmatches[1] and rtrim($remotematches[2]) === rtrim($localmatches[2])) {
1778             return true;
1779         } else {
1780             return false;
1781         }
1782     }
1786 /**
1787  * Defines the structure of objects returned by {@link available_update_checker::get_update_info()}
1788  */
1789 class available_update_info {
1791     /** @var string frankenstyle component name */
1792     public $component;
1793     /** @var int the available version of the component */
1794     public $version;
1795     /** @var string|null optional release name */
1796     public $release = null;
1797     /** @var int|null optional maturity info, eg {@link MATURITY_STABLE} */
1798     public $maturity = null;
1799     /** @var string|null optional URL of a page with more info about the update */
1800     public $url = null;
1801     /** @var string|null optional URL of a ZIP package that can be downloaded and installed */
1802     public $download = null;
1803     /** @var string|null of self::download is set, then this must be the MD5 hash of the ZIP */
1804     public $downloadmd5 = null;
1806     /**
1807      * Creates new instance of the class
1808      *
1809      * The $info array must provide at least the 'version' value and optionally all other
1810      * values to populate the object's properties.
1811      *
1812      * @param string $name the frankenstyle component name
1813      * @param array $info associative array with other properties
1814      */
1815     public function __construct($name, array $info) {
1816         $this->component = $name;
1817         foreach ($info as $k => $v) {
1818             if (property_exists('available_update_info', $k) and $k != 'component') {
1819                 $this->$k = $v;
1820             }
1821         }
1822     }
1826 /**
1827  * Implements a communication bridge to the mdeploy.php utility
1828  */
1829 class available_update_deployer {
1831     const HTTP_PARAM_PREFIX     = 'updteautodpldata_';  // Hey, even Google has not heard of such a prefix! So it MUST be safe :-p
1832     const HTTP_PARAM_CHECKER    = 'datapackagesize';    // Name of the parameter that holds the number of items in the received data items
1834     /** @var available_update_deployer holds the singleton instance */
1835     protected static $singletoninstance;
1836     /** @var moodle_url URL of a page that includes the deployer UI */
1837     protected $callerurl;
1838     /** @var moodle_url URL to return after the deployment */
1839     protected $returnurl;
1841     /**
1842      * Direct instantiation not allowed, use the factory method {@link self::instance()}
1843      */
1844     protected function __construct() {
1845     }
1847     /**
1848      * Sorry, this is singleton
1849      */
1850     protected function __clone() {
1851     }
1853     /**
1854      * Factory method for this class
1855      *
1856      * @return available_update_deployer the singleton instance
1857      */
1858     public static function instance() {
1859         if (is_null(self::$singletoninstance)) {
1860             self::$singletoninstance = new self();
1861         }
1862         return self::$singletoninstance;
1863     }
1865     /**
1866      * Reset caches used by this script
1867      *
1868      * @param bool $phpunitreset is this called as a part of PHPUnit reset?
1869      */
1870     public static function reset_caches($phpunitreset = false) {
1871         if ($phpunitreset) {
1872             self::$singletoninstance = null;
1873         }
1874     }
1876     /**
1877      * Is automatic deployment enabled?
1878      *
1879      * @return bool
1880      */
1881     public function enabled() {
1882         global $CFG;
1884         if (!empty($CFG->disableupdateautodeploy)) {
1885             // The feature is prohibited via config.php
1886             return false;
1887         }
1889         return get_config('updateautodeploy');
1890     }
1892     /**
1893      * Sets some base properties of the class to make it usable.
1894      *
1895      * @param moodle_url $callerurl the base URL of a script that will handle the class'es form data
1896      * @param moodle_url $returnurl the final URL to return to when the deployment is finished
1897      */
1898     public function initialize(moodle_url $callerurl, moodle_url $returnurl) {
1900         if (!$this->enabled()) {
1901             throw new coding_exception('Unable to initialize the deployer, the feature is not enabled.');
1902         }
1904         $this->callerurl = $callerurl;
1905         $this->returnurl = $returnurl;
1906     }
1908     /**
1909      * Has the deployer been initialized?
1910      *
1911      * Initialized deployer means that the following properties were set:
1912      * callerurl, returnurl
1913      *
1914      * @return bool
1915      */
1916     public function initialized() {
1918         if (!$this->enabled()) {
1919             return false;
1920         }
1922         if (empty($this->callerurl)) {
1923             return false;
1924         }
1926         if (empty($this->returnurl)) {
1927             return false;
1928         }
1930         return true;
1931     }
1933     /**
1934      * Returns a list of reasons why the deployment can not happen
1935      *
1936      * If the returned array is empty, the deployment seems to be possible. The returned
1937      * structure is an associative array with keys representing individual impediments.
1938      * Possible keys are: missingdownloadurl, missingdownloadmd5, notwritable.
1939      *
1940      * @param available_update_info $info
1941      * @return array
1942      */
1943     public function deployment_impediments(available_update_info $info) {
1945         $impediments = array();
1947         if (empty($info->download)) {
1948             $impediments['missingdownloadurl'] = true;
1949         }
1951         if (empty($info->downloadmd5)) {
1952             $impediments['missingdownloadmd5'] = true;
1953         }
1955         if (!empty($info->download) and !$this->update_downloadable($info->download)) {
1956             $impediments['notdownloadable'] = true;
1957         }
1959         if (!$this->component_writable($info->component)) {
1960             $impediments['notwritable'] = true;
1961         }
1963         return $impediments;
1964     }
1966     /**
1967      * Check to see if the current version of the plugin seems to be a checkout of an external repository.
1968      *
1969      * @see plugin_manager::plugin_external_source()
1970      * @param available_update_info $info
1971      * @return false|string
1972      */
1973     public function plugin_external_source(available_update_info $info) {
1975         $paths = get_plugin_types(true);
1976         list($plugintype, $pluginname) = normalize_component($info->component);
1977         $pluginroot = $paths[$plugintype].'/'.$pluginname;
1979         if (is_dir($pluginroot.'/.git')) {
1980             return 'git';
1981         }
1983         if (is_dir($pluginroot.'/CVS')) {
1984             return 'cvs';
1985         }
1987         if (is_dir($pluginroot.'/.svn')) {
1988             return 'svn';
1989         }
1991         return false;
1992     }
1994     /**
1995      * Prepares a renderable widget to confirm installation of an available update.
1996      *
1997      * @param available_update_info $info component version to deploy
1998      * @return renderable
1999      */
2000     public function make_confirm_widget(available_update_info $info) {
2002         if (!$this->initialized()) {
2003             throw new coding_exception('Illegal method call - deployer not initialized.');
2004         }
2006         $params = $this->data_to_params(array(
2007             'updateinfo' => (array)$info,   // see http://www.php.net/manual/en/language.types.array.php#language.types.array.casting
2008         ));
2010         $widget = new single_button(
2011             new moodle_url($this->callerurl, $params),
2012             get_string('updateavailableinstall', 'core_admin'),
2013             'post'
2014         );
2016         return $widget;
2017     }
2019     /**
2020      * Prepares a renderable widget to execute installation of an available update.
2021      *
2022      * @param available_update_info $info component version to deploy
2023      * @param moodle_url $returnurl URL to return after the installation execution
2024      * @return renderable
2025      */
2026     public function make_execution_widget(available_update_info $info, moodle_url $returnurl = null) {
2027         global $CFG;
2029         if (!$this->initialized()) {
2030             throw new coding_exception('Illegal method call - deployer not initialized.');
2031         }
2033         $pluginrootpaths = get_plugin_types(true);
2035         list($plugintype, $pluginname) = normalize_component($info->component);
2037         if (empty($pluginrootpaths[$plugintype])) {
2038             throw new coding_exception('Unknown plugin type root location', $plugintype);
2039         }
2041         list($passfile, $password) = $this->prepare_authorization();
2043         if (is_null($returnurl)) {
2044             $returnurl = new moodle_url('/admin');
2045         } else {
2046             $returnurl = $returnurl;
2047         }
2049         $params = array(
2050             'upgrade' => true,
2051             'type' => $plugintype,
2052             'name' => $pluginname,
2053             'typeroot' => $pluginrootpaths[$plugintype],
2054             'package' => $info->download,
2055             'md5' => $info->downloadmd5,
2056             'dataroot' => $CFG->dataroot,
2057             'dirroot' => $CFG->dirroot,
2058             'passfile' => $passfile,
2059             'password' => $password,
2060             'returnurl' => $returnurl->out(false),
2061         );
2063         if (!empty($CFG->proxyhost)) {
2064             // MDL-36973 - Beware - we should call just !is_proxybypass() here. But currently, our
2065             // cURL wrapper class does not do it. So, to have consistent behaviour, we pass proxy
2066             // setting regardless the $CFG->proxybypass setting. Once the {@link curl} class is
2067             // fixed, the condition should be amended.
2068             if (true or !is_proxybypass($info->download)) {
2069                 if (empty($CFG->proxyport)) {
2070                     $params['proxy'] = $CFG->proxyhost;
2071                 } else {
2072                     $params['proxy'] = $CFG->proxyhost.':'.$CFG->proxyport;
2073                 }
2075                 if (!empty($CFG->proxyuser) and !empty($CFG->proxypassword)) {
2076                     $params['proxyuserpwd'] = $CFG->proxyuser.':'.$CFG->proxypassword;
2077                 }
2079                 if (!empty($CFG->proxytype)) {
2080                     $params['proxytype'] = $CFG->proxytype;
2081                 }
2082             }
2083         }
2085         $widget = new single_button(
2086             new moodle_url('/mdeploy.php', $params),
2087             get_string('updateavailableinstall', 'core_admin'),
2088             'post'
2089         );
2091         return $widget;
2092     }
2094     /**
2095      * Returns array of data objects passed to this tool.
2096      *
2097      * @return array
2098      */
2099     public function submitted_data() {
2101         $data = $this->params_to_data($_POST);
2103         if (empty($data) or empty($data[self::HTTP_PARAM_CHECKER])) {
2104             return false;
2105         }
2107         if (!empty($data['updateinfo']) and is_object($data['updateinfo'])) {
2108             $updateinfo = $data['updateinfo'];
2109             if (!empty($updateinfo->component) and !empty($updateinfo->version)) {
2110                 $data['updateinfo'] = new available_update_info($updateinfo->component, (array)$updateinfo);
2111             }
2112         }
2114         if (!empty($data['callerurl'])) {
2115             $data['callerurl'] = new moodle_url($data['callerurl']);
2116         }
2118         if (!empty($data['returnurl'])) {
2119             $data['returnurl'] = new moodle_url($data['returnurl']);
2120         }
2122         return $data;
2123     }
2125     /**
2126      * Handles magic getters and setters for protected properties.
2127      *
2128      * @param string $name method name, e.g. set_returnurl()
2129      * @param array $arguments arguments to be passed to the array
2130      */
2131     public function __call($name, array $arguments = array()) {
2133         if (substr($name, 0, 4) === 'set_') {
2134             $property = substr($name, 4);
2135             if (empty($property)) {
2136                 throw new coding_exception('Invalid property name (empty)');
2137             }
2138             if (empty($arguments)) {
2139                 $arguments = array(true); // Default value for flag-like properties.
2140             }
2141             // Make sure it is a protected property.
2142             $isprotected = false;
2143             $reflection = new ReflectionObject($this);
2144             foreach ($reflection->getProperties(ReflectionProperty::IS_PROTECTED) as $reflectionproperty) {
2145                 if ($reflectionproperty->getName() === $property) {
2146                     $isprotected = true;
2147                     break;
2148                 }
2149             }
2150             if (!$isprotected) {
2151                 throw new coding_exception('Unable to set property - it does not exist or it is not protected');
2152             }
2153             $value = reset($arguments);
2154             $this->$property = $value;
2155             return;
2156         }
2158         if (substr($name, 0, 4) === 'get_') {
2159             $property = substr($name, 4);
2160             if (empty($property)) {
2161                 throw new coding_exception('Invalid property name (empty)');
2162             }
2163             if (!empty($arguments)) {
2164                 throw new coding_exception('No parameter expected');
2165             }
2166             // Make sure it is a protected property.
2167             $isprotected = false;
2168             $reflection = new ReflectionObject($this);
2169             foreach ($reflection->getProperties(ReflectionProperty::IS_PROTECTED) as $reflectionproperty) {
2170                 if ($reflectionproperty->getName() === $property) {
2171                     $isprotected = true;
2172                     break;
2173                 }
2174             }
2175             if (!$isprotected) {
2176                 throw new coding_exception('Unable to get property - it does not exist or it is not protected');
2177             }
2178             return $this->$property;
2179         }
2180     }
2182     /**
2183      * Generates a random token and stores it in a file in moodledata directory.
2184      *
2185      * @return array of the (string)filename and (string)password in this order
2186      */
2187     public function prepare_authorization() {
2188         global $CFG;
2190         make_upload_directory('mdeploy/auth/');
2192         $attempts = 0;
2193         $success = false;
2195         while (!$success and $attempts < 5) {
2196             $attempts++;
2198             $passfile = $this->generate_passfile();
2199             $password = $this->generate_password();
2200             $now = time();
2202             $filepath = $CFG->dataroot.'/mdeploy/auth/'.$passfile;
2204             if (!file_exists($filepath)) {
2205                 $success = file_put_contents($filepath, $password . PHP_EOL . $now . PHP_EOL, LOCK_EX);
2206             }
2207         }
2209         if ($success) {
2210             return array($passfile, $password);
2212         } else {
2213             throw new moodle_exception('unable_prepare_authorization', 'core_plugin');
2214         }
2215     }
2217     // End of external API
2219     /**
2220      * Prepares an array of HTTP parameters that can be passed to another page.
2221      *
2222      * @param array|object $data associative array or an object holding the data, data JSON-able
2223      * @return array suitable as a param for moodle_url
2224      */
2225     protected function data_to_params($data) {
2227         // Append some our own data
2228         if (!empty($this->callerurl)) {
2229             $data['callerurl'] = $this->callerurl->out(false);
2230         }
2231         if (!empty($this->returnurl)) {
2232             $data['returnurl'] = $this->returnurl->out(false);
2233         }
2235         // Finally append the count of items in the package.
2236         $data[self::HTTP_PARAM_CHECKER] = count($data);
2238         // Generate params
2239         $params = array();
2240         foreach ($data as $name => $value) {
2241             $transname = self::HTTP_PARAM_PREFIX.$name;
2242             $transvalue = json_encode($value);
2243             $params[$transname] = $transvalue;
2244         }
2246         return $params;
2247     }
2249     /**
2250      * Converts HTTP parameters passed to the script into native PHP data
2251      *
2252      * @param array $params such as $_REQUEST or $_POST
2253      * @return array data passed for this class
2254      */
2255     protected function params_to_data(array $params) {
2257         if (empty($params)) {
2258             return array();
2259         }
2261         $data = array();
2262         foreach ($params as $name => $value) {
2263             if (strpos($name, self::HTTP_PARAM_PREFIX) === 0) {
2264                 $realname = substr($name, strlen(self::HTTP_PARAM_PREFIX));
2265                 $realvalue = json_decode($value);
2266                 $data[$realname] = $realvalue;
2267             }
2268         }
2270         return $data;
2271     }
2273     /**
2274      * Returns a random string to be used as a filename of the password storage.
2275      *
2276      * @return string
2277      */
2278     protected function generate_passfile() {
2279         return clean_param(uniqid('mdeploy_', true), PARAM_FILE);
2280     }
2282     /**
2283      * Returns a random string to be used as the authorization token
2284      *
2285      * @return string
2286      */
2287     protected function generate_password() {
2288         return complex_random_string();
2289     }
2291     /**
2292      * Checks if the given component's directory is writable
2293      *
2294      * For the purpose of the deployment, the web server process has to have
2295      * write access to all files in the component's directory (recursively) and for the
2296      * directory itself.
2297      *
2298      * @see worker::move_directory_source_precheck()
2299      * @param string $component normalized component name
2300      * @return boolean
2301      */
2302     protected function component_writable($component) {
2304         list($plugintype, $pluginname) = normalize_component($component);
2306         $directory = get_plugin_directory($plugintype, $pluginname);
2308         if (is_null($directory)) {
2309             throw new coding_exception('Unknown component location', $component);
2310         }
2312         return $this->directory_writable($directory);
2313     }
2315     /**
2316      * Checks if the mdeploy.php will be able to fetch the ZIP from the given URL
2317      *
2318      * This is mainly supposed to check if the transmission over HTTPS would
2319      * work. That is, if the CA certificates are present at the server.
2320      *
2321      * @param string $downloadurl the URL of the ZIP package to download
2322      * @return bool
2323      */
2324     protected function update_downloadable($downloadurl) {
2325         global $CFG;
2327         $curloptions = array(
2328             'CURLOPT_SSL_VERIFYHOST' => 2,      // this is the default in {@link curl} class but just in case
2329             'CURLOPT_SSL_VERIFYPEER' => true,
2330         );
2332         $curl = new curl(array('proxy' => true));
2333         $result = $curl->head($downloadurl, $curloptions);
2334         $errno = $curl->get_errno();
2335         if (empty($errno)) {
2336             return true;
2337         } else {
2338             return false;
2339         }
2340     }
2342     /**
2343      * Checks if the directory and all its contents (recursively) is writable
2344      *
2345      * @param string $path full path to a directory
2346      * @return boolean
2347      */
2348     private function directory_writable($path) {
2350         if (!is_writable($path)) {
2351             return false;
2352         }
2354         if (is_dir($path)) {
2355             $handle = opendir($path);
2356         } else {
2357             return false;
2358         }
2360         $result = true;
2362         while ($filename = readdir($handle)) {
2363             $filepath = $path.'/'.$filename;
2365             if ($filename === '.' or $filename === '..') {
2366                 continue;
2367             }
2369             if (is_dir($filepath)) {
2370                 $result = $result && $this->directory_writable($filepath);
2372             } else {
2373                 $result = $result && is_writable($filepath);
2374             }
2375         }
2377         closedir($handle);
2379         return $result;
2380     }
2384 /**
2385  * Factory class producing required subclasses of {@link plugininfo_base}
2386  */
2387 class plugininfo_default_factory {
2389     /**
2390      * Makes a new instance of the plugininfo class
2391      *
2392      * @param string $type the plugin type, eg. 'mod'
2393      * @param string $typerootdir full path to the location of all the plugins of this type
2394      * @param string $name the plugin name, eg. 'workshop'
2395      * @param string $namerootdir full path to the location of the plugin
2396      * @param string $typeclass the name of class that holds the info about the plugin
2397      * @return plugininfo_base the instance of $typeclass
2398      */
2399     public static function make($type, $typerootdir, $name, $namerootdir, $typeclass) {
2400         $plugin              = new $typeclass();
2401         $plugin->type        = $type;
2402         $plugin->typerootdir = $typerootdir;
2403         $plugin->name        = $name;
2404         $plugin->rootdir     = $namerootdir;
2406         $plugin->init_display_name();
2407         $plugin->load_disk_version();
2408         $plugin->load_db_version();
2409         $plugin->load_required_main_version();
2410         $plugin->init_is_standard();
2412         return $plugin;
2413     }
2417 /**
2418  * Base class providing access to the information about a plugin
2419  *
2420  * @property-read string component the component name, type_name
2421  */
2422 abstract class plugininfo_base {
2424     /** @var string the plugintype name, eg. mod, auth or workshopform */
2425     public $type;
2426     /** @var string full path to the location of all the plugins of this type */
2427     public $typerootdir;
2428     /** @var string the plugin name, eg. assignment, ldap */
2429     public $name;
2430     /** @var string the localized plugin name */
2431     public $displayname;
2432     /** @var string the plugin source, one of plugin_manager::PLUGIN_SOURCE_xxx constants */
2433     public $source;
2434     /** @var fullpath to the location of this plugin */
2435     public $rootdir;
2436     /** @var int|string the version of the plugin's source code */
2437     public $versiondisk;
2438     /** @var int|string the version of the installed plugin */
2439     public $versiondb;
2440     /** @var int|float|string required version of Moodle core  */
2441     public $versionrequires;
2442     /** @var array other plugins that this one depends on, lazy-loaded by {@link get_other_required_plugins()} */
2443     public $dependencies;
2444     /** @var int number of instances of the plugin - not supported yet */
2445     public $instances;
2446     /** @var int order of the plugin among other plugins of the same type - not supported yet */
2447     public $sortorder;
2448     /** @var array|null array of {@link available_update_info} for this plugin */
2449     public $availableupdates;
2451     /**
2452      * Gathers and returns the information about all plugins of the given type
2453      *
2454      * @param string $type the name of the plugintype, eg. mod, auth or workshopform
2455      * @param string $typerootdir full path to the location of the plugin dir
2456      * @param string $typeclass the name of the actually called class
2457      * @return array of plugintype classes, indexed by the plugin name
2458      */
2459     public static function get_plugins($type, $typerootdir, $typeclass) {
2461         // get the information about plugins at the disk
2462         $plugins = get_plugin_list($type);
2463         $ondisk = array();
2464         foreach ($plugins as $pluginname => $pluginrootdir) {
2465             $ondisk[$pluginname] = plugininfo_default_factory::make($type, $typerootdir,
2466                 $pluginname, $pluginrootdir, $typeclass);
2467         }
2468         return $ondisk;
2469     }
2471     /**
2472      * Sets {@link $displayname} property to a localized name of the plugin
2473      */
2474     public function init_display_name() {
2475         if (!get_string_manager()->string_exists('pluginname', $this->component)) {
2476             $this->displayname = '[pluginname,' . $this->component . ']';
2477         } else {
2478             $this->displayname = get_string('pluginname', $this->component);
2479         }
2480     }
2482     /**
2483      * Magic method getter, redirects to read only values.
2484      *
2485      * @param string $name
2486      * @return mixed
2487      */
2488     public function __get($name) {
2489         switch ($name) {
2490             case 'component': return $this->type . '_' . $this->name;
2492             default:
2493                 debugging('Invalid plugin property accessed! '.$name);
2494                 return null;
2495         }
2496     }
2498     /**
2499      * Return the full path name of a file within the plugin.
2500      *
2501      * No check is made to see if the file exists.
2502      *
2503      * @param string $relativepath e.g. 'version.php'.
2504      * @return string e.g. $CFG->dirroot . '/mod/quiz/version.php'.
2505      */
2506     public function full_path($relativepath) {
2507         if (empty($this->rootdir)) {
2508             return '';
2509         }
2510         return $this->rootdir . '/' . $relativepath;
2511     }
2513     /**
2514      * Load the data from version.php.
2515      *
2516      * @param bool $disablecache do not attempt to obtain data from the cache
2517      * @return stdClass the object called $plugin defined in version.php
2518      */
2519     protected function load_version_php($disablecache=false) {
2521         $cache = cache::make('core', 'plugininfo_base');
2523         $versionsphp = $cache->get('versions_php');
2525         if (!$disablecache and $versionsphp !== false and isset($versionsphp[$this->component])) {
2526             return $versionsphp[$this->component];
2527         }
2529         $versionfile = $this->full_path('version.php');
2531         $plugin = new stdClass();
2532         if (is_readable($versionfile)) {
2533             include($versionfile);
2534         }
2535         $versionsphp[$this->component] = $plugin;
2536         $cache->set('versions_php', $versionsphp);
2538         return $plugin;
2539     }
2541     /**
2542      * Sets {@link $versiondisk} property to a numerical value representing the
2543      * version of the plugin's source code.
2544      *
2545      * If the value is null after calling this method, either the plugin
2546      * does not use versioning (typically does not have any database
2547      * data) or is missing from disk.
2548      */
2549     public function load_disk_version() {
2550         $plugin = $this->load_version_php();
2551         if (isset($plugin->version)) {
2552             $this->versiondisk = $plugin->version;
2553         }
2554     }
2556     /**
2557      * Sets {@link $versionrequires} property to a numerical value representing
2558      * the version of Moodle core that this plugin requires.
2559      */
2560     public function load_required_main_version() {
2561         $plugin = $this->load_version_php();
2562         if (isset($plugin->requires)) {
2563             $this->versionrequires = $plugin->requires;
2564         }
2565     }
2567     /**
2568      * Initialise {@link $dependencies} to the list of other plugins (in any)
2569      * that this one requires to be installed.
2570      */
2571     protected function load_other_required_plugins() {
2572         $plugin = $this->load_version_php();
2573         if (!empty($plugin->dependencies)) {
2574             $this->dependencies = $plugin->dependencies;
2575         } else {
2576             $this->dependencies = array(); // By default, no dependencies.
2577         }
2578     }
2580     /**
2581      * Get the list of other plugins that this plugin requires to be installed.
2582      *
2583      * @return array with keys the frankenstyle plugin name, and values either
2584      *      a version string (like '2011101700') or the constant ANY_VERSION.
2585      */
2586     public function get_other_required_plugins() {
2587         if (is_null($this->dependencies)) {
2588             $this->load_other_required_plugins();
2589         }
2590         return $this->dependencies;
2591     }
2593     /**
2594      * Is this is a subplugin?
2595      *
2596      * @return boolean
2597      */
2598     public function is_subplugin() {
2599         return ($this->get_parent_plugin() !== false);
2600     }
2602     /**
2603      * If I am a subplugin, return the name of my parent plugin.
2604      *
2605      * @return string|bool false if not a subplugin, name of the parent otherwise
2606      */
2607     public function get_parent_plugin() {
2608         return $this->get_plugin_manager()->get_parent_of_subplugin($this->type);
2609     }
2611     /**
2612      * Sets {@link $versiondb} property to a numerical value representing the
2613      * currently installed version of the plugin.
2614      *
2615      * If the value is null after calling this method, either the plugin
2616      * does not use versioning (typically does not have any database
2617      * data) or has not been installed yet.
2618      */
2619     public function load_db_version() {
2620         if ($ver = self::get_version_from_config_plugins($this->component)) {
2621             $this->versiondb = $ver;
2622         }
2623     }
2625     /**
2626      * Sets {@link $source} property to one of plugin_manager::PLUGIN_SOURCE_xxx
2627      * constants.
2628      *
2629      * If the property's value is null after calling this method, then
2630      * the type of the plugin has not been recognized and you should throw
2631      * an exception.
2632      */
2633     public function init_is_standard() {
2635         $standard = plugin_manager::standard_plugins_list($this->type);
2637         if ($standard !== false) {
2638             $standard = array_flip($standard);
2639             if (isset($standard[$this->name])) {
2640                 $this->source = plugin_manager::PLUGIN_SOURCE_STANDARD;
2641             } else if (!is_null($this->versiondb) and is_null($this->versiondisk)
2642                     and plugin_manager::is_deleted_standard_plugin($this->type, $this->name)) {
2643                 $this->source = plugin_manager::PLUGIN_SOURCE_STANDARD; // to be deleted
2644             } else {
2645                 $this->source = plugin_manager::PLUGIN_SOURCE_EXTENSION;
2646             }
2647         }
2648     }
2650     /**
2651      * Returns true if the plugin is shipped with the official distribution
2652      * of the current Moodle version, false otherwise.
2653      *
2654      * @return bool
2655      */
2656     public function is_standard() {
2657         return $this->source === plugin_manager::PLUGIN_SOURCE_STANDARD;
2658     }
2660     /**
2661      * Returns true if the the given Moodle version is enough to run this plugin
2662      *
2663      * @param string|int|double $moodleversion
2664      * @return bool
2665      */
2666     public function is_core_dependency_satisfied($moodleversion) {
2668         if (empty($this->versionrequires)) {
2669             return true;
2671         } else {
2672             return (double)$this->versionrequires <= (double)$moodleversion;
2673         }
2674     }
2676     /**
2677      * Returns the status of the plugin
2678      *
2679      * @return string one of plugin_manager::PLUGIN_STATUS_xxx constants
2680      */
2681     public function get_status() {
2683         if (is_null($this->versiondb) and is_null($this->versiondisk)) {
2684             return plugin_manager::PLUGIN_STATUS_NODB;
2686         } else if (is_null($this->versiondb) and !is_null($this->versiondisk)) {
2687             return plugin_manager::PLUGIN_STATUS_NEW;
2689         } else if (!is_null($this->versiondb) and is_null($this->versiondisk)) {
2690             if (plugin_manager::is_deleted_standard_plugin($this->type, $this->name)) {
2691                 return plugin_manager::PLUGIN_STATUS_DELETE;
2692             } else {
2693                 return plugin_manager::PLUGIN_STATUS_MISSING;
2694             }
2696         } else if ((string)$this->versiondb === (string)$this->versiondisk) {
2697             return plugin_manager::PLUGIN_STATUS_UPTODATE;
2699         } else if ($this->versiondb < $this->versiondisk) {
2700             return plugin_manager::PLUGIN_STATUS_UPGRADE;
2702         } else if ($this->versiondb > $this->versiondisk) {
2703             return plugin_manager::PLUGIN_STATUS_DOWNGRADE;
2705         } else {
2706             // $version = pi(); and similar funny jokes - hopefully Donald E. Knuth will never contribute to Moodle ;-)
2707             throw new coding_exception('Unable to determine plugin state, check the plugin versions');
2708         }
2709     }
2711     /**
2712      * Returns the information about plugin availability
2713      *
2714      * True means that the plugin is enabled. False means that the plugin is
2715      * disabled. Null means that the information is not available, or the
2716      * plugin does not support configurable availability or the availability
2717      * can not be changed.
2718      *
2719      * @return null|bool
2720      */
2721     public function is_enabled() {
2722         return null;
2723     }
2725     /**
2726      * Populates the property {@link $availableupdates} with the information provided by
2727      * available update checker
2728      *
2729      * @param available_update_checker $provider the class providing the available update info
2730      */
2731     public function check_available_updates(available_update_checker $provider) {
2732         global $CFG;
2734         if (isset($CFG->updateminmaturity)) {
2735             $minmaturity = $CFG->updateminmaturity;
2736         } else {
2737             // this can happen during the very first upgrade to 2.3
2738             $minmaturity = MATURITY_STABLE;
2739         }
2741         $this->availableupdates = $provider->get_update_info($this->component,
2742             array('minmaturity' => $minmaturity));
2743     }
2745     /**
2746      * If there are updates for this plugin available, returns them.
2747      *
2748      * Returns array of {@link available_update_info} objects, if some update
2749      * is available. Returns null if there is no update available or if the update
2750      * availability is unknown.
2751      *
2752      * @return array|null
2753      */
2754     public function available_updates() {
2756         if (empty($this->availableupdates) or !is_array($this->availableupdates)) {
2757             return null;
2758         }
2760         $updates = array();
2762         foreach ($this->availableupdates as $availableupdate) {
2763             if ($availableupdate->version > $this->versiondisk) {
2764                 $updates[] = $availableupdate;
2765             }
2766         }
2768         if (empty($updates)) {
2769             return null;
2770         }
2772         return $updates;
2773     }
2775     /**
2776      * Returns the node name used in admin settings menu for this plugin settings (if applicable)
2777      *
2778      * @return null|string node name or null if plugin does not create settings node (default)
2779      */
2780     public function get_settings_section_name() {
2781         return null;
2782     }
2784     /**
2785      * Returns the URL of the plugin settings screen
2786      *
2787      * Null value means that the plugin either does not have the settings screen
2788      * or its location is not available via this library.
2789      *
2790      * @return null|moodle_url
2791      */
2792     public function get_settings_url() {
2793         $section = $this->get_settings_section_name();
2794         if ($section === null) {
2795             return null;
2796         }
2797         $settings = admin_get_root()->locate($section);
2798         if ($settings && $settings instanceof admin_settingpage) {
2799             return new moodle_url('/admin/settings.php', array('section' => $section));
2800         } else if ($settings && $settings instanceof admin_externalpage) {
2801             return new moodle_url($settings->url);
2802         } else {
2803             return null;
2804         }
2805     }
2807     /**
2808      * Loads plugin settings to the settings tree
2809      *
2810      * This function usually includes settings.php file in plugins folder.
2811      * Alternatively it can create a link to some settings page (instance of admin_externalpage)
2812      *
2813      * @param part_of_admin_tree $adminroot
2814      * @param string $parentnodename
2815      * @param bool $hassiteconfig whether the current user has moodle/site:config capability
2816      */
2817     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
2818     }
2820     /**
2821      * Should there be a way to uninstall the plugin via the administration UI
2822      *
2823      * By default, uninstallation is allowed for all non-standard add-ons. Subclasses
2824      * may want to override this to allow uninstallation of all plugins (simply by
2825      * returning true unconditionally). Subplugins follow their parent plugin's
2826      * decision by default.
2827      *
2828      * Note that even if true is returned, the core may still prohibit the uninstallation,
2829      * e.g. in case there are other plugins that depend on this one.
2830      *
2831      * @return boolean
2832      */
2833     public function is_uninstall_allowed() {
2835         if ($this->is_subplugin()) {
2836             return $this->get_plugin_manager()->get_plugin_info($this->get_parent_plugin())->is_uninstall_allowed();
2837         }
2839         if ($this->is_standard()) {
2840             return false;
2841         }
2843         return true;
2844     }
2846     /**
2847      * Optional extra warning before uninstallation, for example number of uses in courses.
2848      *
2849      * @return string
2850      */
2851     public function get_uninstall_extra_warning() {
2852         return '';
2853     }
2855     /**
2856      * Returns the URL of the screen where this plugin can be uninstalled
2857      *
2858      * Visiting that URL must be safe, that is a manual confirmation is needed
2859      * for actual uninstallation of the plugin. By default, URL to a common
2860      * uninstalling tool is returned.
2861      *
2862      * @return moodle_url
2863      */
2864     public function get_uninstall_url() {
2865         return $this->get_default_uninstall_url();
2866     }
2868     /**
2869      * Returns relative directory of the plugin with heading '/'
2870      *
2871      * @return string
2872      */
2873     public function get_dir() {
2874         global $CFG;
2876         return substr($this->rootdir, strlen($CFG->dirroot));
2877     }
2879     /**
2880      * Hook method to implement certain steps when uninstalling the plugin.
2881      *
2882      * This hook is called by {@link plugin_manager::uninstall_plugin()} so
2883      * it is basically usable only for those plugin types that use the default
2884      * uninstall tool provided by {@link self::get_default_uninstall_url()}.
2885      *
2886      * @param progress_trace $progress traces the process
2887      * @return bool true on success, false on failure
2888      */
2889     public function uninstall(progress_trace $progress) {
2890         return true;
2891     }
2893     /**
2894      * Returns URL to a script that handles common plugin uninstall procedure.
2895      *
2896      * This URL is suitable for plugins that do not have their own UI
2897      * for uninstalling.
2898      *
2899      * @return moodle_url
2900      */
2901     protected final function get_default_uninstall_url() {
2902         return new moodle_url('/admin/plugins.php', array(
2903             'sesskey' => sesskey(),
2904             'uninstall' => $this->component,
2905             'confirm' => 0,
2906         ));
2907     }
2909     /**
2910      * Provides access to plugin versions from the {config_plugins} table
2911      *
2912      * @param string $plugin plugin name
2913      * @param bool $disablecache do not attempt to obtain data from the cache
2914      * @return int|bool the stored value or false if not found
2915      */
2916     protected function get_version_from_config_plugins($plugin, $disablecache=false) {
2917         global $DB;
2919         $cache = cache::make('core', 'plugininfo_base');
2921         $pluginversions = $cache->get('versions_db');
2923         if ($pluginversions === false or $disablecache) {
2924             try {
2925                 $pluginversions = $DB->get_records_menu('config_plugins', array('name' => 'version'), 'plugin', 'plugin,value');
2926             } catch (dml_exception $e) {
2927                 // before install
2928                 $pluginversions = array();
2929             }
2930             $cache->set('versions_db', $pluginversions);
2931         }
2933         if (isset($pluginversions[$plugin])) {
2934             return $pluginversions[$plugin];
2935         } else {
2936             return false;
2937         }
2938     }
2940     /**
2941      * Provides access to the plugin_manager singleton.
2942      *
2943      * @return plugin_manmager
2944      */
2945     protected function get_plugin_manager() {
2946         return plugin_manager::instance();
2947     }
2951 /**
2952  * General class for all plugin types that do not have their own class
2953  */
2954 class plugininfo_general extends plugininfo_base {
2958 /**
2959  * Class for page side blocks
2960  */
2961 class plugininfo_block extends plugininfo_base {
2963     public static function get_plugins($type, $typerootdir, $typeclass) {
2965         // get the information about blocks at the disk
2966         $blocks = parent::get_plugins($type, $typerootdir, $typeclass);
2968         // add blocks missing from disk
2969         $blocksinfo = self::get_blocks_info();
2970         foreach ($blocksinfo as $blockname => $blockinfo) {
2971             if (isset($blocks[$blockname])) {
2972                 continue;
2973             }
2974             $plugin                 = new $typeclass();
2975             $plugin->type           = $type;
2976             $plugin->typerootdir    = $typerootdir;
2977             $plugin->name           = $blockname;
2978             $plugin->rootdir        = null;
2979             $plugin->displayname    = $blockname;
2980             $plugin->versiondb      = $blockinfo->version;
2981             $plugin->init_is_standard();
2983             $blocks[$blockname]   = $plugin;
2984         }
2986         return $blocks;
2987     }
2989     /**
2990      * Magic method getter, redirects to read only values.
2991      *
2992      * For block plugins pretends the object has 'visible' property for compatibility
2993      * with plugins developed for Moodle version below 2.4
2994      *
2995      * @param string $name
2996      * @return mixed
2997      */
2998     public function __get($name) {
2999         if ($name === 'visible') {
3000             debugging('This is now an instance of plugininfo_block, please use $block->is_enabled() instead of $block->visible', DEBUG_DEVELOPER);
3001             return ($this->is_enabled() !== false);
3002         }
3003         return parent::__get($name);
3004     }
3006     public function init_display_name() {
3008         if (get_string_manager()->string_exists('pluginname', 'block_' . $this->name)) {
3009             $this->displayname = get_string('pluginname', 'block_' . $this->name);
3011         } else if (($block = block_instance($this->name)) !== false) {
3012             $this->displayname = $block->get_title();
3014         } else {
3015             parent::init_display_name();
3016         }
3017     }
3019     public function load_db_version() {
3020         global $DB;
3022         $blocksinfo = self::get_blocks_info();
3023         if (isset($blocksinfo[$this->name]->version)) {
3024             $this->versiondb = $blocksinfo[$this->name]->version;
3025         }
3026     }
3028     public function is_enabled() {
3030         $blocksinfo = self::get_blocks_info();
3031         if (isset($blocksinfo[$this->name]->visible)) {
3032             if ($blocksinfo[$this->name]->visible) {
3033                 return true;
3034             } else {
3035                 return false;
3036             }
3037         } else {
3038             return parent::is_enabled();
3039         }
3040     }
3042     public function get_settings_section_name() {
3043         return 'blocksetting' . $this->name;
3044     }
3046     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3047         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3048         $ADMIN = $adminroot; // may be used in settings.php
3049         $block = $this; // also can be used inside settings.php
3050         $section = $this->get_settings_section_name();
3052         if (!$hassiteconfig || (($blockinstance = block_instance($this->name)) === false)) {
3053             return;
3054         }
3056         $settings = null;
3057         if ($blockinstance->has_config()) {
3058             if (file_exists($this->full_path('settings.php'))) {
3059                 $settings = new admin_settingpage($section, $this->displayname,
3060                         'moodle/site:config', $this->is_enabled() === false);
3061                 include($this->full_path('settings.php')); // this may also set $settings to null
3062             } else {
3063                 $blocksinfo = self::get_blocks_info();
3064                 $settingsurl = new moodle_url('/admin/block.php', array('block' => $blocksinfo[$this->name]->id));
3065                 $settings = new admin_externalpage($section, $this->displayname,
3066                         $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
3067             }
3068         }
3069         if ($settings) {
3070             $ADMIN->add($parentnodename, $settings);
3071         }
3072     }
3074     public function is_uninstall_allowed() {
3075         return true;
3076     }
3078     /**
3079      * Warnign with number of block instances.
3080      *
3081      * @return string
3082      */
3083     public function get_uninstall_extra_warning() {
3084         global $DB;
3086         if (!$count = $DB->count_records('block_instances', array('blockname'=>$this->name))) {
3087             return '';
3088         }
3090         return '<p>'.get_string('uninstallextraconfirmblock', 'core_plugin', array('instances'=>$count)).'</p>';
3091     }
3093     /**
3094      * Provides access to the records in {block} table
3095      *
3096      * @param bool $disablecache do not attempt to obtain data from the cache
3097      * @return array array of stdClasses
3098      */
3099     protected static function get_blocks_info($disablecache=false) {
3100         global $DB;
3102         $cache = cache::make('core', 'plugininfo_block');
3104         $blocktypes = $cache->get('blocktypes');
3106         if ($blocktypes === false or $disablecache) {
3107             try {
3108                 $blocktypes = $DB->get_records('block', null, 'name', 'name,id,version,visible');
3109             } catch (dml_exception $e) {
3110                 // before install
3111                 $blocktypes = array();
3112             }
3113             $cache->set('blocktypes', $blocktypes);
3114         }
3116         return $blocktypes;
3117     }
3121 /**
3122  * Class for text filters
3123  */
3124 class plugininfo_filter extends plugininfo_base {
3126     public static function get_plugins($type, $typerootdir, $typeclass) {
3127         global $CFG, $DB;
3129         $filters = array();
3131         // get the list of filters in /filter location
3132         $installed = filter_get_all_installed();
3134         foreach ($installed as $name => $displayname) {
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    = $displayname;
3142             $plugin->load_disk_version();
3143             $plugin->load_db_version();
3144             $plugin->load_required_main_version();
3145             $plugin->init_is_standard();
3147             $filters[$plugin->name] = $plugin;
3148         }
3150         // Do not mess with filter registration here!
3152         $globalstates = self::get_global_states();
3154         // make sure that all registered filters are installed, just in case
3155         foreach ($globalstates as $name => $info) {
3156             if (!isset($filters[$name])) {
3157                 // oops, there is a record in filter_active but the filter is not installed
3158                 $plugin                 = new $typeclass();
3159                 $plugin->type           = $type;
3160                 $plugin->typerootdir    = $typerootdir;
3161                 $plugin->name           = $name;
3162                 $plugin->rootdir        = "$CFG->dirroot/filter/$name";
3163                 $plugin->displayname    = $name;
3165                 $plugin->load_db_version();
3167                 if (is_null($plugin->versiondb)) {
3168                     // this is a hack to stimulate 'Missing from disk' error
3169                     // because $plugin->versiondisk will be null !== false
3170                     $plugin->versiondb = false;
3171                 }
3173                 $filters[$plugin->name] = $plugin;
3174             }
3175         }
3177         return $filters;
3178     }
3180     public function init_display_name() {
3181         // do nothing, the name is set in self::get_plugins()
3182     }
3184     public function is_enabled() {
3186         $globalstates = self::get_global_states();
3188         foreach ($globalstates as $name => $info) {
3189             if ($name === $this->name) {
3190                 if ($info->active == TEXTFILTER_DISABLED) {
3191                     return false;
3192                 } else {
3193                     // it may be 'On' or 'Off, but available'
3194                     return null;
3195                 }
3196             }
3197         }
3199         return null;
3200     }
3202     public function get_settings_section_name() {
3203         return 'filtersetting' . $this->name;
3204     }
3206     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3207         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3208         $ADMIN = $adminroot; // may be used in settings.php
3209         $filter = $this; // also can be used inside settings.php
3211         $settings = null;
3212         if ($hassiteconfig && file_exists($this->full_path('filtersettings.php'))) {
3213             $section = $this->get_settings_section_name();
3214             $settings = new admin_settingpage($section, $this->displayname,
3215                     'moodle/site:config', $this->is_enabled() === false);
3216             include($this->full_path('filtersettings.php')); // this may also set $settings to null
3217         }
3218         if ($settings) {
3219             $ADMIN->add($parentnodename, $settings);
3220         }
3221     }
3223     public function is_uninstall_allowed() {
3224         return true;
3225     }
3227     public function get_uninstall_url() {
3228         return new moodle_url('/admin/filters.php', array('sesskey' => sesskey(), 'filterpath' => $this->name, 'action' => 'delete'));
3229     }
3231     /**
3232      * Provides access to the results of {@link filter_get_global_states()}
3233      * but indexed by the normalized filter name
3234      *
3235      * The legacy filter name is available as ->legacyname property.
3236      *
3237      * @param bool $disablecache do not attempt to obtain data from the cache
3238      * @return array
3239      */
3240     protected static function get_global_states($disablecache=false) {
3241         global $DB;
3243         $cache = cache::make('core', 'plugininfo_filter');
3245         $globalstates = $cache->get('globalstates');
3247         if ($globalstates === false or $disablecache) {
3249             if (!$DB->get_manager()->table_exists('filter_active')) {
3250                 // Not installed yet.
3251                 $cache->set('globalstates', array());
3252                 return array();
3253             }
3255             $globalstates = array();
3257             foreach (filter_get_global_states() as $name => $info) {
3258                 if (strpos($name, '/') !== false) {
3259                     // Skip existing before upgrade to new names.
3260                     continue;
3261                 }
3263                 $filterinfo = new stdClass();
3264                 $filterinfo->active = $info->active;
3265                 $filterinfo->sortorder = $info->sortorder;
3266                 $globalstates[$name] = $filterinfo;
3267             }
3269             $cache->set('globalstates', $globalstates);
3270         }
3272         return $globalstates;
3273     }
3277 /**
3278  * Class for activity modules
3279  */
3280 class plugininfo_mod extends plugininfo_base {
3282     public static function get_plugins($type, $typerootdir, $typeclass) {
3284         // get the information about plugins at the disk
3285         $modules = parent::get_plugins($type, $typerootdir, $typeclass);
3287         // add modules missing from disk
3288         $modulesinfo = self::get_modules_info();
3289         foreach ($modulesinfo as $modulename => $moduleinfo) {
3290             if (isset($modules[$modulename])) {
3291                 continue;
3292             }
3293             $plugin                 = new $typeclass();
3294             $plugin->type           = $type;
3295             $plugin->typerootdir    = $typerootdir;
3296             $plugin->name           = $modulename;
3297             $plugin->rootdir        = null;
3298             $plugin->displayname    = $modulename;
3299             $plugin->versiondb      = $moduleinfo->version;
3300             $plugin->init_is_standard();
3302             $modules[$modulename]   = $plugin;
3303         }
3305         return $modules;
3306     }
3308     /**
3309      * Magic method getter, redirects to read only values.
3310      *
3311      * For module plugins we pretend the object has 'visible' property for compatibility
3312      * with plugins developed for Moodle version below 2.4
3313      *
3314      * @param string $name
3315      * @return mixed
3316      */
3317     public function __get($name) {
3318         if ($name === 'visible') {
3319             debugging('This is now an instance of plugininfo_mod, please use $module->is_enabled() instead of $module->visible', DEBUG_DEVELOPER);
3320             return ($this->is_enabled() !== false);
3321         }
3322         return parent::__get($name);
3323     }
3325     public function init_display_name() {
3326         if (get_string_manager()->string_exists('pluginname', $this->component)) {
3327             $this->displayname = get_string('pluginname', $this->component);
3328         } else {
3329             $this->displayname = get_string('modulename', $this->component);
3330         }
3331     }
3333     /**
3334      * Load the data from version.php.
3335      *
3336      * @param bool $disablecache do not attempt to obtain data from the cache
3337      * @return object the data object defined in version.php.
3338      */
3339     protected function load_version_php($disablecache=false) {
3341         $cache = cache::make('core', 'plugininfo_base');
3343         $versionsphp = $cache->get('versions_php');
3345         if (!$disablecache and $versionsphp !== false and isset($versionsphp[$this->component])) {
3346             return $versionsphp[$this->component];
3347         }
3349         $versionfile = $this->full_path('version.php');
3351         $module = new stdClass();
3352         $plugin = new stdClass();
3353         if (is_readable($versionfile)) {
3354             include($versionfile);
3355         }
3356         if (!isset($module->version) and isset($plugin->version)) {
3357             $module = $plugin;
3358         }
3359         $versionsphp[$this->component] = $module;
3360         $cache->set('versions_php', $versionsphp);
3362         return $module;
3363     }
3365     public function load_db_version() {
3366         global $DB;
3368         $modulesinfo = self::get_modules_info();
3369         if (isset($modulesinfo[$this->name]->version)) {
3370             $this->versiondb = $modulesinfo[$this->name]->version;
3371         }
3372     }
3374     public function is_enabled() {
3376         $modulesinfo = self::get_modules_info();
3377         if (isset($modulesinfo[$this->name]->visible)) {
3378             if ($modulesinfo[$this->name]->visible) {
3379                 return true;
3380             } else {
3381                 return false;
3382             }
3383         } else {
3384             return parent::is_enabled();
3385         }
3386     }
3388     public function get_settings_section_name() {
3389         return 'modsetting' . $this->name;
3390     }
3392     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3393         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3394         $ADMIN = $adminroot; // may be used in settings.php
3395         $module = $this; // also can be used inside settings.php
3396         $section = $this->get_settings_section_name();
3398         $modulesinfo = self::get_modules_info();
3399         $settings = null;
3400         if ($hassiteconfig && isset($modulesinfo[$this->name]) && file_exists($this->full_path('settings.php'))) {
3401             $settings = new admin_settingpage($section, $this->displayname,
3402                     'moodle/site:config', $this->is_enabled() === false);
3403             include($this->full_path('settings.php')); // this may also set $settings to null
3404         }
3405         if ($settings) {
3406             $ADMIN->add($parentnodename, $settings);
3407         }
3408     }
3410     /**
3411      * Allow all activity modules but Forum to be uninstalled.
3413      * This exception for the Forum has been hard-coded in Moodle since ages,
3414      * we may want to re-think it one day.
3415      */
3416     public function is_uninstall_allowed() {
3417         if ($this->name === 'forum') {
3418             return false;
3419         } else {
3420             return true;
3421         }
3422     }
3424     /**
3425      * Return warning with number of activities and number of affected courses.
3426      *
3427      * @return string
3428      */
3429     public function get_uninstall_extra_warning() {
3430         global $DB;
3432         if (!$module = $DB->get_record('modules', array('name'=>$this->name))) {
3433             return '';
3434         }
3436         if (!$count = $DB->count_records('course_modules', array('module'=>$module->id))) {
3437             return '';
3438         }
3440         $sql = "SELECT COUNT('x')
3441                   FROM (
3442                     SELECT course
3443                       FROM {course_modules}
3444                      WHERE module = :mid
3445                   GROUP BY course
3446                   ) c";
3447         $courses = $DB->count_records_sql($sql, array('mid'=>$module->id));
3449         return '<p>'.get_string('uninstallextraconfirmmod', 'core_plugin', array('instances'=>$count, 'courses'=>$courses)).'</p>';
3450     }
3452     /**
3453      * Provides access to the records in {modules} table
3454      *
3455      * @param bool $disablecache do not attempt to obtain data from the cache
3456      * @return array array of stdClasses
3457      */
3458     protected static function get_modules_info($disablecache=false) {
3459         global $DB;
3461         $cache = cache::make('core', 'plugininfo_mod');
3463         $modulesinfo = $cache->get('modulesinfo');
3465         if ($modulesinfo === false or $disablecache) {
3466             try {
3467                 $modulesinfo = $DB->get_records('modules', null, 'name', 'name,id,version,visible');
3468             } catch (dml_exception $e) {
3469                 // before install
3470                 $modulesinfo = array();
3471             }
3472             $cache->set('modulesinfo', $modulesinfo);
3473         }
3475         return $modulesinfo;
3476     }
3480 /**
3481  * Class for question behaviours.
3482  */
3483 class plugininfo_qbehaviour extends plugininfo_base {
3485     public function is_uninstall_allowed() {
3486         return true;
3487     }
3489     public function get_uninstall_url() {
3490         return new moodle_url('/admin/qbehaviours.php',
3491                 array('delete' => $this->name, 'sesskey' => sesskey()));
3492     }
3496 /**
3497  * Class for question types
3498  */
3499 class plugininfo_qtype extends plugininfo_base {
3501     public function is_uninstall_allowed() {
3502         return true;
3503     }
3505     public function get_uninstall_url() {
3506         return new moodle_url('/admin/qtypes.php',
3507                 array('delete' => $this->name, 'sesskey' => sesskey()));
3508     }
3510     public function get_settings_section_name() {
3511         return 'qtypesetting' . $this->name;
3512     }
3514     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3515         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3516         $ADMIN = $adminroot; // may be used in settings.php
3517         $qtype = $this; // also can be used inside settings.php
3518         $section = $this->get_settings_section_name();
3520         $settings = null;
3521         $systemcontext = context_system::instance();
3522         if (($hassiteconfig || has_capability('moodle/question:config', $systemcontext)) &&
3523                 file_exists($this->full_path('settings.php'))) {
3524             $settings = new admin_settingpage($section, $this->displayname,
3525                     'moodle/question:config', $this->is_enabled() === false);
3526             include($this->full_path('settings.php')); // this may also set $settings to null
3527         }
3528         if ($settings) {
3529             $ADMIN->add($parentnodename, $settings);
3530         }
3531     }
3535 /**
3536  * Class for authentication plugins
3537  */
3538 class plugininfo_auth extends plugininfo_base {
3540     public function is_enabled() {
3541         global $CFG;
3543         if (in_array($this->name, array('nologin', 'manual'))) {
3544             // these two are always enabled and can't be disabled
3545             return null;
3546         }
3548         $enabled = array_flip(explode(',', $CFG->auth));
3550         return isset($enabled[$this->name]);
3551     }
3553     public function get_settings_section_name() {
3554         return 'authsetting' . $this->name;
3555     }
3557     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3558         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3559         $ADMIN = $adminroot; // may be used in settings.php
3560         $auth = $this; // also to be used inside settings.php
3561         $section = $this->get_settings_section_name();
3563         $settings = null;
3564         if ($hassiteconfig) {
3565             if (file_exists($this->full_path('settings.php'))) {
3566                 // TODO: finish implementation of common settings - locking, etc.
3567                 $settings = new admin_settingpage($section, $this->displayname,
3568                         'moodle/site:config', $this->is_enabled() === false);
3569                 include($this->full_path('settings.php')); // this may also set $settings to null
3570             } else {
3571                 $settingsurl = new moodle_url('/admin/auth_config.php', array('auth' => $this->name));
3572                 $settings = new admin_externalpage($section, $this->displayname,
3573                         $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
3574             }
3575         }
3576         if ($settings) {
3577             $ADMIN->add($parentnodename, $settings);
3578         }
3579     }
3583 /**
3584  * Class for enrolment plugins
3585  */
3586 class plugininfo_enrol extends plugininfo_base {
3588     public function is_enabled() {
3589         global $CFG;
3591         // We do not actually need whole enrolment classes here so we do not call
3592         // {@link enrol_get_plugins()}. Note that this may produce slightly different
3593         // results, for example if the enrolment plugin does not contain lib.php
3594         // but it is listed in $CFG->enrol_plugins_enabled