8a5ce0fc462f2b8c8e72b4a5fbdb19fab27a1f1d
[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;
64     /** @var array list of installed plugins $name=>$version */
65     protected $installedplugins = null;
66     /** @var array list of all enabled plugins $name=>$name */
67     protected $enabledplugins = null;
68     /** @var array list of all enabled plugins $name=>$diskversion */
69     protected $presentplugins = null;
71     /**
72      * Direct initiation not allowed, use the factory method {@link self::instance()}
73      */
74     protected function __construct() {
75     }
77     /**
78      * Sorry, this is singleton
79      */
80     protected function __clone() {
81     }
83     /**
84      * Factory method for this class
85      *
86      * @return plugin_manager the singleton instance
87      */
88     public static function instance() {
89         if (is_null(self::$singletoninstance)) {
90             self::$singletoninstance = new self();
91         }
92         return self::$singletoninstance;
93     }
95     /**
96      * Reset all caches.
97      * @param bool $phpunitreset
98      */
99     public static function reset_caches($phpunitreset = false) {
100         if ($phpunitreset) {
101             self::$singletoninstance = null;
102         } else {
103             if (self::$singletoninstance) {
104                 self::$singletoninstance->pluginsinfo = null;
105                 self::$singletoninstance->subpluginsinfo = null;
106                 self::$singletoninstance->installedplugins = null;
107                 self::$singletoninstance->enabledplugins = null;
108                 self::$singletoninstance->presentplugins = null;
109             }
110         }
111         $cache = cache::make('core', 'plugin_manager');
112         $cache->purge();
113     }
115     /**
116      * Returns the result of {@link core_component::get_plugin_types()} ordered for humans
117      *
118      * @see self::reorder_plugin_types()
119      * @return array (string)name => (string)location
120      */
121     public function get_plugin_types() {
122         if (func_num_args() > 0) {
123             if (!func_get_arg(0)) {
124                 throw coding_exception('plugin_manager->get_plugin_types() does not support relative paths.');
125             }
126         }
127         return $this->reorder_plugin_types(core_component::get_plugin_types());
128     }
130     /**
131      * Load list of installed plugins,
132      * always call before using $this->installedplugins.
133      *
134      * This method is caching results for all plugins.
135      */
136     protected function load_installed_plugins() {
137         global $DB, $CFG;
139         if ($this->installedplugins) {
140             return;
141         }
143         if (empty($CFG->version)) {
144             // Nothing installed yet.
145             $this->installedplugins = array();
146             return;
147         }
149         $cache = cache::make('core', 'plugin_manager');
150         $installed = $cache->get('installed');
152         if (is_array($installed)) {
153             $this->installedplugins = $installed;
154             return;
155         }
157         $this->installedplugins = array();
159         if ($CFG->version < 2013092001.02) {
160             // We did not upgrade the database yet.
161             $modules = $DB->get_records('modules', array(), 'name ASC', 'id, name, version');
162             foreach ($modules as $module) {
163                 $this->installedplugins['mod'][$module->name] = $module->version;
164             }
165             $blocks = $DB->get_records('block', array(), 'name ASC', 'id, name, version');
166             foreach ($blocks as $block) {
167                 $this->installedplugins['block'][$block->name] = $block->version;
168             }
169         }
171         $versions = $DB->get_records('config_plugins', array('name'=>'version'));
172         foreach ($versions as $version) {
173             $parts = explode('_', $version->plugin, 2);
174             if (!isset($parts[1])) {
175                 // Invalid component, there must be at least one "_".
176                 continue;
177             }
178             // Do not verify here if plugin type and name are valid.
179             $this->installedplugins[$parts[0]][$parts[1]] = $version->value;
180         }
182         foreach ($this->installedplugins as $key => $value) {
183             ksort($this->installedplugins[$key]);
184         }
186         $cache->set('installed', $this->installedplugins);
187     }
189     /**
190      * Return list of installed plugins of given type.
191      * @param string $type
192      * @return array $name=>$version
193      */
194     public function get_installed_plugins($type) {
195         $this->load_installed_plugins();
196         if (isset($this->installedplugins[$type])) {
197             return $this->installedplugins[$type];
198         }
199         return array();
200     }
202     /**
203      * Load list of all enabled plugins,
204      * call before using $this->enabledplugins.
205      *
206      * This method is caching results from individual plugin info classes.
207      */
208     protected function load_enabled_plugins() {
209         global $CFG;
211         if ($this->enabledplugins) {
212             return;
213         }
215         if (empty($CFG->version)) {
216             $this->enabledplugins = array();
217             return;
218         }
220         $cache = cache::make('core', 'plugin_manager');
221         $enabled = $cache->get('enabled');
223         if (is_array($enabled)) {
224             $this->enabledplugins = $enabled;
225             return;
226         }
228         $this->enabledplugins = array();
230         require_once($CFG->libdir.'/adminlib.php');
232         $plugintypes = core_component::get_plugin_types();
233         foreach ($plugintypes as $plugintype => $fulldir) {
234             // Hack: include mod and editor subplugin management classes first,
235             //       the adminlib.php is supposed to contain extra admin settings too.
236             $plugininfoclass = 'plugininfo_' . $plugintype;
237             if (!class_exists($plugininfoclass) and file_exists("$fulldir/adminlib.php")) {
238                 include_once("$fulldir/adminlib.php");
239             }
240             if (class_exists($plugininfoclass)) {
241                 $enabled = $plugininfoclass::get_enabled_plugins();
242                 if (!is_array($enabled)) {
243                     continue;
244                 }
245                 $this->enabledplugins[$plugintype] = $enabled;
246             }
247         }
249         $cache->set('enabled', $this->enabledplugins);
250     }
252     /**
253      * Get list of enabled plugins of given type,
254      * the result may contain missing plugins.
255      *
256      * @param string $type
257      * @return array|null  list of enabled plugins of this type, null if unknown
258      */
259     public function get_enabled_plugins($type) {
260         $this->load_enabled_plugins();
261         if (isset($this->enabledplugins[$type])) {
262             return $this->enabledplugins[$type];
263         }
264         return null;
265     }
267     /**
268      * Load list of all present plugins - call before using $this->presentplugins.
269      */
270     protected function load_present_plugins() {
271         if ($this->presentplugins) {
272             return;
273         }
275         $cache = cache::make('core', 'plugin_manager');
276         $present = $cache->get('present');
278         if (is_array($present)) {
279             $this->presentplugins = $present;
280             return;
281         }
283         $this->presentplugins = array();
285         $plugintypes = core_component::get_plugin_types();
286         foreach ($plugintypes as $type => $typedir) {
287             $plugs = core_component::get_plugin_list($type);
288             foreach ($plugs as $plug => $fullplug) {
289                 $plugin = new stdClass();
290                 $plugin->version = null;
291                 $module = $plugin;
292                 @include($fullplug.'/version.php');
293                 $this->presentplugins[$type][$plug] = $plugin;
294             }
295         }
297         $cache->set('present', $this->presentplugins);
298     }
300     /**
301      * Get list of present plugins of given type.
302      *
303      * @param string $type
304      * @return array|null  list of presnet plugins $name=>$diskversion, null if unknown
305      */
306     public function get_present_plugins($type) {
307         $this->load_present_plugins();
308         if (isset($this->presentplugins[$type])) {
309             return $this->presentplugins[$type];
310         }
311         return null;
312     }
314     /**
315      * Returns a tree of known plugins and information about them
316      *
317      * @return array 2D array. The first keys are plugin type names (e.g. qtype);
318      *      the second keys are the plugin local name (e.g. multichoice); and
319      *      the values are the corresponding objects extending {@link plugininfo_base}
320      */
321     public function get_plugins() {
322         global $CFG;
324         if (is_array($this->pluginsinfo)) {
325             return $this->pluginsinfo;
326         }
328         $this->pluginsinfo = array();
330         // Hack: include mod and editor subplugin management classes first,
331         //       the adminlib.php is supposed to contain extra admin settings too.
332         require_once($CFG->libdir.'/adminlib.php');
333         foreach (core_component::get_plugin_types_with_subplugins() as $type => $ignored) {
334             foreach (core_component::get_plugin_list($type) as $dir) {
335                 if (file_exists("$dir/adminlib.php")) {
336                     include_once("$dir/adminlib.php");
337                 }
338             }
339         }
340         $plugintypes = $this->get_plugin_types();
341         foreach ($plugintypes as $plugintype => $plugintyperootdir) {
342             if (in_array($plugintype, array('base', 'general'))) {
343                 throw new coding_exception('Illegal usage of reserved word for plugin type');
344             }
345             if (class_exists('plugininfo_' . $plugintype)) {
346                 $plugintypeclass = 'plugininfo_' . $plugintype;
347             } else {
348                 $plugintypeclass = 'plugininfo_general';
349             }
350             if (!in_array('plugininfo_base', class_parents($plugintypeclass))) {
351                 throw new coding_exception('Class ' . $plugintypeclass . ' must extend plugininfo_base');
352             }
353             $plugins = $plugintypeclass::get_plugins($plugintype, $plugintyperootdir, $plugintypeclass);
354             $this->pluginsinfo[$plugintype] = $plugins;
355         }
357         if (empty($CFG->disableupdatenotifications) and !during_initial_install()) {
358             // append the information about available updates provided by {@link available_update_checker()}
359             $provider = available_update_checker::instance();
360             foreach ($this->pluginsinfo as $plugintype => $plugins) {
361                 foreach ($plugins as $plugininfoholder) {
362                     $plugininfoholder->check_available_updates($provider);
363                 }
364             }
365         }
367         return $this->pluginsinfo;
368     }
370     /**
371      * Returns list of known plugins of the given type.
372      *
373      * This method returns the subset of the tree returned by {@link self::get_plugins()}.
374      * If the given type is not known, empty array is returned.
375      *
376      * @param string $type plugin type, e.g. 'mod' or 'workshopallocation'
377      * @return array (string)plugin name (e.g. 'workshop') => corresponding subclass of {@link plugininfo_base}
378      */
379     public function get_plugins_of_type($type) {
381         $plugins = $this->get_plugins();
383         if (!isset($plugins[$type])) {
384             return array();
385         }
387         return $plugins[$type];
388     }
390     /**
391      * Returns list of all known subplugins of the given plugin.
392      *
393      * For plugins that do not provide subplugins (i.e. there is no support for it),
394      * empty array is returned.
395      *
396      * @param string $component full component name, e.g. 'mod_workshop'
397      * @return array (string) component name (e.g. 'workshopallocation_random') => subclass of {@link plugininfo_base}
398      */
399     public function get_subplugins_of_plugin($component) {
401         $pluginfo = $this->get_plugin_info($component);
403         if (is_null($pluginfo)) {
404             return array();
405         }
407         $subplugins = $this->get_subplugins();
409         if (!isset($subplugins[$pluginfo->component])) {
410             return array();
411         }
413         $list = array();
415         foreach ($subplugins[$pluginfo->component] as $subdata) {
416             foreach ($this->get_plugins_of_type($subdata->type) as $subpluginfo) {
417                 $list[$subpluginfo->component] = $subpluginfo;
418             }
419         }
421         return $list;
422     }
424     /**
425      * Returns list of plugins that define their subplugins and the information
426      * about them from the db/subplugins.php file.
427      *
428      * @return array with keys like 'mod_quiz', and values the data from the
429      *      corresponding db/subplugins.php file.
430      */
431     public function get_subplugins() {
433         if (is_array($this->subpluginsinfo)) {
434             return $this->subpluginsinfo;
435         }
437         $this->subpluginsinfo = array();
438         foreach (core_component::get_plugin_types_with_subplugins() as $type => $ignored) {
439             foreach (core_component::get_plugin_list($type) as $component => $ownerdir) {
440                 $componentsubplugins = array();
441                 if (file_exists($ownerdir . '/db/subplugins.php')) {
442                     $subplugins = array();
443                     include($ownerdir . '/db/subplugins.php');
444                     foreach ($subplugins as $subplugintype => $subplugintyperootdir) {
445                         $subplugin = new stdClass();
446                         $subplugin->type = $subplugintype;
447                         $subplugin->typerootdir = $subplugintyperootdir;
448                         $componentsubplugins[$subplugintype] = $subplugin;
449                     }
450                     $this->subpluginsinfo[$type . '_' . $component] = $componentsubplugins;
451                 }
452             }
453         }
455         return $this->subpluginsinfo;
456     }
458     /**
459      * Returns the name of the plugin that defines the given subplugin type
460      *
461      * If the given subplugin type is not actually a subplugin, returns false.
462      *
463      * @param string $subplugintype the name of subplugin type, eg. workshopform or quiz
464      * @return false|string the name of the parent plugin, eg. mod_workshop
465      */
466     public function get_parent_of_subplugin($subplugintype) {
468         $parent = false;
469         foreach ($this->get_subplugins() as $pluginname => $subplugintypes) {
470             if (isset($subplugintypes[$subplugintype])) {
471                 $parent = $pluginname;
472                 break;
473             }
474         }
476         return $parent;
477     }
479     /**
480      * Returns a localized name of a given plugin
481      *
482      * @param string $component name of the plugin, eg mod_workshop or auth_ldap
483      * @return string
484      */
485     public function plugin_name($component) {
487         $pluginfo = $this->get_plugin_info($component);
489         if (is_null($pluginfo)) {
490             throw new moodle_exception('err_unknown_plugin', 'core_plugin', '', array('plugin' => $component));
491         }
493         return $pluginfo->displayname;
494     }
496     /**
497      * Returns a localized name of a plugin typed in singular form
498      *
499      * Most plugin types define their names in core_plugin lang file. In case of subplugins,
500      * we try to ask the parent plugin for the name. In the worst case, we will return
501      * the value of the passed $type parameter.
502      *
503      * @param string $type the type of the plugin, e.g. mod or workshopform
504      * @return string
505      */
506     public function plugintype_name($type) {
508         if (get_string_manager()->string_exists('type_' . $type, 'core_plugin')) {
509             // for most plugin types, their names are defined in core_plugin lang file
510             return get_string('type_' . $type, 'core_plugin');
512         } else if ($parent = $this->get_parent_of_subplugin($type)) {
513             // if this is a subplugin, try to ask the parent plugin for the name
514             if (get_string_manager()->string_exists('subplugintype_' . $type, $parent)) {
515                 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type, $parent);
516             } else {
517                 return $this->plugin_name($parent) . ' / ' . $type;
518             }
520         } else {
521             return $type;
522         }
523     }
525     /**
526      * Returns a localized name of a plugin type in plural form
527      *
528      * Most plugin types define their names in core_plugin lang file. In case of subplugins,
529      * we try to ask the parent plugin for the name. In the worst case, we will return
530      * the value of the passed $type parameter.
531      *
532      * @param string $type the type of the plugin, e.g. mod or workshopform
533      * @return string
534      */
535     public function plugintype_name_plural($type) {
537         if (get_string_manager()->string_exists('type_' . $type . '_plural', 'core_plugin')) {
538             // for most plugin types, their names are defined in core_plugin lang file
539             return get_string('type_' . $type . '_plural', 'core_plugin');
541         } else if ($parent = $this->get_parent_of_subplugin($type)) {
542             // if this is a subplugin, try to ask the parent plugin for the name
543             if (get_string_manager()->string_exists('subplugintype_' . $type . '_plural', $parent)) {
544                 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type . '_plural', $parent);
545             } else {
546                 return $this->plugin_name($parent) . ' / ' . $type;
547             }
549         } else {
550             return $type;
551         }
552     }
554     /**
555      * Returns information about the known plugin, or null
556      *
557      * @param string $component frankenstyle component name.
558      * @return plugininfo_base|null the corresponding plugin information.
559      */
560     public function get_plugin_info($component) {
561         list($type, $name) = core_component::normalize_component($component);
562         $plugins = $this->get_plugins();
563         if (isset($plugins[$type][$name])) {
564             return $plugins[$type][$name];
565         } else {
566             return null;
567         }
568     }
570     /**
571      * Check to see if the current version of the plugin seems to be a checkout of an external repository.
572      *
573      * @see available_update_deployer::plugin_external_source()
574      * @param string $component frankenstyle component name
575      * @return false|string
576      */
577     public function plugin_external_source($component) {
579         $plugininfo = $this->get_plugin_info($component);
581         if (is_null($plugininfo)) {
582             return false;
583         }
585         $pluginroot = $plugininfo->rootdir;
587         if (is_dir($pluginroot.'/.git')) {
588             return 'git';
589         }
591         if (is_dir($pluginroot.'/CVS')) {
592             return 'cvs';
593         }
595         if (is_dir($pluginroot.'/.svn')) {
596             return 'svn';
597         }
599         return false;
600     }
602     /**
603      * Get a list of any other plugins that require this one.
604      * @param string $component frankenstyle component name.
605      * @return array of frankensyle component names that require this one.
606      */
607     public function other_plugins_that_require($component) {
608         $others = array();
609         foreach ($this->get_plugins() as $type => $plugins) {
610             foreach ($plugins as $plugin) {
611                 $required = $plugin->get_other_required_plugins();
612                 if (isset($required[$component])) {
613                     $others[] = $plugin->component;
614                 }
615             }
616         }
617         return $others;
618     }
620     /**
621      * Check a dependencies list against the list of installed plugins.
622      * @param array $dependencies compenent name to required version or ANY_VERSION.
623      * @return bool true if all the dependencies are satisfied.
624      */
625     public function are_dependencies_satisfied($dependencies) {
626         foreach ($dependencies as $component => $requiredversion) {
627             $otherplugin = $this->get_plugin_info($component);
628             if (is_null($otherplugin)) {
629                 return false;
630             }
632             if ($requiredversion != ANY_VERSION and $otherplugin->versiondisk < $requiredversion) {
633                 return false;
634             }
635         }
637         return true;
638     }
640     /**
641      * Checks all dependencies for all installed plugins
642      *
643      * This is used by install and upgrade. The array passed by reference as the second
644      * argument is populated with the list of plugins that have failed dependencies (note that
645      * a single plugin can appear multiple times in the $failedplugins).
646      *
647      * @param int $moodleversion the version from version.php.
648      * @param array $failedplugins to return the list of plugins with non-satisfied dependencies
649      * @return bool true if all the dependencies are satisfied for all plugins.
650      */
651     public function all_plugins_ok($moodleversion, &$failedplugins = array()) {
653         $return = true;
654         foreach ($this->get_plugins() as $type => $plugins) {
655             foreach ($plugins as $plugin) {
657                 if (!$plugin->is_core_dependency_satisfied($moodleversion)) {
658                     $return = false;
659                     $failedplugins[] = $plugin->component;
660                 }
662                 if (!$this->are_dependencies_satisfied($plugin->get_other_required_plugins())) {
663                     $return = false;
664                     $failedplugins[] = $plugin->component;
665                 }
666             }
667         }
669         return $return;
670     }
672     /**
673      * Is it possible to uninstall the given plugin?
674      *
675      * False is returned if the plugininfo subclass declares the uninstall should
676      * not be allowed via {@link plugininfo_base::is_uninstall_allowed()} or if the
677      * core vetoes it (e.g. becase the plugin or some of its subplugins is required
678      * by some other installed plugin).
679      *
680      * @param string $component full frankenstyle name, e.g. mod_foobar
681      * @return bool
682      */
683     public function can_uninstall_plugin($component) {
685         $pluginfo = $this->get_plugin_info($component);
687         if (is_null($pluginfo)) {
688             return false;
689         }
691         if (!$this->common_uninstall_check($pluginfo)) {
692             return false;
693         }
695         // If it has subplugins, check they can be uninstalled too.
696         $subplugins = $this->get_subplugins_of_plugin($pluginfo->component);
697         foreach ($subplugins as $subpluginfo) {
698             if (!$this->common_uninstall_check($subpluginfo)) {
699                 return false;
700             }
701             // Check if there are some other plugins requiring this subplugin
702             // (but the parent and siblings).
703             foreach ($this->other_plugins_that_require($subpluginfo->component) as $requiresme) {
704                 $ismyparent = ($pluginfo->component === $requiresme);
705                 $ismysibling = in_array($requiresme, array_keys($subplugins));
706                 if (!$ismyparent and !$ismysibling) {
707                     return false;
708                 }
709             }
710         }
712         // Check if there are some other plugins requiring this plugin
713         // (but its subplugins).
714         foreach ($this->other_plugins_that_require($pluginfo->component) as $requiresme) {
715             $ismysubplugin = in_array($requiresme, array_keys($subplugins));
716             if (!$ismysubplugin) {
717                 return false;
718             }
719         }
721         return true;
722     }
724     /**
725      * Returns uninstall URL if exists.
726      *
727      * @param string $component
728      * @return moodle_url uninstall URL, null if uninstall not supported
729      */
730     public function get_uninstall_url($component) {
731         if (!$this->can_uninstall_plugin($component)) {
732             return null;
733         }
735         $pluginfo = $this->get_plugin_info($component);
737         if (is_null($pluginfo)) {
738             return null;
739         }
741         return $pluginfo->get_uninstall_url();
742     }
744     /**
745      * Uninstall the given plugin.
746      *
747      * Automatically cleans-up all remaining configuration data, log records, events,
748      * files from the file pool etc.
749      *
750      * In the future, the functionality of {@link uninstall_plugin()} function may be moved
751      * into this method and all the code should be refactored to use it. At the moment, we
752      * mimic this future behaviour by wrapping that function call.
753      *
754      * @param string $component
755      * @param progress_trace $progress traces the process
756      * @return bool true on success, false on errors/problems
757      */
758     public function uninstall_plugin($component, progress_trace $progress) {
760         $pluginfo = $this->get_plugin_info($component);
762         if (is_null($pluginfo)) {
763             return false;
764         }
766         // Give the pluginfo class a chance to execute some steps.
767         $result = $pluginfo->uninstall($progress);
768         if (!$result) {
769             return false;
770         }
772         // Call the legacy core function to uninstall the plugin.
773         ob_start();
774         uninstall_plugin($pluginfo->type, $pluginfo->name);
775         $progress->output(ob_get_clean());
777         return true;
778     }
780     /**
781      * Checks if there are some plugins with a known available update
782      *
783      * @return bool true if there is at least one available update
784      */
785     public function some_plugins_updatable() {
786         foreach ($this->get_plugins() as $type => $plugins) {
787             foreach ($plugins as $plugin) {
788                 if ($plugin->available_updates()) {
789                     return true;
790                 }
791             }
792         }
794         return false;
795     }
797     /**
798      * Check to see if the given plugin folder can be removed by the web server process.
799      *
800      * @param string $component full frankenstyle component
801      * @return bool
802      */
803     public function is_plugin_folder_removable($component) {
805         $pluginfo = $this->get_plugin_info($component);
807         if (is_null($pluginfo)) {
808             return false;
809         }
811         // To be able to remove the plugin folder, its parent must be writable, too.
812         if (!is_writable(dirname($pluginfo->rootdir))) {
813             return false;
814         }
816         // Check that the folder and all its content is writable (thence removable).
817         return $this->is_directory_removable($pluginfo->rootdir);
818     }
820     /**
821      * Defines a list of all plugins that were originally shipped in the standard Moodle distribution,
822      * but are not anymore and are deleted during upgrades.
823      *
824      * The main purpose of this list is to hide missing plugins during upgrade.
825      *
826      * @param string $type plugin type
827      * @param string $name plugin name
828      * @return bool
829      */
830     public static function is_deleted_standard_plugin($type, $name) {
832         // Example of the array structure:
833         // $plugins = array(
834         //     'block' => array('admin', 'admin_tree'),
835         //     'mod' => array('assignment'),
836         // );
837         // Do not include plugins that were removed during upgrades to versions that are
838         // not supported as source versions for upgrade any more. For example, at MOODLE_23_STABLE
839         // branch, listed should be no plugins that were removed at 1.9.x - 2.1.x versions as
840         // Moodle 2.3 supports upgrades from 2.2.x only.
841         $plugins = array(
842             'qformat' => array('blackboard'),
843             'enrol' => array('authorize'),
844             'tool' => array('bloglevelupgrade'),
845         );
847         if (!isset($plugins[$type])) {
848             return false;
849         }
850         return in_array($name, $plugins[$type]);
851     }
853     /**
854      * Defines a white list of all plugins shipped in the standard Moodle distribution
855      *
856      * @param string $type
857      * @return false|array array of standard plugins or false if the type is unknown
858      */
859     public static function standard_plugins_list($type) {
861         $standard_plugins = array(
863             'assignment' => array(
864                 'offline', 'online', 'upload', 'uploadsingle'
865             ),
867             'assignsubmission' => array(
868                 'comments', 'file', 'onlinetext'
869             ),
871             'assignfeedback' => array(
872                 'comments', 'file', 'offline'
873             ),
875             'atto' => array(
876                 'bold', 'clear', 'html', 'image', 'indent', 'italic', 'link',
877                 'media', 'orderedlist', 'outdent', 'strike', 'title',
878                 'underline', 'unlink', 'unorderedlist'
879             ),
881             'auth' => array(
882                 'cas', 'db', 'email', 'fc', 'imap', 'ldap', 'manual', 'mnet',
883                 'nntp', 'nologin', 'none', 'pam', 'pop3', 'radius',
884                 'shibboleth', 'webservice'
885             ),
887             'block' => array(
888                 'activity_modules', 'admin_bookmarks', 'badges', 'blog_menu',
889                 'blog_recent', 'blog_tags', 'calendar_month',
890                 'calendar_upcoming', 'comments', 'community',
891                 'completionstatus', 'course_list', 'course_overview',
892                 'course_summary', 'feedback', 'glossary_random', 'html',
893                 'login', 'mentees', 'messages', 'mnet_hosts', 'myprofile',
894                 'navigation', 'news_items', 'online_users', 'participants',
895                 'private_files', 'quiz_results', 'recent_activity',
896                 'rss_client', 'search_forums', 'section_links',
897                 'selfcompletion', 'settings', 'site_main_menu',
898                 'social_activities', 'tag_flickr', 'tag_youtube', 'tags'
899             ),
901             'booktool' => array(
902                 'exportimscp', 'importhtml', 'print'
903             ),
905             'cachelock' => array(
906                 'file'
907             ),
909             'cachestore' => array(
910                 'file', 'memcache', 'memcached', 'mongodb', 'session', 'static'
911             ),
913             'calendartype' => array(
914                 'gregorian'
915             ),
917             'coursereport' => array(
918                 //deprecated!
919             ),
921             'datafield' => array(
922                 'checkbox', 'date', 'file', 'latlong', 'menu', 'multimenu',
923                 'number', 'picture', 'radiobutton', 'text', 'textarea', 'url'
924             ),
926             'datapreset' => array(
927                 'imagegallery'
928             ),
930             'editor' => array(
931                 'textarea', 'tinymce', 'atto'
932             ),
934             'enrol' => array(
935                 'category', 'cohort', 'database', 'flatfile',
936                 'guest', 'imsenterprise', 'ldap', 'manual', 'meta', 'mnet',
937                 'paypal', 'self'
938             ),
940             'filter' => array(
941                 'activitynames', 'algebra', 'censor', 'emailprotect',
942                 'emoticon', 'mediaplugin', 'multilang', 'tex', 'tidy',
943                 'urltolink', 'data', 'glossary'
944             ),
946             'format' => array(
947                 'singleactivity', 'social', 'topics', 'weeks'
948             ),
950             'gradeexport' => array(
951                 'ods', 'txt', 'xls', 'xml'
952             ),
954             'gradeimport' => array(
955                 'csv', 'xml'
956             ),
958             'gradereport' => array(
959                 'grader', 'outcomes', 'overview', 'user'
960             ),
962             'gradingform' => array(
963                 'rubric', 'guide'
964             ),
966             'local' => array(
967             ),
969             'message' => array(
970                 'email', 'jabber', 'popup'
971             ),
973             'mnetservice' => array(
974                 'enrol'
975             ),
977             'mod' => array(
978                 'assign', 'assignment', 'book', 'chat', 'choice', 'data', 'feedback', 'folder',
979                 'forum', 'glossary', 'imscp', 'label', 'lesson', 'lti', 'page',
980                 'quiz', 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop'
981             ),
983             'plagiarism' => array(
984             ),
986             'portfolio' => array(
987                 'boxnet', 'download', 'flickr', 'googledocs', 'mahara', 'picasa'
988             ),
990             'profilefield' => array(
991                 'checkbox', 'datetime', 'menu', 'text', 'textarea'
992             ),
994             'qbehaviour' => array(
995                 'adaptive', 'adaptivenopenalty', 'deferredcbm',
996                 'deferredfeedback', 'immediatecbm', 'immediatefeedback',
997                 'informationitem', 'interactive', 'interactivecountback',
998                 'manualgraded', 'missing'
999             ),
1001             'qformat' => array(
1002                 'aiken', 'blackboard_six', 'examview', 'gift',
1003                 'learnwise', 'missingword', 'multianswer', 'webct',
1004                 'xhtml', 'xml'
1005             ),
1007             'qtype' => array(
1008                 'calculated', 'calculatedmulti', 'calculatedsimple',
1009                 'description', 'essay', 'match', 'missingtype', 'multianswer',
1010                 'multichoice', 'numerical', 'random', 'randomsamatch',
1011                 'shortanswer', 'truefalse'
1012             ),
1014             'quiz' => array(
1015                 'grading', 'overview', 'responses', 'statistics'
1016             ),
1018             'quizaccess' => array(
1019                 'delaybetweenattempts', 'ipaddress', 'numattempts', 'openclosedate',
1020                 'password', 'safebrowser', 'securewindow', 'timelimit'
1021             ),
1023             'report' => array(
1024                 'backups', 'completion', 'configlog', 'courseoverview',
1025                 'log', 'loglive', 'outline', 'participation', 'progress', 'questioninstances', 'security', 'stats', 'performance'
1026             ),
1028             'repository' => array(
1029                 'alfresco', 'areafiles', 'boxnet', 'coursefiles', 'dropbox', 'equella', 'filesystem',
1030                 'flickr', 'flickr_public', 'googledocs', 'local', 'merlot',
1031                 'picasa', 'recent', 'skydrive', 's3', 'upload', 'url', 'user', 'webdav',
1032                 'wikimedia', 'youtube'
1033             ),
1035             'scormreport' => array(
1036                 'basic',
1037                 'interactions',
1038                 'graphs',
1039                 'objectives'
1040             ),
1042             'tinymce' => array(
1043                 'ctrlhelp', 'dragmath', 'managefiles', 'moodleemoticon', 'moodleimage',
1044                 'moodlemedia', 'moodlenolink', 'pdw', 'spellchecker', 'wrap'
1045             ),
1047             'theme' => array(
1048                 'afterburner', 'anomaly', 'arialist', 'base', 'binarius', 'bootstrapbase',
1049                 'boxxie', 'brick', 'canvas', 'clean', 'formal_white', 'formfactor',
1050                 'fusion', 'leatherbound', 'magazine', 'mymobile', 'nimble',
1051                 'nonzero', 'overlay', 'serenity', 'sky_high', 'splash',
1052                 'standard', 'standardold'
1053             ),
1055             'tool' => array(
1056                 'assignmentupgrade', 'behat', 'capability', 'customlang',
1057                 'dbtransfer', 'generator', 'health', 'innodb', 'installaddon',
1058                 'langimport', 'multilangupgrade', 'phpunit', 'profiling',
1059                 'qeupgradehelper', 'replace', 'spamcleaner', 'timezoneimport',
1060                 'unittest', 'uploadcourse', 'uploaduser', 'unsuproles', 'xmldb'
1061             ),
1063             'webservice' => array(
1064                 'amf', 'rest', 'soap', 'xmlrpc'
1065             ),
1067             'workshopallocation' => array(
1068                 'manual', 'random', 'scheduled'
1069             ),
1071             'workshopeval' => array(
1072                 'best'
1073             ),
1075             'workshopform' => array(
1076                 'accumulative', 'comments', 'numerrors', 'rubric'
1077             )
1078         );
1080         if (isset($standard_plugins[$type])) {
1081             return $standard_plugins[$type];
1082         } else {
1083             return false;
1084         }
1085     }
1087     /**
1088      * Reorders plugin types into a sequence to be displayed
1089      *
1090      * For technical reasons, plugin types returned by {@link core_component::get_plugin_types()} are
1091      * in a certain order that does not need to fit the expected order for the display.
1092      * Particularly, activity modules should be displayed first as they represent the
1093      * real heart of Moodle. They should be followed by other plugin types that are
1094      * used to build the courses (as that is what one expects from LMS). After that,
1095      * other supportive plugin types follow.
1096      *
1097      * @param array $types associative array
1098      * @return array same array with altered order of items
1099      */
1100     protected function reorder_plugin_types(array $types) {
1101         $fix = array(
1102             'mod'        => $types['mod'],
1103             'block'      => $types['block'],
1104             'qtype'      => $types['qtype'],
1105             'qbehaviour' => $types['qbehaviour'],
1106             'qformat'    => $types['qformat'],
1107             'filter'     => $types['filter'],
1108             'enrol'      => $types['enrol'],
1109         );
1110         foreach ($types as $type => $path) {
1111             if (!isset($fix[$type])) {
1112                 $fix[$type] = $path;
1113             }
1114         }
1115         return $fix;
1116     }
1118     /**
1119      * Check if the given directory can be removed by the web server process.
1120      *
1121      * This recursively checks that the given directory and all its contents
1122      * it writable.
1123      *
1124      * @param string $fullpath
1125      * @return boolean
1126      */
1127     protected function is_directory_removable($fullpath) {
1129         if (!is_writable($fullpath)) {
1130             return false;
1131         }
1133         if (is_dir($fullpath)) {
1134             $handle = opendir($fullpath);
1135         } else {
1136             return false;
1137         }
1139         $result = true;
1141         while ($filename = readdir($handle)) {
1143             if ($filename === '.' or $filename === '..') {
1144                 continue;
1145             }
1147             $subfilepath = $fullpath.'/'.$filename;
1149             if (is_dir($subfilepath)) {
1150                 $result = $result && $this->is_directory_removable($subfilepath);
1152             } else {
1153                 $result = $result && is_writable($subfilepath);
1154             }
1155         }
1157         closedir($handle);
1159         return $result;
1160     }
1162     /**
1163      * Helper method that implements common uninstall prerequisities
1164      *
1165      * @param plugininfo_base $pluginfo
1166      * @return bool
1167      */
1168     protected function common_uninstall_check(plugininfo_base $pluginfo) {
1170         if (!$pluginfo->is_uninstall_allowed()) {
1171             // The plugin's plugininfo class declares it should not be uninstalled.
1172             return false;
1173         }
1175         if ($pluginfo->get_status() === plugin_manager::PLUGIN_STATUS_NEW) {
1176             // The plugin is not installed. It should be either installed or removed from the disk.
1177             // Relying on this temporary state may be tricky.
1178             return false;
1179         }
1181         if (is_null($pluginfo->get_uninstall_url())) {
1182             // Backwards compatibility.
1183             debugging('plugininfo_base subclasses should use is_uninstall_allowed() instead of returning null in get_uninstall_url()',
1184                 DEBUG_DEVELOPER);
1185             return false;
1186         }
1188         return true;
1189     }
1193 /**
1194  * General exception thrown by the {@link available_update_checker} class
1195  */
1196 class available_update_checker_exception extends moodle_exception {
1198     /**
1199      * @param string $errorcode exception description identifier
1200      * @param mixed $debuginfo debugging data to display
1201      */
1202     public function __construct($errorcode, $debuginfo=null) {
1203         parent::__construct($errorcode, 'core_plugin', '', null, print_r($debuginfo, true));
1204     }
1208 /**
1209  * Singleton class that handles checking for available updates
1210  */
1211 class available_update_checker {
1213     /** @var available_update_checker holds the singleton instance */
1214     protected static $singletoninstance;
1215     /** @var null|int the timestamp of when the most recent response was fetched */
1216     protected $recentfetch = null;
1217     /** @var null|array the recent response from the update notification provider */
1218     protected $recentresponse = null;
1219     /** @var null|string the numerical version of the local Moodle code */
1220     protected $currentversion = null;
1221     /** @var null|string the release info of the local Moodle code */
1222     protected $currentrelease = null;
1223     /** @var null|string branch of the local Moodle code */
1224     protected $currentbranch = null;
1225     /** @var array of (string)frankestyle => (string)version list of additional plugins deployed at this site */
1226     protected $currentplugins = array();
1228     /**
1229      * Direct initiation not allowed, use the factory method {@link self::instance()}
1230      */
1231     protected function __construct() {
1232     }
1234     /**
1235      * Sorry, this is singleton
1236      */
1237     protected function __clone() {
1238     }
1240     /**
1241      * Factory method for this class
1242      *
1243      * @return available_update_checker the singleton instance
1244      */
1245     public static function instance() {
1246         if (is_null(self::$singletoninstance)) {
1247             self::$singletoninstance = new self();
1248         }
1249         return self::$singletoninstance;
1250     }
1252     /**
1253      * Reset any caches
1254      * @param bool $phpunitreset
1255      */
1256     public static function reset_caches($phpunitreset = false) {
1257         if ($phpunitreset) {
1258             self::$singletoninstance = null;
1259         }
1260     }
1262     /**
1263      * Returns the timestamp of the last execution of {@link fetch()}
1264      *
1265      * @return int|null null if it has never been executed or we don't known
1266      */
1267     public function get_last_timefetched() {
1269         $this->restore_response();
1271         if (!empty($this->recentfetch)) {
1272             return $this->recentfetch;
1274         } else {
1275             return null;
1276         }
1277     }
1279     /**
1280      * Fetches the available update status from the remote site
1281      *
1282      * @throws available_update_checker_exception
1283      */
1284     public function fetch() {
1285         $response = $this->get_response();
1286         $this->validate_response($response);
1287         $this->store_response($response);
1288     }
1290     /**
1291      * Returns the available update information for the given component
1292      *
1293      * This method returns null if the most recent response does not contain any information
1294      * about it. The returned structure is an array of available updates for the given
1295      * component. Each update info is an object with at least one property called
1296      * 'version'. Other possible properties are 'release', 'maturity', 'url' and 'downloadurl'.
1297      *
1298      * For the 'core' component, the method returns real updates only (those with higher version).
1299      * For all other components, the list of all known remote updates is returned and the caller
1300      * (usually the {@link plugin_manager}) is supposed to make the actual comparison of versions.
1301      *
1302      * @param string $component frankenstyle
1303      * @param array $options with supported keys 'minmaturity' and/or 'notifybuilds'
1304      * @return null|array null or array of available_update_info objects
1305      */
1306     public function get_update_info($component, array $options = array()) {
1308         if (!isset($options['minmaturity'])) {
1309             $options['minmaturity'] = 0;
1310         }
1312         if (!isset($options['notifybuilds'])) {
1313             $options['notifybuilds'] = false;
1314         }
1316         if ($component == 'core') {
1317             $this->load_current_environment();
1318         }
1320         $this->restore_response();
1322         if (empty($this->recentresponse['updates'][$component])) {
1323             return null;
1324         }
1326         $updates = array();
1327         foreach ($this->recentresponse['updates'][$component] as $info) {
1328             $update = new available_update_info($component, $info);
1329             if (isset($update->maturity) and ($update->maturity < $options['minmaturity'])) {
1330                 continue;
1331             }
1332             if ($component == 'core') {
1333                 if ($update->version <= $this->currentversion) {
1334                     continue;
1335                 }
1336                 if (empty($options['notifybuilds']) and $this->is_same_release($update->release)) {
1337                     continue;
1338                 }
1339             }
1340             $updates[] = $update;
1341         }
1343         if (empty($updates)) {
1344             return null;
1345         }
1347         return $updates;
1348     }
1350     /**
1351      * The method being run via cron.php
1352      */
1353     public function cron() {
1354         global $CFG;
1356         if (!$this->cron_autocheck_enabled()) {
1357             $this->cron_mtrace('Automatic check for available updates not enabled, skipping.');
1358             return;
1359         }
1361         $now = $this->cron_current_timestamp();
1363         if ($this->cron_has_fresh_fetch($now)) {
1364             $this->cron_mtrace('Recently fetched info about available updates is still fresh enough, skipping.');
1365             return;
1366         }
1368         if ($this->cron_has_outdated_fetch($now)) {
1369             $this->cron_mtrace('Outdated or missing info about available updates, forced fetching ... ', '');
1370             $this->cron_execute();
1371             return;
1372         }
1374         $offset = $this->cron_execution_offset();
1375         $start = mktime(1, 0, 0, date('n', $now), date('j', $now), date('Y', $now)); // 01:00 AM today local time
1376         if ($now > $start + $offset) {
1377             $this->cron_mtrace('Regular daily check for available updates ... ', '');
1378             $this->cron_execute();
1379             return;
1380         }
1381     }
1383     /// end of public API //////////////////////////////////////////////////////
1385     /**
1386      * Makes cURL request to get data from the remote site
1387      *
1388      * @return string raw request result
1389      * @throws available_update_checker_exception
1390      */
1391     protected function get_response() {
1392         global $CFG;
1393         require_once($CFG->libdir.'/filelib.php');
1395         $curl = new curl(array('proxy' => true));
1396         $response = $curl->post($this->prepare_request_url(), $this->prepare_request_params(), $this->prepare_request_options());
1397         $curlerrno = $curl->get_errno();
1398         if (!empty($curlerrno)) {
1399             throw new available_update_checker_exception('err_response_curl', 'cURL error '.$curlerrno.': '.$curl->error);
1400         }
1401         $curlinfo = $curl->get_info();
1402         if ($curlinfo['http_code'] != 200) {
1403             throw new available_update_checker_exception('err_response_http_code', $curlinfo['http_code']);
1404         }
1405         return $response;
1406     }
1408     /**
1409      * Makes sure the response is valid, has correct API format etc.
1410      *
1411      * @param string $response raw response as returned by the {@link self::get_response()}
1412      * @throws available_update_checker_exception
1413      */
1414     protected function validate_response($response) {
1416         $response = $this->decode_response($response);
1418         if (empty($response)) {
1419             throw new available_update_checker_exception('err_response_empty');
1420         }
1422         if (empty($response['status']) or $response['status'] !== 'OK') {
1423             throw new available_update_checker_exception('err_response_status', $response['status']);
1424         }
1426         if (empty($response['apiver']) or $response['apiver'] !== '1.2') {
1427             throw new available_update_checker_exception('err_response_format_version', $response['apiver']);
1428         }
1430         if (empty($response['forbranch']) or $response['forbranch'] !== moodle_major_version(true)) {
1431             throw new available_update_checker_exception('err_response_target_version', $response['forbranch']);
1432         }
1433     }
1435     /**
1436      * Decodes the raw string response from the update notifications provider
1437      *
1438      * @param string $response as returned by {@link self::get_response()}
1439      * @return array decoded response structure
1440      */
1441     protected function decode_response($response) {
1442         return json_decode($response, true);
1443     }
1445     /**
1446      * Stores the valid fetched response for later usage
1447      *
1448      * This implementation uses the config_plugins table as the permanent storage.
1449      *
1450      * @param string $response raw valid data returned by {@link self::get_response()}
1451      */
1452     protected function store_response($response) {
1454         set_config('recentfetch', time(), 'core_plugin');
1455         set_config('recentresponse', $response, 'core_plugin');
1457         $this->restore_response(true);
1458     }
1460     /**
1461      * Loads the most recent raw response record we have fetched
1462      *
1463      * After this method is called, $this->recentresponse is set to an array. If the
1464      * array is empty, then either no data have been fetched yet or the fetched data
1465      * do not have expected format (and thence they are ignored and a debugging
1466      * message is displayed).
1467      *
1468      * This implementation uses the config_plugins table as the permanent storage.
1469      *
1470      * @param bool $forcereload reload even if it was already loaded
1471      */
1472     protected function restore_response($forcereload = false) {
1474         if (!$forcereload and !is_null($this->recentresponse)) {
1475             // we already have it, nothing to do
1476             return;
1477         }
1479         $config = get_config('core_plugin');
1481         if (!empty($config->recentresponse) and !empty($config->recentfetch)) {
1482             try {
1483                 $this->validate_response($config->recentresponse);
1484                 $this->recentfetch = $config->recentfetch;
1485                 $this->recentresponse = $this->decode_response($config->recentresponse);
1486             } catch (available_update_checker_exception $e) {
1487                 // The server response is not valid. Behave as if no data were fetched yet.
1488                 // This may happen when the most recent update info (cached locally) has been
1489                 // fetched with the previous branch of Moodle (like during an upgrade from 2.x
1490                 // to 2.y) or when the API of the response has changed.
1491                 $this->recentresponse = array();
1492             }
1494         } else {
1495             $this->recentresponse = array();
1496         }
1497     }
1499     /**
1500      * Compares two raw {@link $recentresponse} records and returns the list of changed updates
1501      *
1502      * This method is used to populate potential update info to be sent to site admins.
1503      *
1504      * @param array $old
1505      * @param array $new
1506      * @throws available_update_checker_exception
1507      * @return array parts of $new['updates'] that have changed
1508      */
1509     protected function compare_responses(array $old, array $new) {
1511         if (empty($new)) {
1512             return array();
1513         }
1515         if (!array_key_exists('updates', $new)) {
1516             throw new available_update_checker_exception('err_response_format');
1517         }
1519         if (empty($old)) {
1520             return $new['updates'];
1521         }
1523         if (!array_key_exists('updates', $old)) {
1524             throw new available_update_checker_exception('err_response_format');
1525         }
1527         $changes = array();
1529         foreach ($new['updates'] as $newcomponent => $newcomponentupdates) {
1530             if (empty($old['updates'][$newcomponent])) {
1531                 $changes[$newcomponent] = $newcomponentupdates;
1532                 continue;
1533             }
1534             foreach ($newcomponentupdates as $newcomponentupdate) {
1535                 $inold = false;
1536                 foreach ($old['updates'][$newcomponent] as $oldcomponentupdate) {
1537                     if ($newcomponentupdate['version'] == $oldcomponentupdate['version']) {
1538                         $inold = true;
1539                     }
1540                 }
1541                 if (!$inold) {
1542                     if (!isset($changes[$newcomponent])) {
1543                         $changes[$newcomponent] = array();
1544                     }
1545                     $changes[$newcomponent][] = $newcomponentupdate;
1546                 }
1547             }
1548         }
1550         return $changes;
1551     }
1553     /**
1554      * Returns the URL to send update requests to
1555      *
1556      * During the development or testing, you can set $CFG->alternativeupdateproviderurl
1557      * to a custom URL that will be used. Otherwise the standard URL will be returned.
1558      *
1559      * @return string URL
1560      */
1561     protected function prepare_request_url() {
1562         global $CFG;
1564         if (!empty($CFG->config_php_settings['alternativeupdateproviderurl'])) {
1565             return $CFG->config_php_settings['alternativeupdateproviderurl'];
1566         } else {
1567             return 'https://download.moodle.org/api/1.2/updates.php';
1568         }
1569     }
1571     /**
1572      * Sets the properties currentversion, currentrelease, currentbranch and currentplugins
1573      *
1574      * @param bool $forcereload
1575      */
1576     protected function load_current_environment($forcereload=false) {
1577         global $CFG;
1579         if (!is_null($this->currentversion) and !$forcereload) {
1580             // nothing to do
1581             return;
1582         }
1584         $version = null;
1585         $release = null;
1587         require($CFG->dirroot.'/version.php');
1588         $this->currentversion = $version;
1589         $this->currentrelease = $release;
1590         $this->currentbranch = moodle_major_version(true);
1592         $pluginman = plugin_manager::instance();
1593         foreach ($pluginman->get_plugins() as $type => $plugins) {
1594             foreach ($plugins as $plugin) {
1595                 if (!$plugin->is_standard()) {
1596                     $this->currentplugins[$plugin->component] = $plugin->versiondisk;
1597                 }
1598             }
1599         }
1600     }
1602     /**
1603      * Returns the list of HTTP params to be sent to the updates provider URL
1604      *
1605      * @return array of (string)param => (string)value
1606      */
1607     protected function prepare_request_params() {
1608         global $CFG;
1610         $this->load_current_environment();
1611         $this->restore_response();
1613         $params = array();
1614         $params['format'] = 'json';
1616         if (isset($this->recentresponse['ticket'])) {
1617             $params['ticket'] = $this->recentresponse['ticket'];
1618         }
1620         if (isset($this->currentversion)) {
1621             $params['version'] = $this->currentversion;
1622         } else {
1623             throw new coding_exception('Main Moodle version must be already known here');
1624         }
1626         if (isset($this->currentbranch)) {
1627             $params['branch'] = $this->currentbranch;
1628         } else {
1629             throw new coding_exception('Moodle release must be already known here');
1630         }
1632         $plugins = array();
1633         foreach ($this->currentplugins as $plugin => $version) {
1634             $plugins[] = $plugin.'@'.$version;
1635         }
1636         if (!empty($plugins)) {
1637             $params['plugins'] = implode(',', $plugins);
1638         }
1640         return $params;
1641     }
1643     /**
1644      * Returns the list of cURL options to use when fetching available updates data
1645      *
1646      * @return array of (string)param => (string)value
1647      */
1648     protected function prepare_request_options() {
1649         global $CFG;
1651         $options = array(
1652             'CURLOPT_SSL_VERIFYHOST' => 2,      // this is the default in {@link curl} class but just in case
1653             'CURLOPT_SSL_VERIFYPEER' => true,
1654         );
1656         return $options;
1657     }
1659     /**
1660      * Returns the current timestamp
1661      *
1662      * @return int the timestamp
1663      */
1664     protected function cron_current_timestamp() {
1665         return time();
1666     }
1668     /**
1669      * Output cron debugging info
1670      *
1671      * @see mtrace()
1672      * @param string $msg output message
1673      * @param string $eol end of line
1674      */
1675     protected function cron_mtrace($msg, $eol = PHP_EOL) {
1676         mtrace($msg, $eol);
1677     }
1679     /**
1680      * Decide if the autocheck feature is disabled in the server setting
1681      *
1682      * @return bool true if autocheck enabled, false if disabled
1683      */
1684     protected function cron_autocheck_enabled() {
1685         global $CFG;
1687         if (empty($CFG->updateautocheck)) {
1688             return false;
1689         } else {
1690             return true;
1691         }
1692     }
1694     /**
1695      * Decide if the recently fetched data are still fresh enough
1696      *
1697      * @param int $now current timestamp
1698      * @return bool true if no need to re-fetch, false otherwise
1699      */
1700     protected function cron_has_fresh_fetch($now) {
1701         $recent = $this->get_last_timefetched();
1703         if (empty($recent)) {
1704             return false;
1705         }
1707         if ($now < $recent) {
1708             $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1709             return true;
1710         }
1712         if ($now - $recent > 24 * HOURSECS) {
1713             return false;
1714         }
1716         return true;
1717     }
1719     /**
1720      * Decide if the fetch is outadated or even missing
1721      *
1722      * @param int $now current timestamp
1723      * @return bool false if no need to re-fetch, true otherwise
1724      */
1725     protected function cron_has_outdated_fetch($now) {
1726         $recent = $this->get_last_timefetched();
1728         if (empty($recent)) {
1729             return true;
1730         }
1732         if ($now < $recent) {
1733             $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1734             return false;
1735         }
1737         if ($now - $recent > 48 * HOURSECS) {
1738             return true;
1739         }
1741         return false;
1742     }
1744     /**
1745      * Returns the cron execution offset for this site
1746      *
1747      * The main {@link self::cron()} is supposed to run every night in some random time
1748      * between 01:00 and 06:00 AM (local time). The exact moment is defined by so called
1749      * execution offset, that is the amount of time after 01:00 AM. The offset value is
1750      * initially generated randomly and then used consistently at the site. This way, the
1751      * regular checks against the download.moodle.org server are spread in time.
1752      *
1753      * @return int the offset number of seconds from range 1 sec to 5 hours
1754      */
1755     protected function cron_execution_offset() {
1756         global $CFG;
1758         if (empty($CFG->updatecronoffset)) {
1759             set_config('updatecronoffset', rand(1, 5 * HOURSECS));
1760         }
1762         return $CFG->updatecronoffset;
1763     }
1765     /**
1766      * Fetch available updates info and eventually send notification to site admins
1767      */
1768     protected function cron_execute() {
1770         try {
1771             $this->restore_response();
1772             $previous = $this->recentresponse;
1773             $this->fetch();
1774             $this->restore_response(true);
1775             $current = $this->recentresponse;
1776             $changes = $this->compare_responses($previous, $current);
1777             $notifications = $this->cron_notifications($changes);
1778             $this->cron_notify($notifications);
1779             $this->cron_mtrace('done');
1780         } catch (available_update_checker_exception $e) {
1781             $this->cron_mtrace('FAILED!');
1782         }
1783     }
1785     /**
1786      * Given the list of changes in available updates, pick those to send to site admins
1787      *
1788      * @param array $changes as returned by {@link self::compare_responses()}
1789      * @return array of available_update_info objects to send to site admins
1790      */
1791     protected function cron_notifications(array $changes) {
1792         global $CFG;
1794         $notifications = array();
1795         $pluginman = plugin_manager::instance();
1796         $plugins = $pluginman->get_plugins(true);
1798         foreach ($changes as $component => $componentchanges) {
1799             if (empty($componentchanges)) {
1800                 continue;
1801             }
1802             $componentupdates = $this->get_update_info($component,
1803                 array('minmaturity' => $CFG->updateminmaturity, 'notifybuilds' => $CFG->updatenotifybuilds));
1804             if (empty($componentupdates)) {
1805                 continue;
1806             }
1807             // notify only about those $componentchanges that are present in $componentupdates
1808             // to respect the preferences
1809             foreach ($componentchanges as $componentchange) {
1810                 foreach ($componentupdates as $componentupdate) {
1811                     if ($componentupdate->version == $componentchange['version']) {
1812                         if ($component == 'core') {
1813                             // In case of 'core', we already know that the $componentupdate
1814                             // is a real update with higher version ({@see self::get_update_info()}).
1815                             // We just perform additional check for the release property as there
1816                             // can be two Moodle releases having the same version (e.g. 2.4.0 and 2.5dev shortly
1817                             // after the release). We can do that because we have the release info
1818                             // always available for the core.
1819                             if ((string)$componentupdate->release === (string)$componentchange['release']) {
1820                                 $notifications[] = $componentupdate;
1821                             }
1822                         } else {
1823                             // Use the plugin_manager to check if the detected $componentchange
1824                             // is a real update with higher version. That is, the $componentchange
1825                             // is present in the array of {@link available_update_info} objects
1826                             // returned by the plugin's available_updates() method.
1827                             list($plugintype, $pluginname) = core_component::normalize_component($component);
1828                             if (!empty($plugins[$plugintype][$pluginname])) {
1829                                 $availableupdates = $plugins[$plugintype][$pluginname]->available_updates();
1830                                 if (!empty($availableupdates)) {
1831                                     foreach ($availableupdates as $availableupdate) {
1832                                         if ($availableupdate->version == $componentchange['version']) {
1833                                             $notifications[] = $componentupdate;
1834                                         }
1835                                     }
1836                                 }
1837                             }
1838                         }
1839                     }
1840                 }
1841             }
1842         }
1844         return $notifications;
1845     }
1847     /**
1848      * Sends the given notifications to site admins via messaging API
1849      *
1850      * @param array $notifications array of available_update_info objects to send
1851      */
1852     protected function cron_notify(array $notifications) {
1853         global $CFG;
1855         if (empty($notifications)) {
1856             return;
1857         }
1859         $admins = get_admins();
1861         if (empty($admins)) {
1862             return;
1863         }
1865         $this->cron_mtrace('sending notifications ... ', '');
1867         $text = get_string('updatenotifications', 'core_admin') . PHP_EOL;
1868         $html = html_writer::tag('h1', get_string('updatenotifications', 'core_admin')) . PHP_EOL;
1870         $coreupdates = array();
1871         $pluginupdates = array();
1873         foreach ($notifications as $notification) {
1874             if ($notification->component == 'core') {
1875                 $coreupdates[] = $notification;
1876             } else {
1877                 $pluginupdates[] = $notification;
1878             }
1879         }
1881         if (!empty($coreupdates)) {
1882             $text .= PHP_EOL . get_string('updateavailable', 'core_admin') . PHP_EOL;
1883             $html .= html_writer::tag('h2', get_string('updateavailable', 'core_admin')) . PHP_EOL;
1884             $html .= html_writer::start_tag('ul') . PHP_EOL;
1885             foreach ($coreupdates as $coreupdate) {
1886                 $html .= html_writer::start_tag('li');
1887                 if (isset($coreupdate->release)) {
1888                     $text .= get_string('updateavailable_release', 'core_admin', $coreupdate->release);
1889                     $html .= html_writer::tag('strong', get_string('updateavailable_release', 'core_admin', $coreupdate->release));
1890                 }
1891                 if (isset($coreupdate->version)) {
1892                     $text .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
1893                     $html .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
1894                 }
1895                 if (isset($coreupdate->maturity)) {
1896                     $text .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
1897                     $html .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
1898                 }
1899                 $text .= PHP_EOL;
1900                 $html .= html_writer::end_tag('li') . PHP_EOL;
1901             }
1902             $text .= PHP_EOL;
1903             $html .= html_writer::end_tag('ul') . PHP_EOL;
1905             $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/index.php');
1906             $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
1907             $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/index.php', $CFG->wwwroot.'/'.$CFG->admin.'/index.php'));
1908             $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
1909         }
1911         if (!empty($pluginupdates)) {
1912             $text .= PHP_EOL . get_string('updateavailableforplugin', 'core_admin') . PHP_EOL;
1913             $html .= html_writer::tag('h2', get_string('updateavailableforplugin', 'core_admin')) . PHP_EOL;
1915             $html .= html_writer::start_tag('ul') . PHP_EOL;
1916             foreach ($pluginupdates as $pluginupdate) {
1917                 $html .= html_writer::start_tag('li');
1918                 $text .= get_string('pluginname', $pluginupdate->component);
1919                 $html .= html_writer::tag('strong', get_string('pluginname', $pluginupdate->component));
1921                 $text .= ' ('.$pluginupdate->component.')';
1922                 $html .= ' ('.$pluginupdate->component.')';
1924                 $text .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
1925                 $html .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
1927                 $text .= PHP_EOL;
1928                 $html .= html_writer::end_tag('li') . PHP_EOL;
1929             }
1930             $text .= PHP_EOL;
1931             $html .= html_writer::end_tag('ul') . PHP_EOL;
1933             $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php');
1934             $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
1935             $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/plugins.php', $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php'));
1936             $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
1937         }
1939         $a = array('siteurl' => $CFG->wwwroot);
1940         $text .= get_string('updatenotificationfooter', 'core_admin', $a) . PHP_EOL;
1941         $a = array('siteurl' => html_writer::link($CFG->wwwroot, $CFG->wwwroot));
1942         $html .= html_writer::tag('footer', html_writer::tag('p', get_string('updatenotificationfooter', 'core_admin', $a),
1943             array('style' => 'font-size:smaller; color:#333;')));
1945         foreach ($admins as $admin) {
1946             $message = new stdClass();
1947             $message->component         = 'moodle';
1948             $message->name              = 'availableupdate';
1949             $message->userfrom          = get_admin();
1950             $message->userto            = $admin;
1951             $message->subject           = get_string('updatenotificationsubject', 'core_admin', array('siteurl' => $CFG->wwwroot));
1952             $message->fullmessage       = $text;
1953             $message->fullmessageformat = FORMAT_PLAIN;
1954             $message->fullmessagehtml   = $html;
1955             $message->smallmessage      = get_string('updatenotifications', 'core_admin');
1956             $message->notification      = 1;
1957             message_send($message);
1958         }
1959     }
1961     /**
1962      * Compare two release labels and decide if they are the same
1963      *
1964      * @param string $remote release info of the available update
1965      * @param null|string $local release info of the local code, defaults to $release defined in version.php
1966      * @return boolean true if the releases declare the same minor+major version
1967      */
1968     protected function is_same_release($remote, $local=null) {
1970         if (is_null($local)) {
1971             $this->load_current_environment();
1972             $local = $this->currentrelease;
1973         }
1975         $pattern = '/^([0-9\.\+]+)([^(]*)/';
1977         preg_match($pattern, $remote, $remotematches);
1978         preg_match($pattern, $local, $localmatches);
1980         $remotematches[1] = str_replace('+', '', $remotematches[1]);
1981         $localmatches[1] = str_replace('+', '', $localmatches[1]);
1983         if ($remotematches[1] === $localmatches[1] and rtrim($remotematches[2]) === rtrim($localmatches[2])) {
1984             return true;
1985         } else {
1986             return false;
1987         }
1988     }
1992 /**
1993  * Defines the structure of objects returned by {@link available_update_checker::get_update_info()}
1994  */
1995 class available_update_info {
1997     /** @var string frankenstyle component name */
1998     public $component;
1999     /** @var int the available version of the component */
2000     public $version;
2001     /** @var string|null optional release name */
2002     public $release = null;
2003     /** @var int|null optional maturity info, eg {@link MATURITY_STABLE} */
2004     public $maturity = null;
2005     /** @var string|null optional URL of a page with more info about the update */
2006     public $url = null;
2007     /** @var string|null optional URL of a ZIP package that can be downloaded and installed */
2008     public $download = null;
2009     /** @var string|null of self::download is set, then this must be the MD5 hash of the ZIP */
2010     public $downloadmd5 = null;
2012     /**
2013      * Creates new instance of the class
2014      *
2015      * The $info array must provide at least the 'version' value and optionally all other
2016      * values to populate the object's properties.
2017      *
2018      * @param string $name the frankenstyle component name
2019      * @param array $info associative array with other properties
2020      */
2021     public function __construct($name, array $info) {
2022         $this->component = $name;
2023         foreach ($info as $k => $v) {
2024             if (property_exists('available_update_info', $k) and $k != 'component') {
2025                 $this->$k = $v;
2026             }
2027         }
2028     }
2032 /**
2033  * Implements a communication bridge to the mdeploy.php utility
2034  */
2035 class available_update_deployer {
2037     const HTTP_PARAM_PREFIX     = 'updteautodpldata_';  // Hey, even Google has not heard of such a prefix! So it MUST be safe :-p
2038     const HTTP_PARAM_CHECKER    = 'datapackagesize';    // Name of the parameter that holds the number of items in the received data items
2040     /** @var available_update_deployer holds the singleton instance */
2041     protected static $singletoninstance;
2042     /** @var moodle_url URL of a page that includes the deployer UI */
2043     protected $callerurl;
2044     /** @var moodle_url URL to return after the deployment */
2045     protected $returnurl;
2047     /**
2048      * Direct instantiation not allowed, use the factory method {@link self::instance()}
2049      */
2050     protected function __construct() {
2051     }
2053     /**
2054      * Sorry, this is singleton
2055      */
2056     protected function __clone() {
2057     }
2059     /**
2060      * Factory method for this class
2061      *
2062      * @return available_update_deployer the singleton instance
2063      */
2064     public static function instance() {
2065         if (is_null(self::$singletoninstance)) {
2066             self::$singletoninstance = new self();
2067         }
2068         return self::$singletoninstance;
2069     }
2071     /**
2072      * Reset caches used by this script
2073      *
2074      * @param bool $phpunitreset is this called as a part of PHPUnit reset?
2075      */
2076     public static function reset_caches($phpunitreset = false) {
2077         if ($phpunitreset) {
2078             self::$singletoninstance = null;
2079         }
2080     }
2082     /**
2083      * Is automatic deployment enabled?
2084      *
2085      * @return bool
2086      */
2087     public function enabled() {
2088         global $CFG;
2090         if (!empty($CFG->disableupdateautodeploy)) {
2091             // The feature is prohibited via config.php
2092             return false;
2093         }
2095         return get_config('updateautodeploy');
2096     }
2098     /**
2099      * Sets some base properties of the class to make it usable.
2100      *
2101      * @param moodle_url $callerurl the base URL of a script that will handle the class'es form data
2102      * @param moodle_url $returnurl the final URL to return to when the deployment is finished
2103      */
2104     public function initialize(moodle_url $callerurl, moodle_url $returnurl) {
2106         if (!$this->enabled()) {
2107             throw new coding_exception('Unable to initialize the deployer, the feature is not enabled.');
2108         }
2110         $this->callerurl = $callerurl;
2111         $this->returnurl = $returnurl;
2112     }
2114     /**
2115      * Has the deployer been initialized?
2116      *
2117      * Initialized deployer means that the following properties were set:
2118      * callerurl, returnurl
2119      *
2120      * @return bool
2121      */
2122     public function initialized() {
2124         if (!$this->enabled()) {
2125             return false;
2126         }
2128         if (empty($this->callerurl)) {
2129             return false;
2130         }
2132         if (empty($this->returnurl)) {
2133             return false;
2134         }
2136         return true;
2137     }
2139     /**
2140      * Returns a list of reasons why the deployment can not happen
2141      *
2142      * If the returned array is empty, the deployment seems to be possible. The returned
2143      * structure is an associative array with keys representing individual impediments.
2144      * Possible keys are: missingdownloadurl, missingdownloadmd5, notwritable.
2145      *
2146      * @param available_update_info $info
2147      * @return array
2148      */
2149     public function deployment_impediments(available_update_info $info) {
2151         $impediments = array();
2153         if (empty($info->download)) {
2154             $impediments['missingdownloadurl'] = true;
2155         }
2157         if (empty($info->downloadmd5)) {
2158             $impediments['missingdownloadmd5'] = true;
2159         }
2161         if (!empty($info->download) and !$this->update_downloadable($info->download)) {
2162             $impediments['notdownloadable'] = true;
2163         }
2165         if (!$this->component_writable($info->component)) {
2166             $impediments['notwritable'] = true;
2167         }
2169         return $impediments;
2170     }
2172     /**
2173      * Check to see if the current version of the plugin seems to be a checkout of an external repository.
2174      *
2175      * @see plugin_manager::plugin_external_source()
2176      * @param available_update_info $info
2177      * @return false|string
2178      */
2179     public function plugin_external_source(available_update_info $info) {
2181         $paths = core_component::get_plugin_types();
2182         list($plugintype, $pluginname) = core_component::normalize_component($info->component);
2183         $pluginroot = $paths[$plugintype].'/'.$pluginname;
2185         if (is_dir($pluginroot.'/.git')) {
2186             return 'git';
2187         }
2189         if (is_dir($pluginroot.'/CVS')) {
2190             return 'cvs';
2191         }
2193         if (is_dir($pluginroot.'/.svn')) {
2194             return 'svn';
2195         }
2197         return false;
2198     }
2200     /**
2201      * Prepares a renderable widget to confirm installation of an available update.
2202      *
2203      * @param available_update_info $info component version to deploy
2204      * @return renderable
2205      */
2206     public function make_confirm_widget(available_update_info $info) {
2208         if (!$this->initialized()) {
2209             throw new coding_exception('Illegal method call - deployer not initialized.');
2210         }
2212         $params = $this->data_to_params(array(
2213             'updateinfo' => (array)$info,   // see http://www.php.net/manual/en/language.types.array.php#language.types.array.casting
2214         ));
2216         $widget = new single_button(
2217             new moodle_url($this->callerurl, $params),
2218             get_string('updateavailableinstall', 'core_admin'),
2219             'post'
2220         );
2222         return $widget;
2223     }
2225     /**
2226      * Prepares a renderable widget to execute installation of an available update.
2227      *
2228      * @param available_update_info $info component version to deploy
2229      * @param moodle_url $returnurl URL to return after the installation execution
2230      * @return renderable
2231      */
2232     public function make_execution_widget(available_update_info $info, moodle_url $returnurl = null) {
2233         global $CFG;
2235         if (!$this->initialized()) {
2236             throw new coding_exception('Illegal method call - deployer not initialized.');
2237         }
2239         $pluginrootpaths = core_component::get_plugin_types();
2241         list($plugintype, $pluginname) = core_component::normalize_component($info->component);
2243         if (empty($pluginrootpaths[$plugintype])) {
2244             throw new coding_exception('Unknown plugin type root location', $plugintype);
2245         }
2247         list($passfile, $password) = $this->prepare_authorization();
2249         if (is_null($returnurl)) {
2250             $returnurl = new moodle_url('/admin');
2251         } else {
2252             $returnurl = $returnurl;
2253         }
2255         $params = array(
2256             'upgrade' => true,
2257             'type' => $plugintype,
2258             'name' => $pluginname,
2259             'typeroot' => $pluginrootpaths[$plugintype],
2260             'package' => $info->download,
2261             'md5' => $info->downloadmd5,
2262             'dataroot' => $CFG->dataroot,
2263             'dirroot' => $CFG->dirroot,
2264             'passfile' => $passfile,
2265             'password' => $password,
2266             'returnurl' => $returnurl->out(false),
2267         );
2269         if (!empty($CFG->proxyhost)) {
2270             // MDL-36973 - Beware - we should call just !is_proxybypass() here. But currently, our
2271             // cURL wrapper class does not do it. So, to have consistent behaviour, we pass proxy
2272             // setting regardless the $CFG->proxybypass setting. Once the {@link curl} class is
2273             // fixed, the condition should be amended.
2274             if (true or !is_proxybypass($info->download)) {
2275                 if (empty($CFG->proxyport)) {
2276                     $params['proxy'] = $CFG->proxyhost;
2277                 } else {
2278                     $params['proxy'] = $CFG->proxyhost.':'.$CFG->proxyport;
2279                 }
2281                 if (!empty($CFG->proxyuser) and !empty($CFG->proxypassword)) {
2282                     $params['proxyuserpwd'] = $CFG->proxyuser.':'.$CFG->proxypassword;
2283                 }
2285                 if (!empty($CFG->proxytype)) {
2286                     $params['proxytype'] = $CFG->proxytype;
2287                 }
2288             }
2289         }
2291         $widget = new single_button(
2292             new moodle_url('/mdeploy.php', $params),
2293             get_string('updateavailableinstall', 'core_admin'),
2294             'post'
2295         );
2297         return $widget;
2298     }
2300     /**
2301      * Returns array of data objects passed to this tool.
2302      *
2303      * @return array
2304      */
2305     public function submitted_data() {
2307         $data = $this->params_to_data($_POST);
2309         if (empty($data) or empty($data[self::HTTP_PARAM_CHECKER])) {
2310             return false;
2311         }
2313         if (!empty($data['updateinfo']) and is_object($data['updateinfo'])) {
2314             $updateinfo = $data['updateinfo'];
2315             if (!empty($updateinfo->component) and !empty($updateinfo->version)) {
2316                 $data['updateinfo'] = new available_update_info($updateinfo->component, (array)$updateinfo);
2317             }
2318         }
2320         if (!empty($data['callerurl'])) {
2321             $data['callerurl'] = new moodle_url($data['callerurl']);
2322         }
2324         if (!empty($data['returnurl'])) {
2325             $data['returnurl'] = new moodle_url($data['returnurl']);
2326         }
2328         return $data;
2329     }
2331     /**
2332      * Handles magic getters and setters for protected properties.
2333      *
2334      * @param string $name method name, e.g. set_returnurl()
2335      * @param array $arguments arguments to be passed to the array
2336      */
2337     public function __call($name, array $arguments = array()) {
2339         if (substr($name, 0, 4) === 'set_') {
2340             $property = substr($name, 4);
2341             if (empty($property)) {
2342                 throw new coding_exception('Invalid property name (empty)');
2343             }
2344             if (empty($arguments)) {
2345                 $arguments = array(true); // Default value for flag-like properties.
2346             }
2347             // Make sure it is a protected property.
2348             $isprotected = false;
2349             $reflection = new ReflectionObject($this);
2350             foreach ($reflection->getProperties(ReflectionProperty::IS_PROTECTED) as $reflectionproperty) {
2351                 if ($reflectionproperty->getName() === $property) {
2352                     $isprotected = true;
2353                     break;
2354                 }
2355             }
2356             if (!$isprotected) {
2357                 throw new coding_exception('Unable to set property - it does not exist or it is not protected');
2358             }
2359             $value = reset($arguments);
2360             $this->$property = $value;
2361             return;
2362         }
2364         if (substr($name, 0, 4) === 'get_') {
2365             $property = substr($name, 4);
2366             if (empty($property)) {
2367                 throw new coding_exception('Invalid property name (empty)');
2368             }
2369             if (!empty($arguments)) {
2370                 throw new coding_exception('No parameter expected');
2371             }
2372             // Make sure it is a protected property.
2373             $isprotected = false;
2374             $reflection = new ReflectionObject($this);
2375             foreach ($reflection->getProperties(ReflectionProperty::IS_PROTECTED) as $reflectionproperty) {
2376                 if ($reflectionproperty->getName() === $property) {
2377                     $isprotected = true;
2378                     break;
2379                 }
2380             }
2381             if (!$isprotected) {
2382                 throw new coding_exception('Unable to get property - it does not exist or it is not protected');
2383             }
2384             return $this->$property;
2385         }
2386     }
2388     /**
2389      * Generates a random token and stores it in a file in moodledata directory.
2390      *
2391      * @return array of the (string)filename and (string)password in this order
2392      */
2393     public function prepare_authorization() {
2394         global $CFG;
2396         make_upload_directory('mdeploy/auth/');
2398         $attempts = 0;
2399         $success = false;
2401         while (!$success and $attempts < 5) {
2402             $attempts++;
2404             $passfile = $this->generate_passfile();
2405             $password = $this->generate_password();
2406             $now = time();
2408             $filepath = $CFG->dataroot.'/mdeploy/auth/'.$passfile;
2410             if (!file_exists($filepath)) {
2411                 $success = file_put_contents($filepath, $password . PHP_EOL . $now . PHP_EOL, LOCK_EX);
2412                 chmod($filepath, $CFG->filepermissions);
2413             }
2414         }
2416         if ($success) {
2417             return array($passfile, $password);
2419         } else {
2420             throw new moodle_exception('unable_prepare_authorization', 'core_plugin');
2421         }
2422     }
2424     // End of external API
2426     /**
2427      * Prepares an array of HTTP parameters that can be passed to another page.
2428      *
2429      * @param array|object $data associative array or an object holding the data, data JSON-able
2430      * @return array suitable as a param for moodle_url
2431      */
2432     protected function data_to_params($data) {
2434         // Append some our own data
2435         if (!empty($this->callerurl)) {
2436             $data['callerurl'] = $this->callerurl->out(false);
2437         }
2438         if (!empty($this->returnurl)) {
2439             $data['returnurl'] = $this->returnurl->out(false);
2440         }
2442         // Finally append the count of items in the package.
2443         $data[self::HTTP_PARAM_CHECKER] = count($data);
2445         // Generate params
2446         $params = array();
2447         foreach ($data as $name => $value) {
2448             $transname = self::HTTP_PARAM_PREFIX.$name;
2449             $transvalue = json_encode($value);
2450             $params[$transname] = $transvalue;
2451         }
2453         return $params;
2454     }
2456     /**
2457      * Converts HTTP parameters passed to the script into native PHP data
2458      *
2459      * @param array $params such as $_REQUEST or $_POST
2460      * @return array data passed for this class
2461      */
2462     protected function params_to_data(array $params) {
2464         if (empty($params)) {
2465             return array();
2466         }
2468         $data = array();
2469         foreach ($params as $name => $value) {
2470             if (strpos($name, self::HTTP_PARAM_PREFIX) === 0) {
2471                 $realname = substr($name, strlen(self::HTTP_PARAM_PREFIX));
2472                 $realvalue = json_decode($value);
2473                 $data[$realname] = $realvalue;
2474             }
2475         }
2477         return $data;
2478     }
2480     /**
2481      * Returns a random string to be used as a filename of the password storage.
2482      *
2483      * @return string
2484      */
2485     protected function generate_passfile() {
2486         return clean_param(uniqid('mdeploy_', true), PARAM_FILE);
2487     }
2489     /**
2490      * Returns a random string to be used as the authorization token
2491      *
2492      * @return string
2493      */
2494     protected function generate_password() {
2495         return complex_random_string();
2496     }
2498     /**
2499      * Checks if the given component's directory is writable
2500      *
2501      * For the purpose of the deployment, the web server process has to have
2502      * write access to all files in the component's directory (recursively) and for the
2503      * directory itself.
2504      *
2505      * @see worker::move_directory_source_precheck()
2506      * @param string $component normalized component name
2507      * @return boolean
2508      */
2509     protected function component_writable($component) {
2511         list($plugintype, $pluginname) = core_component::normalize_component($component);
2513         $directory = core_component::get_plugin_directory($plugintype, $pluginname);
2515         if (is_null($directory)) {
2516             throw new coding_exception('Unknown component location', $component);
2517         }
2519         return $this->directory_writable($directory);
2520     }
2522     /**
2523      * Checks if the mdeploy.php will be able to fetch the ZIP from the given URL
2524      *
2525      * This is mainly supposed to check if the transmission over HTTPS would
2526      * work. That is, if the CA certificates are present at the server.
2527      *
2528      * @param string $downloadurl the URL of the ZIP package to download
2529      * @return bool
2530      */
2531     protected function update_downloadable($downloadurl) {
2532         global $CFG;
2534         $curloptions = array(
2535             'CURLOPT_SSL_VERIFYHOST' => 2,      // this is the default in {@link curl} class but just in case
2536             'CURLOPT_SSL_VERIFYPEER' => true,
2537         );
2539         $curl = new curl(array('proxy' => true));
2540         $result = $curl->head($downloadurl, $curloptions);
2541         $errno = $curl->get_errno();
2542         if (empty($errno)) {
2543             return true;
2544         } else {
2545             return false;
2546         }
2547     }
2549     /**
2550      * Checks if the directory and all its contents (recursively) is writable
2551      *
2552      * @param string $path full path to a directory
2553      * @return boolean
2554      */
2555     private function directory_writable($path) {
2557         if (!is_writable($path)) {
2558             return false;
2559         }
2561         if (is_dir($path)) {
2562             $handle = opendir($path);
2563         } else {
2564             return false;
2565         }
2567         $result = true;
2569         while ($filename = readdir($handle)) {
2570             $filepath = $path.'/'.$filename;
2572             if ($filename === '.' or $filename === '..') {
2573                 continue;
2574             }
2576             if (is_dir($filepath)) {
2577                 $result = $result && $this->directory_writable($filepath);
2579             } else {
2580                 $result = $result && is_writable($filepath);
2581             }
2582         }
2584         closedir($handle);
2586         return $result;
2587     }
2591 /**
2592  * Factory class producing required subclasses of {@link plugininfo_base}
2593  */
2594 class plugininfo_default_factory {
2596     /**
2597      * Makes a new instance of the plugininfo class
2598      *
2599      * @param string $type the plugin type, eg. 'mod'
2600      * @param string $typerootdir full path to the location of all the plugins of this type
2601      * @param string $name the plugin name, eg. 'workshop'
2602      * @param string $namerootdir full path to the location of the plugin
2603      * @param string $typeclass the name of class that holds the info about the plugin
2604      * @return plugininfo_base the instance of $typeclass
2605      */
2606     public static function make($type, $typerootdir, $name, $namerootdir, $typeclass) {
2607         $plugin              = new $typeclass();
2608         $plugin->type        = $type;
2609         $plugin->typerootdir = $typerootdir;
2610         $plugin->name        = $name;
2611         $plugin->rootdir     = $namerootdir;
2613         $plugin->init_display_name();
2614         $plugin->load_disk_version();
2615         $plugin->load_db_version();
2616         $plugin->init_is_standard();
2618         return $plugin;
2619     }
2623 /**
2624  * Base class providing access to the information about a plugin
2625  *
2626  * @property-read string component the component name, type_name
2627  */
2628 abstract class plugininfo_base {
2630     /** @var string the plugintype name, eg. mod, auth or workshopform */
2631     public $type;
2632     /** @var string full path to the location of all the plugins of this type */
2633     public $typerootdir;
2634     /** @var string the plugin name, eg. assignment, ldap */
2635     public $name;
2636     /** @var string the localized plugin name */
2637     public $displayname;
2638     /** @var string the plugin source, one of plugin_manager::PLUGIN_SOURCE_xxx constants */
2639     public $source;
2640     /** @var string fullpath to the location of this plugin */
2641     public $rootdir;
2642     /** @var int|string the version of the plugin's source code */
2643     public $versiondisk;
2644     /** @var int|string the version of the installed plugin */
2645     public $versiondb;
2646     /** @var int|float|string required version of Moodle core  */
2647     public $versionrequires;
2648     /** @var array other plugins that this one depends on, lazy-loaded by {@link get_other_required_plugins()} */
2649     public $dependencies;
2650     /** @var int number of instances of the plugin - not supported yet */
2651     public $instances;
2652     /** @var int order of the plugin among other plugins of the same type - not supported yet */
2653     public $sortorder;
2654     /** @var array|null array of {@link available_update_info} for this plugin */
2655     public $availableupdates;
2657     /**
2658      * Finds all enabled plugins, the result may include missing plugins.
2659      * @return array|null of enabled plugins $pluginname=>$pluginname, null means unknown
2660      */
2661     public static function get_enabled_plugins() {
2662         return null;
2663     }
2665     /**
2666      * Gathers and returns the information about all plugins of the given type,
2667      * either on disk or previously installed.
2668      *
2669      * @param string $type the name of the plugintype, eg. mod, auth or workshopform
2670      * @param string $typerootdir full path to the location of the plugin dir
2671      * @param string $typeclass the name of the actually called class
2672      * @return array of plugintype classes, indexed by the plugin name
2673      */
2674     public static function get_plugins($type, $typerootdir, $typeclass) {
2675         // Get the information about plugins at the disk.
2676         $plugins = core_component::get_plugin_list($type);
2677         $return = array();
2678         foreach ($plugins as $pluginname => $pluginrootdir) {
2679             $return[$pluginname] = plugininfo_default_factory::make($type, $typerootdir,
2680                 $pluginname, $pluginrootdir, $typeclass);
2681         }
2683         // Fetch missing incorrectly uninstalled plugins.
2684         $manager = plugin_manager::instance();
2685         $plugins = $manager->get_installed_plugins($type);
2687         foreach ($plugins as $name => $version) {
2688             if (isset($return[$name])) {
2689                 continue;
2690             }
2691             $plugin              = new $typeclass();
2692             $plugin->type        = $type;
2693             $plugin->typerootdir = $typerootdir;
2694             $plugin->name        = $name;
2695             $plugin->rootdir     = null;
2696             $plugin->displayname = $name;
2697             $plugin->versiondb   = $version;
2698             $plugin->init_is_standard();
2700             $return[$name] = $plugin;
2701         }
2703         return $return;
2704     }
2706     /**
2707      * Is this plugin already installed and updated?
2708      * @return bool true if plugin installed and upgraded.
2709      */
2710     public function is_updated() {
2711         if (!$this->rootdir) {
2712             return false;
2713         }
2714         if ($this->versiondb === null and $this->versiondisk === null) {
2715             // There is no version.php or version info inside,
2716             // for now let's pretend it is ok.
2717             // TODO: return false once we require version in each plugin.
2718             return true;
2719         }
2721         return ((float)$this->versiondb === (float)$this->versiondisk);
2722     }
2724     /**
2725      * Sets {@link $displayname} property to a localized name of the plugin
2726      */
2727     public function init_display_name() {
2728         if (!get_string_manager()->string_exists('pluginname', $this->component)) {
2729             $this->displayname = '[pluginname,' . $this->component . ']';
2730         } else {
2731             $this->displayname = get_string('pluginname', $this->component);
2732         }
2733     }
2735     /**
2736      * Magic method getter, redirects to read only values.
2737      *
2738      * @param string $name
2739      * @return mixed
2740      */
2741     public function __get($name) {
2742         switch ($name) {
2743             case 'component': return $this->type . '_' . $this->name;
2745             default:
2746                 debugging('Invalid plugin property accessed! '.$name);
2747                 return null;
2748         }
2749     }
2751     /**
2752      * Return the full path name of a file within the plugin.
2753      *
2754      * No check is made to see if the file exists.
2755      *
2756      * @param string $relativepath e.g. 'version.php'.
2757      * @return string e.g. $CFG->dirroot . '/mod/quiz/version.php'.
2758      */
2759     public function full_path($relativepath) {
2760         if (empty($this->rootdir)) {
2761             return '';
2762         }
2763         return $this->rootdir . '/' . $relativepath;
2764     }
2766     /**
2767      * Sets {@link $versiondisk} property to a numerical value representing the
2768      * version of the plugin's source code.
2769      *
2770      * If the value is null after calling this method, either the plugin
2771      * does not use versioning (typically does not have any database
2772      * data) or is missing from disk.
2773      */
2774     public function load_disk_version() {
2775         $versions = plugin_manager::instance()->get_present_plugins($this->type);
2777         $this->versiondisk = null;
2778         $this->versionrequires = null;
2779         $this->dependencies = array();
2781         if (!isset($versions[$this->name])) {
2782             return;
2783         }
2785         $plugin = $versions[$this->name];
2787         if (isset($plugin->version)) {
2788             $this->versiondisk = $plugin->version;
2789         }
2790         if (isset($plugin->requires)) {
2791             $this->versionrequires = $plugin->requires;
2792         }
2793         if (isset($plugin->dependencies)) {
2794             $this->dependencies = $plugin->dependencies;
2795         }
2796     }
2798     /**
2799      * Get the list of other plugins that this plugin requires to be installed.
2800      *
2801      * @return array with keys the frankenstyle plugin name, and values either
2802      *      a version string (like '2011101700') or the constant ANY_VERSION.
2803      */
2804     public function get_other_required_plugins() {
2805         if (is_null($this->dependencies)) {
2806             $this->load_disk_version();
2807         }
2808         return $this->dependencies;
2809     }
2811     /**
2812      * Is this is a subplugin?
2813      *
2814      * @return boolean
2815      */
2816     public function is_subplugin() {
2817         return ($this->get_parent_plugin() !== false);
2818     }
2820     /**
2821      * If I am a subplugin, return the name of my parent plugin.
2822      *
2823      * @return string|bool false if not a subplugin, name of the parent otherwise
2824      */
2825     public function get_parent_plugin() {
2826         return $this->get_plugin_manager()->get_parent_of_subplugin($this->type);
2827     }
2829     /**
2830      * Sets {@link $versiondb} property to a numerical value representing the
2831      * currently installed version of the plugin.
2832      *
2833      * If the value is null after calling this method, either the plugin
2834      * does not use versioning (typically does not have any database
2835      * data) or has not been installed yet.
2836      */
2837     public function load_db_version() {
2838         $versions = plugin_manager::instance()->get_installed_plugins($this->type);
2840         if (isset($versions[$this->name])) {
2841             $this->versiondb = $versions[$this->name];
2842         } else {
2843             $this->versiondb = null;
2844         }
2845     }
2847     /**
2848      * Sets {@link $source} property to one of plugin_manager::PLUGIN_SOURCE_xxx
2849      * constants.
2850      *
2851      * If the property's value is null after calling this method, then
2852      * the type of the plugin has not been recognized and you should throw
2853      * an exception.
2854      */
2855     public function init_is_standard() {
2857         $standard = plugin_manager::standard_plugins_list($this->type);
2859         if ($standard !== false) {
2860             $standard = array_flip($standard);
2861             if (isset($standard[$this->name])) {
2862                 $this->source = plugin_manager::PLUGIN_SOURCE_STANDARD;
2863             } else if (!is_null($this->versiondb) and is_null($this->versiondisk)
2864                     and plugin_manager::is_deleted_standard_plugin($this->type, $this->name)) {
2865                 $this->source = plugin_manager::PLUGIN_SOURCE_STANDARD; // to be deleted
2866             } else {
2867                 $this->source = plugin_manager::PLUGIN_SOURCE_EXTENSION;
2868             }
2869         }
2870     }
2872     /**
2873      * Returns true if the plugin is shipped with the official distribution
2874      * of the current Moodle version, false otherwise.
2875      *
2876      * @return bool
2877      */
2878     public function is_standard() {
2879         return $this->source === plugin_manager::PLUGIN_SOURCE_STANDARD;
2880     }
2882     /**
2883      * Returns true if the the given Moodle version is enough to run this plugin
2884      *
2885      * @param string|int|double $moodleversion
2886      * @return bool
2887      */
2888     public function is_core_dependency_satisfied($moodleversion) {
2890         if (empty($this->versionrequires)) {
2891             return true;
2893         } else {
2894             return (double)$this->versionrequires <= (double)$moodleversion;
2895         }
2896     }
2898     /**
2899      * Returns the status of the plugin
2900      *
2901      * @return string one of plugin_manager::PLUGIN_STATUS_xxx constants
2902      */
2903     public function get_status() {
2905         if (is_null($this->versiondb) and is_null($this->versiondisk)) {
2906             return plugin_manager::PLUGIN_STATUS_NODB;
2908         } else if (is_null($this->versiondb) and !is_null($this->versiondisk)) {
2909             return plugin_manager::PLUGIN_STATUS_NEW;
2911         } else if (!is_null($this->versiondb) and is_null($this->versiondisk)) {
2912             if (plugin_manager::is_deleted_standard_plugin($this->type, $this->name)) {
2913                 return plugin_manager::PLUGIN_STATUS_DELETE;
2914             } else {
2915                 return plugin_manager::PLUGIN_STATUS_MISSING;
2916             }
2918         } else if ((float)$this->versiondb === (float)$this->versiondisk) {
2919             // Note: the float comparison should work fine here
2920             //       because there are no arithmetic operations with the numbers.
2921             return plugin_manager::PLUGIN_STATUS_UPTODATE;
2923         } else if ($this->versiondb < $this->versiondisk) {
2924             return plugin_manager::PLUGIN_STATUS_UPGRADE;
2926         } else if ($this->versiondb > $this->versiondisk) {
2927             return plugin_manager::PLUGIN_STATUS_DOWNGRADE;
2929         } else {
2930             // $version = pi(); and similar funny jokes - hopefully Donald E. Knuth will never contribute to Moodle ;-)
2931             throw new coding_exception('Unable to determine plugin state, check the plugin versions');
2932         }
2933     }
2935     /**
2936      * Returns the information about plugin availability
2937      *
2938      * True means that the plugin is enabled. False means that the plugin is
2939      * disabled. Null means that the information is not available, or the
2940      * plugin does not support configurable availability or the availability
2941      * can not be changed.
2942      *
2943      * @return null|bool
2944      */
2945     public function is_enabled() {
2946         if (!$this->rootdir) {
2947             // Plugin missing.
2948             return false;
2949         }
2951         $enabled = plugin_manager::instance()->get_enabled_plugins($this->type);
2953         if (!is_array($enabled)) {
2954             return null;
2955         }
2957         return isset($enabled[$this->name]);
2958     }
2960     /**
2961      * Populates the property {@link $availableupdates} with the information provided by
2962      * available update checker
2963      *
2964      * @param available_update_checker $provider the class providing the available update info
2965      */
2966     public function check_available_updates(available_update_checker $provider) {
2967         global $CFG;
2969         if (isset($CFG->updateminmaturity)) {
2970             $minmaturity = $CFG->updateminmaturity;
2971         } else {
2972             // this can happen during the very first upgrade to 2.3
2973             $minmaturity = MATURITY_STABLE;
2974         }
2976         $this->availableupdates = $provider->get_update_info($this->component,
2977             array('minmaturity' => $minmaturity));
2978     }
2980     /**
2981      * If there are updates for this plugin available, returns them.
2982      *
2983      * Returns array of {@link available_update_info} objects, if some update
2984      * is available. Returns null if there is no update available or if the update
2985      * availability is unknown.
2986      *
2987      * @return array|null
2988      */
2989     public function available_updates() {
2991         if (empty($this->availableupdates) or !is_array($this->availableupdates)) {
2992             return null;
2993         }
2995         $updates = array();
2997         foreach ($this->availableupdates as $availableupdate) {
2998             if ($availableupdate->version > $this->versiondisk) {
2999                 $updates[] = $availableupdate;
3000             }
3001         }
3003         if (empty($updates)) {
3004             return null;
3005         }
3007         return $updates;
3008     }
3010     /**
3011      * Returns the node name used in admin settings menu for this plugin settings (if applicable)
3012      *
3013      * @return null|string node name or null if plugin does not create settings node (default)
3014      */
3015     public function get_settings_section_name() {
3016         return null;
3017     }
3019     /**
3020      * Returns the URL of the plugin settings screen
3021      *
3022      * Null value means that the plugin either does not have the settings screen
3023      * or its location is not available via this library.
3024      *
3025      * @return null|moodle_url
3026      */
3027     public function get_settings_url() {
3028         $section = $this->get_settings_section_name();
3029         if ($section === null) {
3030             return null;
3031         }
3032         $settings = admin_get_root()->locate($section);
3033         if ($settings && $settings instanceof admin_settingpage) {
3034             return new moodle_url('/admin/settings.php', array('section' => $section));
3035         } else if ($settings && $settings instanceof admin_externalpage) {
3036             return new moodle_url($settings->url);
3037         } else {
3038             return null;
3039         }
3040     }
3042     /**
3043      * Loads plugin settings to the settings tree
3044      *
3045      * This function usually includes settings.php file in plugins folder.
3046      * Alternatively it can create a link to some settings page (instance of admin_externalpage)
3047      *
3048      * @param part_of_admin_tree $adminroot
3049      * @param string $parentnodename
3050      * @param bool $hassiteconfig whether the current user has moodle/site:config capability
3051      */
3052     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3053     }
3055     /**
3056      * Should there be a way to uninstall the plugin via the administration UI
3057      *
3058      * By default, uninstallation is allowed for all non-standard add-ons. Subclasses
3059      * may want to override this to allow uninstallation of all plugins (simply by
3060      * returning true unconditionally). Subplugins follow their parent plugin's
3061      * decision by default.
3062      *
3063      * Note that even if true is returned, the core may still prohibit the uninstallation,
3064      * e.g. in case there are other plugins that depend on this one.
3065      *
3066      * @return bool
3067      */
3068     public function is_uninstall_allowed() {
3070         if ($this->is_subplugin()) {
3071             return $this->get_plugin_manager()->get_plugin_info($this->get_parent_plugin())->is_uninstall_allowed();
3072         }
3074         if ($this->is_standard()) {
3075             return false;
3076         }
3078         return true;
3079     }
3081     /**
3082      * Optional extra warning before uninstallation, for example number of uses in courses.
3083      *
3084      * @return string
3085      */
3086     public function get_uninstall_extra_warning() {
3087         return '';
3088     }
3090     /**
3091      * Returns the URL of the screen where this plugin can be uninstalled
3092      *
3093      * Visiting that URL must be safe, that is a manual confirmation is needed
3094      * for actual uninstallation of the plugin. By default, URL to a common
3095      * uninstalling tool is returned.
3096      *
3097      * @return moodle_url
3098      */
3099     public function get_uninstall_url() {
3100         return $this->get_default_uninstall_url();
3101     }
3103     /**
3104      * Returns relative directory of the plugin with heading '/'
3105      *
3106      * @return string
3107      */
3108     public function get_dir() {
3109         global $CFG;
3111         return substr($this->rootdir, strlen($CFG->dirroot));
3112     }
3114     /**
3115      * Hook method to implement certain steps when uninstalling the plugin.
3116      *
3117      * This hook is called by {@link plugin_manager::uninstall_plugin()} so
3118      * it is basically usable only for those plugin types that use the default
3119      * uninstall tool provided by {@link self::get_default_uninstall_url()}.
3120      *
3121      * @param progress_trace $progress traces the process
3122      * @return bool true on success, false on failure
3123      */
3124     public function uninstall(progress_trace $progress) {
3125         return true;
3126     }
3128     /**
3129      * Returns URL to a script that handles common plugin uninstall procedure.
3130      *
3131      * This URL is suitable for plugins that do not have their own UI
3132      * for uninstalling.
3133      *
3134      * @return moodle_url
3135      */
3136     protected final function get_default_uninstall_url() {
3137         return new moodle_url('/admin/plugins.php', array(
3138             'sesskey' => sesskey(),
3139             'uninstall' => $this->component,
3140             'confirm' => 0,
3141         ));
3142     }
3144     /**
3145      * Provides access to the plugin_manager singleton.
3146      *
3147      * @return plugin_manager
3148      */
3149     protected function get_plugin_manager() {
3150         return plugin_manager::instance();
3151     }
3155 /**
3156  * General class for all plugin types that do not have their own class
3157  */
3158 class plugininfo_general extends plugininfo_base {
3162 /**
3163  * Class for page side blocks
3164  */
3165 class plugininfo_block extends plugininfo_base {
3166     /**
3167      * Finds all enabled plugins, the result may include missing plugins.
3168      * @return array|null of enabled plugins $pluginname=>$pluginname, null means unknown
3169      */
3170     public static function get_enabled_plugins() {
3171         global $DB;
3173         return $DB->get_records_menu('block', array('visible'=>1), 'name ASC', 'name, name AS val');
3174     }
3176     /**
3177      * Magic method getter, redirects to read only values.
3178      *
3179      * For block plugins pretends the object has 'visible' property for compatibility
3180      * with plugins developed for Moodle version below 2.4
3181      *
3182      * @param string $name
3183      * @return mixed
3184      */
3185     public function __get($name) {
3186         if ($name === 'visible') {
3187             debugging('This is now an instance of plugininfo_block, please use $block->is_enabled() instead of $block->visible', DEBUG_DEVELOPER);
3188             return ($this->is_enabled() !== false);
3189         }
3190         return parent::__get($name);
3191     }
3193     public function init_display_name() {
3195         if (get_string_manager()->string_exists('pluginname', 'block_' . $this->name)) {
3196             $this->displayname = get_string('pluginname', 'block_' . $this->name);
3198         } else if (($block = block_instance($this->name)) !== false) {
3199             $this->displayname = $block->get_title();
3201         } else {
3202             parent::init_display_name();
3203         }
3204     }
3206     public function get_settings_section_name() {
3207         return 'blocksetting' . $this->name;
3208     }
3210     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3211         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3212         $ADMIN = $adminroot; // may be used in settings.php
3213         $block = $this; // also can be used inside settings.php
3215         if (!$this->rootdir) {
3216             // Plugin missing.
3217             return;
3218         }
3220         $section = $this->get_settings_section_name();
3222         if (!$hassiteconfig || (($blockinstance = block_instance($this->name)) === false)) {
3223             return;
3224         }
3226         $settings = null;
3227         if ($blockinstance->has_config()) {
3228             if (file_exists($this->full_path('settings.php'))) {
3229                 $settings = new admin_settingpage($section, $this->displayname,
3230                         'moodle/site:config', $this->is_enabled() === false);
3231                 include($this->full_path('settings.php')); // this may also set $settings to null
3232             }
3233         }
3234         if ($settings) {
3235             $ADMIN->add($parentnodename, $settings);
3236         }
3237     }
3239     public function is_uninstall_allowed() {
3240         return true;
3241     }
3243     /**
3244      * Warnign with number of block instances.
3245      *
3246      * @return string
3247      */
3248     public function get_uninstall_extra_warning() {
3249         global $DB;
3251         if (!$count = $DB->count_records('block_instances', array('blockname'=>$this->name))) {
3252             return '';
3253         }
3255         return '<p>'.get_string('uninstallextraconfirmblock', 'core_plugin', array('instances'=>$count)).'</p>';
3256     }
3260 /**
3261  * Class for text filters
3262  */
3263 class plugininfo_filter extends plugininfo_base {
3265     public function init_display_name() {
3266         if (!get_string_manager()->string_exists('filtername', $this->component)) {
3267             $this->displayname = '[filtername,' . $this->component . ']';
3268         } else {
3269             $this->displayname = get_string('filtername', $this->component);
3270         }
3271     }
3273     /**
3274      * Finds all enabled plugins, the result may include missing plugins.
3275      * @return array|null of enabled plugins $pluginname=>$pluginname, null means unknown
3276      */
3277     public static function get_enabled_plugins() {
3278         global $DB, $CFG;
3279         require_once("$CFG->libdir/filterlib.php");
3281         $enabled = array();
3282         $filters = $DB->get_records_select('filter_active', "active <> :disabled", array('disabled'=>TEXTFILTER_DISABLED), 'filter ASC', 'id, filter');
3283         foreach ($filters as $filter) {
3284             $enabled[$filter->filter] = $filter->filter;
3285         }
3287         return $enabled;
3288     }
3290     public function get_settings_section_name() {
3291         return 'filtersetting' . $this->name;
3292     }
3294     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3295         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3296         $ADMIN = $adminroot; // may be used in settings.php
3297         $filter = $this; // also can be used inside settings.php
3299         if (!$this->rootdir) {
3300             // Plugin missing.
3301             return;
3302         }
3304         $settings = null;
3305         if ($hassiteconfig && file_exists($this->full_path('filtersettings.php'))) {
3306             $section = $this->get_settings_section_name();
3307             $settings = new admin_settingpage($section, $this->displayname,
3308                     'moodle/site:config', $this->is_enabled() === false);
3309             include($this->full_path('filtersettings.php')); // this may also set $settings to null
3310         }
3311         if ($settings) {
3312             $ADMIN->add($parentnodename, $settings);
3313         }
3314     }
3316     public function is_uninstall_allowed() {
3317         return true;
3318     }
3320     public function get_uninstall_url() {
3321         return new moodle_url('/admin/filters.php', array('sesskey' => sesskey(), 'filterpath' => $this->name, 'action' => 'delete'));
3322     }
3326 /**
3327  * Class for activity modules
3328  */
3329 class plugininfo_mod extends plugininfo_base {
3330     /**
3331      * Finds all enabled plugins, the result may include missing plugins.
3332      * @return array|null of enabled plugins $pluginname=>$pluginname, null means unknown
3333      */
3334     public static function get_enabled_plugins() {
3335         global $DB;
3336         return $DB->get_records_menu('modules', array('visible'=>1), 'name ASC', 'name, name AS val');
3337     }
3339     /**
3340      * Magic method getter, redirects to read only values.
3341      *
3342      * For module plugins we pretend the object has 'visible' property for compatibility
3343      * with plugins developed for Moodle version below 2.4
3344      *
3345      * @param string $name
3346      * @return mixed
3347      */
3348     public function __get($name) {
3349         if ($name === 'visible') {
3350             debugging('This is now an instance of plugininfo_mod, please use $module->is_enabled() instead of $module->visible', DEBUG_DEVELOPER);
3351             return ($this->is_enabled() !== false);
3352         }
3353         return parent::__get($name);
3354     }
3356     public function init_display_name() {
3357         if (get_string_manager()->string_exists('pluginname', $this->component)) {
3358             $this->displayname = get_string('pluginname', $this->component);
3359         } else {
3360             $this->displayname = get_string('modulename', $this->component);
3361         }
3362     }
3364     public function get_settings_section_name() {
3365         return 'modsetting' . $this->name;
3366     }
3368     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3369         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3370         $ADMIN = $adminroot; // may be used in settings.php
3371         $module = $this; // also can be used inside settings.php
3373         if (!$this->rootdir) {
3374             // Plugin missing.
3375             return;
3376         }
3378         $section = $this->get_settings_section_name();
3380         $settings = null;
3381         if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3382             $settings = new admin_settingpage($section, $this->displayname,
3383                     'moodle/site:config', $this->is_enabled() === false);
3384             include($this->full_path('settings.php')); // this may also set $settings to null
3385         }
3386         if ($settings) {
3387             $ADMIN->add($parentnodename, $settings);
3388         }
3389     }
3391     /**
3392      * Allow all activity modules but Forum to be uninstalled.
3394      * This exception for the Forum has been hard-coded in Moodle since ages,
3395      * we may want to re-think it one day.
3396      */
3397     public function is_uninstall_allowed() {
3398         if ($this->name === 'forum') {
3399             return false;
3400         } else {
3401             return true;
3402         }
3403     }
3405     /**
3406      * Return warning with number of activities and number of affected courses.
3407      *
3408      * @return string
3409      */
3410     public function get_uninstall_extra_warning() {
3411         global $DB;
3413         if (!$module = $DB->get_record('modules', array('name'=>$this->name))) {
3414             return '';
3415         }
3417         if (!$count = $DB->count_records('course_modules', array('module'=>$module->id))) {
3418             return '';
3419         }
3421         $sql = "SELECT COUNT('x')
3422                   FROM (
3423                     SELECT course
3424                       FROM {course_modules}
3425                      WHERE module = :mid
3426                   GROUP BY course
3427                   ) c";
3428         $courses = $DB->count_records_sql($sql, array('mid'=>$module->id));
3430         return '<p>'.get_string('uninstallextraconfirmmod', 'core_plugin', array('instances'=>$count, 'courses'=>$courses)).'</p>';
3431     }
3435 /**
3436  * Class for question behaviours.
3437  */
3438 class plugininfo_qbehaviour extends plugininfo_base {
3440     public function is_uninstall_allowed() {
3441         return true;
3442     }
3444     public function get_uninstall_url() {
3445         return new moodle_url('/admin/qbehaviours.php',
3446                 array('delete' => $this->name, 'sesskey' => sesskey()));
3447     }
3451 /**
3452  * Class for question types
3453  */
3454 class plugininfo_qtype extends plugininfo_base {
3456     public function is_uninstall_allowed() {
3457         return true;
3458     }
3460     public function get_uninstall_url() {
3461         return new moodle_url('/admin/qtypes.php',
3462                 array('delete' => $this->name, 'sesskey' => sesskey()));
3463     }
3465     public function get_settings_section_name() {
3466         return 'qtypesetting' . $this->name;
3467     }
3469     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3470         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3471         $ADMIN = $adminroot; // may be used in settings.php
3472         $qtype = $this; // also can be used inside settings.php
3474         if (!$this->rootdir) {
3475             // Most probably somebody deleted dir without proper uninstall.
3476             return;
3477         }
3478         $section = $this->get_settings_section_name();
3480         $settings = null;
3481         $systemcontext = context_system::instance();
3482         if (($hassiteconfig || has_capability('moodle/question:config', $systemcontext)) &&
3483                 file_exists($this->full_path('settings.php'))) {
3484             $settings = new admin_settingpage($section, $this->displayname,
3485                     'moodle/question:config', $this->is_enabled() === false);
3486             include($this->full_path('settings.php')); // this may also set $settings to null
3487         }
3488         if ($settings) {
3489             $ADMIN->add($parentnodename, $settings);
3490         }
3491     }
3495 /**
3496  * Class for authentication plugins
3497  */
3498 class plugininfo_auth extends plugininfo_base {
3499     /**
3500      * Finds all enabled plugins, the result may include missing plugins.
3501      * @return array|null of enabled plugins $pluginname=>$pluginname, null means unknown
3502      */
3503     public static function get_enabled_plugins() {
3504         global $CFG;
3506         // These two are always enabled and can't be disabled.
3507         $enabled = array('nologin'=>'nologin', 'manual'=>'manual');
3508         foreach (explode(',', $CFG->auth) as $auth) {
3509             $enabled[$auth] = $auth;
3510         }
3512         return $enabled;
3513     }
3515     public function get_settings_section_name() {
3516         return 'authsetting' . $this->name;
3517     }
3519     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3520         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3521         $ADMIN = $adminroot; // may be used in settings.php
3522         $auth = $this; // also to be used inside settings.php
3524         if (!$this->rootdir) {
3525             // Plugin missing.
3526             return;
3527         }
3529         $section = $this->get_settings_section_name();
3531         $settings = null;
3532         if ($hassiteconfig) {
3533             if (file_exists($this->full_path('settings.php'))) {
3534                 // TODO: finish implementation of common settings - locking, etc.
3535                 $settings = new admin_settingpage($section, $this->displayname,
3536                         'moodle/site:config', $this->is_enabled() === false);
3537                 include($this->full_path('settings.php')); // this may also set $settings to null
3538             } else {
3539                 $settingsurl = new moodle_url('/admin/auth_config.php', array('auth' => $this->name));
3540                 $settings = new admin_externalpage($section, $this->displayname,
3541                         $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
3542             }
3543         }
3544         if ($settings) {
3545             $ADMIN->add($parentnodename, $settings);
3546         }
3547     }
3551 /**
3552  * Class for enrolment plugins
3553  */
3554 class plugininfo_enrol extends plugininfo_base {
3555     /**
3556      * Finds all enabled plugins, the result may include missing plugins.
3557      * @return array|null of enabled plugins $pluginname=>$pluginname, null means unknown
3558      */
3559     public static function get_enabled_plugins() {
3560         global $CFG;
3562         $enabled = array();
3563         foreach (explode(',', $CFG->enrol_plugins_enabled) as $enrol) {
3564             $enabled[$enrol] = $enrol;
3565         }
3567         return $enabled;
3568     }
3570     public function get_settings_section_name() {
3571         if (file_exists($this->full_path('settings.php'))) {
3572             return 'enrolsettings' . $this->name;
3573         } else {
3574             return null;
3575         }
3576     }
3578     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3579         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3581         if (!$this->rootdir) {
3582             // Plugin missing.
3583             return;
3584         }
3586         if (!$hassiteconfig or !file_exists($this->full_path('settings.php'))) {
3587             return;
3588         }
3589         $section = $this->get_settings_section_name();
3591         $ADMIN = $adminroot; // may be used in settings.php
3592         $enrol = $this; // also can be used inside settings.php
3593         $settings = new admin_settingpage($section, $this->displayname,
3594                 'moodle/site:config', $this->is_enabled() === false);
3596         include($this->full_path('settings.php')); // This may also set $settings to null!
3598         if ($settings) {
3599             $ADMIN->add($parentnodename, $settings);
3600         }
3601     }
3603     public function is_uninstall_allowed() {
3604         if ($this->name === 'manual') {
3605             return false;
3606         }
3607         return true;
3608     }
3610     /**
3611      * Return warning with number of activities and number of affected courses.
3612      *
3613      * @return string
3614      */
3615     public function get_uninstall_extra_warning() {
3616         global $DB, $OUTPUT;
3618         $sql = "SELECT COUNT('x')
3619                   FROM {user_enrolments} ue
3620                   JOIN {enrol} e ON e.id = ue.enrolid
3621                  WHERE e.enrol = :plugin";
3622         $count = $DB->count_records_sql($sql, array('plugin'=>$this->name));
3624         if (!$count) {
3625             return '';
3626         }
3628         $migrateurl = new moodle_url('/admin/enrol.php', array('action'=>'migrate', 'enrol'=>$this->name, 'sesskey'=>sesskey()));
3629         $migrate = new single_button($migrateurl, get_string('migratetomanual', 'core_enrol'));
3630         $button = $OUTPUT->render($migrate);
3632         $result = '<p>'.get_string('uninstallextraconfirmenrol', 'core_plugin', array('enrolments'=>$count)).'</p>';
3633         $result .= $button;
3635         return $result;
3636     }
3640 /**
3641  * Class for messaging processors
3642  */
3643 class plugininfo_message extends plugininfo_base {
3644     /**
3645      * Finds all enabled plugins, the result may include missing plugins.
3646      * @return array|null of enabled plugins $pluginname=>$pluginname, null means unknown
3647      */
3648     public static function get_enabled_plugins() {
3649         global $DB;
3650         return $DB->get_records_menu('message_processors', array('enabled'=>1), 'name ASC', 'name, name AS val');
3651     }
3653     public function get_settings_section_name() {
3654         return 'messagesetting' . $this->name;
3655     }
3657     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3658         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3659         $ADMIN = $adminroot; // may be used in settings.php
3661         if (!$this->rootdir) {
3662             // Plugin missing.
3663             return;
3664         }
3666         if (!$hassiteconfig) {
3667             return;
3668         }
3669         $section = $this->get_settings_section_name();
3671         $settings = null;
3672         $processors = get_message_processors();
3673         if (isset($processors[$this->name])) {
3674             $processor = $processors[$this->name];
3675             if ($processor->available && $processor->hassettings) {
3676                 $settings = new admin_settingpage($section, $this->displayname,
3677                         'moodle/site:config', $this->is_enabled() === false);
3678                 include($this->full_path('settings.php')); // this may also set $settings to null
3679             }
3680         }
3681         if ($settings) {
3682             $ADMIN->add($parentnodename, $settings);
3683         }
3684     }
3686     public function is_uninstall_allowed() {
3687         $processors = get_message_processors();
3688         if (isset($processors[$this->name])) {
3689             return true;
3690         } else {
3691             return false;
3692         }
3693     }
3695     /**
3696      * @see plugintype_interface::get_uninstall_url()
3697      */
3698     public function get_uninstall_url() {
3699         $processors = get_message_processors();
3700         return new moodle_url('/admin/message.php', array('uninstall' => $processors[$this->name]->id, 'sesskey' => sesskey()));
3701     }
3705 /**
3706  * Class for repositories
3707  */
3708 class plugininfo_repository extends plugininfo_base {
3709     /**
3710      * Finds all enabled plugins, the result may include missing plugins.
3711      * @return array|null of enabled plugins $pluginname=>$pluginname, null means unknown
3712      */
3713     public static function get_enabled_plugins() {
3714         global $DB;
3715         return $DB->get_records_menu('repository', array('visible'=>1), 'type ASC', 'type, type AS val');
3716     }
3718     public function get_settings_section_name() {
3719         return 'repositorysettings'.$this->name;
3720     }
3722     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3723         if (!$this->rootdir) {
3724             // Plugin missing.
3725             return;
3726         }
3728         if ($hassiteconfig && $this->is_enabled()) {
3729             // completely no access to repository setting when it is not enabled
3730             $sectionname = $this->get_settings_section_name();
3731             $settingsurl = new moodle_url('/admin/repository.php',
3732                     array('sesskey' => sesskey(), 'action' => 'edit', 'repos' => $this->name));
3733             $settings = new admin_externalpage($sectionname, $this->displayname,
3734                     $settingsurl, 'moodle/site:config', false);
3735             $adminroot->add($parentnodename, $settings);
3736         }
3737     }
3741 /**
3742  * Class for portfolios
3743  */
3744 class plugininfo_portfolio extends plugininfo_base {
3745     /**
3746      * Finds all enabled plugins, the result may include missing plugins.
3747      * @return array|null of enabled plugins $pluginname=>$pluginname, null means unknown
3748      */
3749     public static function get_enabled_plugins() {
3750         global $DB;
3752         $enabled = array();
3753         $rs = $DB->get_recordset('portfolio_instance', array('visible'=>1), 'plugin ASC', 'plugin');
3754         foreach ($rs as $repository) {
3755             $enabled[$repository->plugin] = $repository->plugin;
3756         }
3758         return $enabled;
3759     }
3763 /**
3764  * Class for themes
3765  */
3766 class plugininfo_theme extends plugininfo_base {
3768     public function is_enabled() {
3769         global $CFG;
3771         if ((!empty($CFG->theme) and $CFG->theme === $this->name) or
3772             (!empty($CFG->themelegacy) and $CFG->themelegacy === $this->name)) {
3773             return true;
3774         } else {
3775             return parent::is_enabled();
3776         }
3777     }
3781 /**
3782  * Class representing an MNet service
3783  */
3784 class plugininfo_mnetservice extends plugininfo_base {
3786     public function is_enabled() {
3787         global $CFG;
3789         if (empty($CFG->mnet_dispatcher_mode) || $CFG->mnet_dispatcher_mode !== 'strict') {
3790             return false;
3791         } else {
3792             return parent::is_enabled();
3793         }
3794     }
3798 /**
3799  * Class for admin tool plugins
3800  */
3801 class plugininfo_tool extends plugininfo_base {
3803     public function is_uninstall_allowed() {
3804         return true;
3805     }
3809 /**
3810  * Class for admin tool plugins
3811  */
3812 class plugininfo_report extends plugininfo_base {
3814     public function is_uninstall_allowed() {
3815         return true;
3816     }
3820 /**
3821  * Class for local plugins
3822  */
3823 class plugininfo_local extends plugininfo_base {
3825     public function is_uninstall_allowed() {
3826         return true;
3827     }
3830 /**
3831  * Class for HTML editors
3832  */
3833 class plugininfo_editor extends plugininfo_base {
3834     /**
3835      * Finds all enabled plugins, the result may include missing plugins.
3836      * @return array|null of enabled plugins $pluginname=>$pluginname, null means unknown
3837      */
3838     public static function get_enabled_plugins() {
3839         global $CFG;
3841         if (empty($CFG->texteditors)) {
3842             return array('tinymce'=>'tinymce', 'textarea'=>'textarea');
3843         }
3845         $enabled = array();
3846         foreach (explode(',', $CFG->texteditors) as $editor) {
3847             $enabled[$editor] = $editor;
3848         }
3850         return $enabled;
3851     }
3853     public function get_settings_section_name() {
3854         return 'editorsettings' . $this->name;
3855     }
3857     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3858         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3859         $ADMIN = $adminroot; // may be used in settings.php
3860         $editor = $this; // also can be used inside settings.php
3862         if (!$this->rootdir) {
3863             // Plugin missing.
3864             return;
3865         }
3867         $section = $this->get_settings_section_name();
3869         $settings = null;
3870         if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3871             $settings = new admin_settingpage($section, $this->displayname,
3872                     'moodle/site:config', $this->is_enabled() === false);
3873             include($this->full_path('settings.php')); // this may also set $settings to null
3874         }
3875         if ($settings) {
3876             $ADMIN->add($parentnodename, $settings);
3877         }
3878     }
3880     /**
3881      * Basic textarea editor can not be uninstalled.
3882      */
3883     public function is_uninstall_allowed() {
3884         if ($this->name === 'textarea') {
3885             return false;
3886         } else {
3887             return true;
3888         }
3889     }
3892 /**
3893  * Class for plagiarism plugins
3894  */
3895 class plugininfo_plagiarism extends plugininfo_base {
3897     public function get_settings_section_name() {
3898         return 'plagiarism'. $this->name;
3899     }
3901     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3902         if (!$this->rootdir) {
3903             // Plugin missing.
3904             return;
3905         }
3907         // plagiarism plugin just redirect to settings.php in the plugins directory
3908         if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3909             $section = $this->get_settings_section_name();
3910             $settingsurl = new moodle_url($this->get_dir().'/settings.php');
3911             $settings = new admin_externalpage($section, $this->displayname,
3912                     $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
3913             $adminroot->add($parentnodename, $settings);
3914         }
3915     }
3917     public function is_uninstall_allowed() {
3918         return true;
3919     }
3922 /**
3923  * Class for webservice protocols
3924  */
3925 class plugininfo_webservice extends plugininfo_base {
3926     /**
3927      * Finds all enabled plugins, the result may include missing plugins.
3928      * @return array|null of enabled plugins $pluginname=>$pluginname, null means unknown
3929      */
3930     public static function get_enabled_plugins() {
3931         global $CFG;
3933         if (empty($CFG->enablewebservices) or empty($CFG->webserviceprotocols)) {
3934             return array();
3935         }
3937         $enabled = array();
3938         foreach (explode(',', $CFG->webserviceprotocols) as $protocol) {
3939             $enabled[$protocol] = $protocol;
3940         }
3942         return $enabled;
3943     }
3945     public function get_settings_section_name() {
3946         return 'webservicesetting' . $this->name;
3947     }
3949     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3950         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3951         $ADMIN = $adminroot; // may be used in settings.php
3952         $webservice = $this; // also can be used inside settings.php
3954         if (!$this->rootdir) {
3955             // Plugin missing.
3956             return;
3957         }
3959         $section = $this->get_settings_section_name();
3961         $settings = null;
3962         if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3963             $settings = new admin_settingpage($section, $this->displayname,
3964                     'moodle/site:config', $this->is_enabled() === false);
3965             include($this->full_path('settings.php')); // this may also set $settings to null
3966         }
3967         if ($settings) {
3968             $ADMIN->add($parentnodename, $settings);
3969         }
3970     }
3972     public function is_uninstall_allowed() {
3973         return false;
3974     }
3977 /**
3978  * Class for course formats
3979  */
3980 class plugininfo_format extends plugininfo_base {
3981     /**
3982      * Finds all enabled plugins, the result may include missing plugins.
3983      * @return array|null of enabled plugins $pluginname=>$pluginname, null means unknown
3984      */
3985     public static function get_enabled_plugins() {
3986         global $DB;
3988         $plugins = plugin_manager::instance()->get_installed_plugins('format');
3989         if (!$plugins) {
3990             return array();
3991         }
3992         $installed = array();
3993         foreach ($plugins as $plugin => $version) {
3994             $installed[] = 'format_'.$plugin;
3995         }
3997         list($installed, $params) = $DB->get_in_or_equal($installed, SQL_PARAMS_NAMED);
3998         $disabled = $DB->get_recordset_select('config_plugins', "plugin $installed AND name = 'disabled'", $params, 'plugin ASC');
3999         foreach ($disabled as $conf) {
4000             if (empty($conf->value)) {
4001                 continue;
4002             }
4003             list($type, $name) = explode('_', $conf->component, 2);
4004             unset($plugins[$name]);
4005         }
4007         $enabled = array();
4008         foreach ($plugins as $plugin => $version) {
4009             $enabled[$plugin] = $plugin;
4010         }
4012         return $enabled;
4013     }
4015     /**
4016      * Gathers and returns the information about all plugins of the given type
4017      *
4018      * @param string $type the name of the plugintype, eg. mod, auth or workshopform
4019      * @param string $typerootdir full path to the location of the plugin dir
4020      * @param string $typeclass the name of the actually called class
4021      * @return array of plugintype classes, indexed by the plugin name
4022      */
4023     public static function get_plugins($type, $typerootdir, $typeclass) {
4024         global $CFG;
4025         $formats = parent::get_plugins($type, $typerootdir, $typeclass);
4026         require_once($CFG->dirroot.'/course/lib.php');
4027         $order = get_sorted_course_formats();
4028         $sortedformats = array();
4029         foreach ($order as $formatname) {
4030             $sortedformats[$formatname] = $formats[$formatname];
4031         }
4032         return $sortedformats;
4033     }
4035     public function get_settings_section_name() {
4036         return 'formatsetting' . $this->name;
4037     }
4039     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
4040         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
4041         $ADMIN = $adminroot; // also may be used in settings.php
4043         if (!$this->rootdir) {
4044             // Plugin missing.
4045             return;
4046         }
4048         $section = $this->get_settings_section_name();
4050         $settings = null;
4051         if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
4052             $settings = new admin_settingpage($section, $this->displayname,
4053                     'moodle/site:config', $this->is_enabled() === false);
4054             include($this->full_path('settings.php')); // this may also set $settings to null
4055         }
4056         if ($settings) {
4057             $ADMIN->add($parentnodename, $settings);
4058         }
4059     }
4061     public function is_uninstall_allowed() {
4062         if ($this->name !== get_config('moodlecourse', 'format') && $this->name !== 'site') {
4063             return true;
4064         } else {
4065             return false;
4066         }
4067     }
4069     public function get_uninstall_extra_warning() {
4070         global $DB;
4072         $coursecount = $DB->count_records('course', array('format' => $this->name));
4074         if (!$coursecount) {
4075             return '';
4076         }
4078         $defaultformat = $this->get_plugin_manager()->plugin_name('format_'.get_config('moodlecourse', 'format'));
4079         $message = get_string(
4080             'formatuninstallwithcourses', 'core_admin',
4081             (object)array('count' => $coursecount, 'format' => $this->displayname,
4082             'defaultformat' => $defaultformat));
4084         return $message;
4085     }