MDL-39056 Use new version of API when fetching available updates info
[moodle.git] / lib / pluginlib.php
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
18 /**
19  * Defines classes used for plugins management
20  *
21  * This library provides a unified interface to various plugin types in
22  * Moodle. It is mainly used by the plugins management admin page and the
23  * plugins check page during the upgrade.
24  *
25  * @package    core
26  * @subpackage admin
27  * @copyright  2011 David Mudrak <david@moodle.com>
28  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
29  */
31 defined('MOODLE_INTERNAL') || die();
33 /**
34  * Singleton class providing general plugins management functionality
35  */
36 class plugin_manager {
38     /** the plugin is shipped with standard Moodle distribution */
39     const PLUGIN_SOURCE_STANDARD    = 'std';
40     /** the plugin is added extension */
41     const PLUGIN_SOURCE_EXTENSION   = 'ext';
43     /** the plugin uses neither database nor capabilities, no versions */
44     const PLUGIN_STATUS_NODB        = 'nodb';
45     /** the plugin is up-to-date */
46     const PLUGIN_STATUS_UPTODATE    = 'uptodate';
47     /** the plugin is about to be installed */
48     const PLUGIN_STATUS_NEW         = 'new';
49     /** the plugin is about to be upgraded */
50     const PLUGIN_STATUS_UPGRADE     = 'upgrade';
51     /** the standard plugin is about to be deleted */
52     const PLUGIN_STATUS_DELETE     = 'delete';
53     /** the version at the disk is lower than the one already installed */
54     const PLUGIN_STATUS_DOWNGRADE   = 'downgrade';
55     /** the plugin is installed but missing from disk */
56     const PLUGIN_STATUS_MISSING     = 'missing';
58     /** @var plugin_manager holds the singleton instance */
59     protected static $singletoninstance;
60     /** @var array of raw plugins information */
61     protected $pluginsinfo = null;
62     /** @var array of raw subplugins information */
63     protected $subpluginsinfo = null;
65     /**
66      * Direct initiation not allowed, use the factory method {@link self::instance()}
67      */
68     protected function __construct() {
69     }
71     /**
72      * Sorry, this is singleton
73      */
74     protected function __clone() {
75     }
77     /**
78      * Factory method for this class
79      *
80      * @return plugin_manager the singleton instance
81      */
82     public static function instance() {
83         if (is_null(self::$singletoninstance)) {
84             self::$singletoninstance = new self();
85         }
86         return self::$singletoninstance;
87     }
89     /**
90      * Reset any caches
91      * @param bool $phpunitreset
92      */
93     public static function reset_caches($phpunitreset = false) {
94         if ($phpunitreset) {
95             self::$singletoninstance = null;
96         }
97     }
99     /**
100      * Returns the result of {@link get_plugin_types()} ordered for humans
101      *
102      * @see self::reorder_plugin_types()
103      * @param bool $fullpaths false means relative paths from dirroot
104      * @return array (string)name => (string)location
105      */
106     public function get_plugin_types($fullpaths = true) {
107         return $this->reorder_plugin_types(get_plugin_types($fullpaths));
108     }
110     /**
111      * Returns a tree of known plugins and information about them
112      *
113      * @param bool $disablecache force reload, cache can be used otherwise
114      * @return array 2D array. The first keys are plugin type names (e.g. qtype);
115      *      the second keys are the plugin local name (e.g. multichoice); and
116      *      the values are the corresponding objects extending {@link plugininfo_base}
117      */
118     public function get_plugins($disablecache=false) {
119         global $CFG;
121         if ($disablecache or is_null($this->pluginsinfo)) {
122             // Hack: include mod and editor subplugin management classes first,
123             //       the adminlib.php is supposed to contain extra admin settings too.
124             require_once($CFG->libdir.'/adminlib.php');
125             foreach(array('mod', 'editor') as $type) {
126                 foreach (get_plugin_list($type) as $dir) {
127                     if (file_exists("$dir/adminlib.php")) {
128                         include_once("$dir/adminlib.php");
129                     }
130                 }
131             }
132             $this->pluginsinfo = array();
133             $plugintypes = $this->get_plugin_types();
134             foreach ($plugintypes as $plugintype => $plugintyperootdir) {
135                 if (in_array($plugintype, array('base', 'general'))) {
136                     throw new coding_exception('Illegal usage of reserved word for plugin type');
137                 }
138                 if (class_exists('plugininfo_' . $plugintype)) {
139                     $plugintypeclass = 'plugininfo_' . $plugintype;
140                 } else {
141                     $plugintypeclass = 'plugininfo_general';
142                 }
143                 if (!in_array('plugininfo_base', class_parents($plugintypeclass))) {
144                     throw new coding_exception('Class ' . $plugintypeclass . ' must extend plugininfo_base');
145                 }
146                 $plugins = call_user_func(array($plugintypeclass, 'get_plugins'), $plugintype, $plugintyperootdir, $plugintypeclass);
147                 $this->pluginsinfo[$plugintype] = $plugins;
148             }
150             if (empty($CFG->disableupdatenotifications) and !during_initial_install()) {
151                 // append the information about available updates provided by {@link available_update_checker()}
152                 $provider = available_update_checker::instance();
153                 foreach ($this->pluginsinfo as $plugintype => $plugins) {
154                     foreach ($plugins as $plugininfoholder) {
155                         $plugininfoholder->check_available_updates($provider);
156                     }
157                 }
158             }
159         }
161         return $this->pluginsinfo;
162     }
164     /**
165      * Returns list of plugins that define their subplugins and the information
166      * about them from the db/subplugins.php file.
167      *
168      * At the moment, only activity modules and editors can define subplugins.
169      *
170      * @param bool $disablecache force reload, cache can be used otherwise
171      * @return array with keys like 'mod_quiz', and values the data from the
172      *      corresponding db/subplugins.php file.
173      */
174     public function get_subplugins($disablecache=false) {
176         if ($disablecache or is_null($this->subpluginsinfo)) {
177             $this->subpluginsinfo = array();
178             foreach (array('mod', 'editor') as $type) {
179                 $owners = get_plugin_list($type);
180                 foreach ($owners as $component => $ownerdir) {
181                     $componentsubplugins = array();
182                     if (file_exists($ownerdir . '/db/subplugins.php')) {
183                         $subplugins = array();
184                         include($ownerdir . '/db/subplugins.php');
185                         foreach ($subplugins as $subplugintype => $subplugintyperootdir) {
186                             $subplugin = new stdClass();
187                             $subplugin->type = $subplugintype;
188                             $subplugin->typerootdir = $subplugintyperootdir;
189                             $componentsubplugins[$subplugintype] = $subplugin;
190                         }
191                         $this->subpluginsinfo[$type . '_' . $component] = $componentsubplugins;
192                     }
193                 }
194             }
195         }
197         return $this->subpluginsinfo;
198     }
200     /**
201      * Returns the name of the plugin that defines the given subplugin type
202      *
203      * If the given subplugin type is not actually a subplugin, returns false.
204      *
205      * @param string $subplugintype the name of subplugin type, eg. workshopform or quiz
206      * @return false|string the name of the parent plugin, eg. mod_workshop
207      */
208     public function get_parent_of_subplugin($subplugintype) {
210         $parent = false;
211         foreach ($this->get_subplugins() as $pluginname => $subplugintypes) {
212             if (isset($subplugintypes[$subplugintype])) {
213                 $parent = $pluginname;
214                 break;
215             }
216         }
218         return $parent;
219     }
221     /**
222      * Returns a localized name of a given plugin
223      *
224      * @param string $plugin name of the plugin, eg mod_workshop or auth_ldap
225      * @return string
226      */
227     public function plugin_name($plugin) {
228         list($type, $name) = normalize_component($plugin);
229         return $this->pluginsinfo[$type][$name]->displayname;
230     }
232     /**
233      * Returns a localized name of a plugin typed in singular form
234      *
235      * Most plugin types define their names in core_plugin lang file. In case of subplugins,
236      * we try to ask the parent plugin for the name. In the worst case, we will return
237      * the value of the passed $type parameter.
238      *
239      * @param string $type the type of the plugin, e.g. mod or workshopform
240      * @return string
241      */
242     public function plugintype_name($type) {
244         if (get_string_manager()->string_exists('type_' . $type, 'core_plugin')) {
245             // for most plugin types, their names are defined in core_plugin lang file
246             return get_string('type_' . $type, 'core_plugin');
248         } else if ($parent = $this->get_parent_of_subplugin($type)) {
249             // if this is a subplugin, try to ask the parent plugin for the name
250             if (get_string_manager()->string_exists('subplugintype_' . $type, $parent)) {
251                 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type, $parent);
252             } else {
253                 return $this->plugin_name($parent) . ' / ' . $type;
254             }
256         } else {
257             return $type;
258         }
259     }
261     /**
262      * Returns a localized name of a plugin type in plural form
263      *
264      * Most plugin types define their names in core_plugin lang file. In case of subplugins,
265      * we try to ask the parent plugin for the name. In the worst case, we will return
266      * the value of the passed $type parameter.
267      *
268      * @param string $type the type of the plugin, e.g. mod or workshopform
269      * @return string
270      */
271     public function plugintype_name_plural($type) {
273         if (get_string_manager()->string_exists('type_' . $type . '_plural', 'core_plugin')) {
274             // for most plugin types, their names are defined in core_plugin lang file
275             return get_string('type_' . $type . '_plural', 'core_plugin');
277         } else if ($parent = $this->get_parent_of_subplugin($type)) {
278             // if this is a subplugin, try to ask the parent plugin for the name
279             if (get_string_manager()->string_exists('subplugintype_' . $type . '_plural', $parent)) {
280                 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type . '_plural', $parent);
281             } else {
282                 return $this->plugin_name($parent) . ' / ' . $type;
283             }
285         } else {
286             return $type;
287         }
288     }
290     /**
291      * @param string $component frankenstyle component name.
292      * @return plugininfo_base|null the corresponding plugin information.
293      */
294     public function get_plugin_info($component) {
295         list($type, $name) = normalize_component($component);
296         $plugins = $this->get_plugins();
297         if (isset($plugins[$type][$name])) {
298             return $plugins[$type][$name];
299         } else {
300             return null;
301         }
302     }
304     /**
305      * Get a list of any other plugins that require this one.
306      * @param string $component frankenstyle component name.
307      * @return array of frankensyle component names that require this one.
308      */
309     public function other_plugins_that_require($component) {
310         $others = array();
311         foreach ($this->get_plugins() as $type => $plugins) {
312             foreach ($plugins as $plugin) {
313                 $required = $plugin->get_other_required_plugins();
314                 if (isset($required[$component])) {
315                     $others[] = $plugin->component;
316                 }
317             }
318         }
319         return $others;
320     }
322     /**
323      * Check a dependencies list against the list of installed plugins.
324      * @param array $dependencies compenent name to required version or ANY_VERSION.
325      * @return bool true if all the dependencies are satisfied.
326      */
327     public function are_dependencies_satisfied($dependencies) {
328         foreach ($dependencies as $component => $requiredversion) {
329             $otherplugin = $this->get_plugin_info($component);
330             if (is_null($otherplugin)) {
331                 return false;
332             }
334             if ($requiredversion != ANY_VERSION and $otherplugin->versiondisk < $requiredversion) {
335                 return false;
336             }
337         }
339         return true;
340     }
342     /**
343      * Checks all dependencies for all installed plugins
344      *
345      * This is used by install and upgrade. The array passed by reference as the second
346      * argument is populated with the list of plugins that have failed dependencies (note that
347      * a single plugin can appear multiple times in the $failedplugins).
348      *
349      * @param int $moodleversion the version from version.php.
350      * @param array $failedplugins to return the list of plugins with non-satisfied dependencies
351      * @return bool true if all the dependencies are satisfied for all plugins.
352      */
353     public function all_plugins_ok($moodleversion, &$failedplugins = array()) {
355         $return = true;
356         foreach ($this->get_plugins() as $type => $plugins) {
357             foreach ($plugins as $plugin) {
359                 if (!$plugin->is_core_dependency_satisfied($moodleversion)) {
360                     $return = false;
361                     $failedplugins[] = $plugin->component;
362                 }
364                 if (!$this->are_dependencies_satisfied($plugin->get_other_required_plugins())) {
365                     $return = false;
366                     $failedplugins[] = $plugin->component;
367                 }
368             }
369         }
371         return $return;
372     }
374     /**
375      * Checks if there are some plugins with a known available update
376      *
377      * @return bool true if there is at least one available update
378      */
379     public function some_plugins_updatable() {
380         foreach ($this->get_plugins() as $type => $plugins) {
381             foreach ($plugins as $plugin) {
382                 if ($plugin->available_updates()) {
383                     return true;
384                 }
385             }
386         }
388         return false;
389     }
391     /**
392      * Defines a list of all plugins that were originally shipped in the standard Moodle distribution,
393      * but are not anymore and are deleted during upgrades.
394      *
395      * The main purpose of this list is to hide missing plugins during upgrade.
396      *
397      * @param string $type plugin type
398      * @param string $name plugin name
399      * @return bool
400      */
401     public static function is_deleted_standard_plugin($type, $name) {
403         // Example of the array structure:
404         // $plugins = array(
405         //     'block' => array('admin', 'admin_tree'),
406         //     'mod' => array('assignment'),
407         // );
408         // Do not include plugins that were removed during upgrades to versions that are
409         // not supported as source versions for upgrade any more. For example, at MOODLE_23_STABLE
410         // branch, listed should be no plugins that were removed at 1.9.x - 2.1.x versions as
411         // Moodle 2.3 supports upgrades from 2.2.x only.
412         $plugins = array(
413             'qformat' => array('blackboard'),
414         );
416         if (!isset($plugins[$type])) {
417             return false;
418         }
419         return in_array($name, $plugins[$type]);
420     }
422     /**
423      * Defines a white list of all plugins shipped in the standard Moodle distribution
424      *
425      * @param string $type
426      * @return false|array array of standard plugins or false if the type is unknown
427      */
428     public static function standard_plugins_list($type) {
429         $standard_plugins = array(
431             'assignment' => array(
432                 'offline', 'online', 'upload', 'uploadsingle'
433             ),
435             'assignsubmission' => array(
436                 'comments', 'file', 'onlinetext'
437             ),
439             'assignfeedback' => array(
440                 'comments', 'file', 'offline'
441             ),
443             'auth' => array(
444                 'cas', 'db', 'email', 'fc', 'imap', 'ldap', 'manual', 'mnet',
445                 'nntp', 'nologin', 'none', 'pam', 'pop3', 'radius',
446                 'shibboleth', 'webservice'
447             ),
449             'block' => array(
450                 'activity_modules', 'admin_bookmarks', 'badges', 'blog_menu',
451                 'blog_recent', 'blog_tags', 'calendar_month',
452                 'calendar_upcoming', 'comments', 'community',
453                 'completionstatus', 'course_list', 'course_overview',
454                 'course_summary', 'feedback', 'glossary_random', 'html',
455                 'login', 'mentees', 'messages', 'mnet_hosts', 'myprofile',
456                 'navigation', 'news_items', 'online_users', 'participants',
457                 'private_files', 'quiz_results', 'recent_activity',
458                 'rss_client', 'search_forums', 'section_links',
459                 'selfcompletion', 'settings', 'site_main_menu',
460                 'social_activities', 'tag_flickr', 'tag_youtube', 'tags'
461             ),
463             'booktool' => array(
464                 'exportimscp', 'importhtml', 'print'
465             ),
467             'cachelock' => array(
468                 'file'
469             ),
471             'cachestore' => array(
472                 'file', 'memcache', 'memcached', 'mongodb', 'session', 'static'
473             ),
475             'coursereport' => array(
476                 //deprecated!
477             ),
479             'datafield' => array(
480                 'checkbox', 'date', 'file', 'latlong', 'menu', 'multimenu',
481                 'number', 'picture', 'radiobutton', 'text', 'textarea', 'url'
482             ),
484             'datapreset' => array(
485                 'imagegallery'
486             ),
488             'editor' => array(
489                 'textarea', 'tinymce'
490             ),
492             'enrol' => array(
493                 'authorize', 'category', 'cohort', 'database', 'flatfile',
494                 'guest', 'imsenterprise', 'ldap', 'manual', 'meta', 'mnet',
495                 'paypal', 'self'
496             ),
498             'filter' => array(
499                 'activitynames', 'algebra', 'censor', 'emailprotect',
500                 'emoticon', 'mediaplugin', 'multilang', 'tex', 'tidy',
501                 'urltolink', 'data', 'glossary'
502             ),
504             'format' => array(
505                 'scorm', 'social', 'topics', 'weeks'
506             ),
508             'gradeexport' => array(
509                 'ods', 'txt', 'xls', 'xml'
510             ),
512             'gradeimport' => array(
513                 'csv', 'xml'
514             ),
516             'gradereport' => array(
517                 'grader', 'outcomes', 'overview', 'user'
518             ),
520             'gradingform' => array(
521                 'rubric', 'guide'
522             ),
524             'local' => array(
525             ),
527             'message' => array(
528                 'email', 'jabber', 'popup'
529             ),
531             'mnetservice' => array(
532                 'enrol'
533             ),
535             'mod' => array(
536                 'assign', 'assignment', 'book', 'chat', 'choice', 'data', 'feedback', 'folder',
537                 'forum', 'glossary', 'imscp', 'label', 'lesson', 'lti', 'page',
538                 'quiz', 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop'
539             ),
541             'plagiarism' => array(
542             ),
544             'portfolio' => array(
545                 'boxnet', 'download', 'flickr', 'googledocs', 'mahara', 'picasa'
546             ),
548             'profilefield' => array(
549                 'checkbox', 'datetime', 'menu', 'text', 'textarea'
550             ),
552             'qbehaviour' => array(
553                 'adaptive', 'adaptivenopenalty', 'deferredcbm',
554                 'deferredfeedback', 'immediatecbm', 'immediatefeedback',
555                 'informationitem', 'interactive', 'interactivecountback',
556                 'manualgraded', 'missing'
557             ),
559             'qformat' => array(
560                 'aiken', 'blackboard_six', 'examview', 'gift',
561                 'learnwise', 'missingword', 'multianswer', 'webct',
562                 'xhtml', 'xml'
563             ),
565             'qtype' => array(
566                 'calculated', 'calculatedmulti', 'calculatedsimple',
567                 'description', 'essay', 'match', 'missingtype', 'multianswer',
568                 'multichoice', 'numerical', 'random', 'randomsamatch',
569                 'shortanswer', 'truefalse'
570             ),
572             'quiz' => array(
573                 'grading', 'overview', 'responses', 'statistics'
574             ),
576             'quizaccess' => array(
577                 'delaybetweenattempts', 'ipaddress', 'numattempts', 'openclosedate',
578                 'password', 'safebrowser', 'securewindow', 'timelimit'
579             ),
581             'report' => array(
582                 'backups', 'completion', 'configlog', 'courseoverview',
583                 'log', 'loglive', 'outline', 'participation', 'progress', 'questioninstances', 'security', 'stats', 'performance'
584             ),
586             'repository' => array(
587                 'alfresco', 'boxnet', 'coursefiles', 'dropbox', 'equella', 'filesystem',
588                 'flickr', 'flickr_public', 'googledocs', 'local', 'merlot',
589                 'picasa', 'recent', 's3', 'upload', 'url', 'user', 'webdav',
590                 'wikimedia', 'youtube'
591             ),
593             'scormreport' => array(
594                 'basic',
595                 'interactions',
596                 'graphs'
597             ),
599             'tinymce' => array(
600                 'dragmath', 'moodleemoticon', 'moodleimage', 'moodlemedia', 'moodlenolink', 'spellchecker',
601             ),
603             'theme' => array(
604                 'afterburner', 'anomaly', 'arialist', 'base', 'binarius', 'bootstrap',
605                 'boxxie', 'brick', 'canvas', 'formal_white', 'formfactor',
606                 'fusion', 'leatherbound', 'magazine', 'mymobile', 'nimble',
607                 'nonzero', 'overlay', 'serenity', 'sky_high', 'splash',
608                 'standard', 'standardold'
609             ),
611             'tool' => array(
612                 'assignmentupgrade', 'behat', 'capability', 'customlang',
613                 'dbtransfer', 'generator', 'health', 'innodb', 'installaddon',
614                 'langimport', 'multilangupgrade', 'phpunit', 'profiling',
615                 'qeupgradehelper', 'replace', 'spamcleaner', 'timezoneimport',
616                 'unittest', 'uploaduser', 'unsuproles', 'xmldb'
617             ),
619             'webservice' => array(
620                 'amf', 'rest', 'soap', 'xmlrpc'
621             ),
623             'workshopallocation' => array(
624                 'manual', 'random', 'scheduled'
625             ),
627             'workshopeval' => array(
628                 'best'
629             ),
631             'workshopform' => array(
632                 'accumulative', 'comments', 'numerrors', 'rubric'
633             )
634         );
636         if (isset($standard_plugins[$type])) {
637             return $standard_plugins[$type];
638         } else {
639             return false;
640         }
641     }
643     /**
644      * Reorders plugin types into a sequence to be displayed
645      *
646      * For technical reasons, plugin types returned by {@link get_plugin_types()} are
647      * in a certain order that does not need to fit the expected order for the display.
648      * Particularly, activity modules should be displayed first as they represent the
649      * real heart of Moodle. They should be followed by other plugin types that are
650      * used to build the courses (as that is what one expects from LMS). After that,
651      * other supportive plugin types follow.
652      *
653      * @param array $types associative array
654      * @return array same array with altered order of items
655      */
656     protected function reorder_plugin_types(array $types) {
657         $fix = array(
658             'mod'        => $types['mod'],
659             'block'      => $types['block'],
660             'qtype'      => $types['qtype'],
661             'qbehaviour' => $types['qbehaviour'],
662             'qformat'    => $types['qformat'],
663             'filter'     => $types['filter'],
664             'enrol'      => $types['enrol'],
665         );
666         foreach ($types as $type => $path) {
667             if (!isset($fix[$type])) {
668                 $fix[$type] = $path;
669             }
670         }
671         return $fix;
672     }
676 /**
677  * General exception thrown by the {@link available_update_checker} class
678  */
679 class available_update_checker_exception extends moodle_exception {
681     /**
682      * @param string $errorcode exception description identifier
683      * @param mixed $debuginfo debugging data to display
684      */
685     public function __construct($errorcode, $debuginfo=null) {
686         parent::__construct($errorcode, 'core_plugin', '', null, print_r($debuginfo, true));
687     }
691 /**
692  * Singleton class that handles checking for available updates
693  */
694 class available_update_checker {
696     /** @var available_update_checker holds the singleton instance */
697     protected static $singletoninstance;
698     /** @var null|int the timestamp of when the most recent response was fetched */
699     protected $recentfetch = null;
700     /** @var null|array the recent response from the update notification provider */
701     protected $recentresponse = null;
702     /** @var null|string the numerical version of the local Moodle code */
703     protected $currentversion = null;
704     /** @var null|string the release info of the local Moodle code */
705     protected $currentrelease = null;
706     /** @var null|string branch of the local Moodle code */
707     protected $currentbranch = null;
708     /** @var array of (string)frankestyle => (string)version list of additional plugins deployed at this site */
709     protected $currentplugins = array();
711     /**
712      * Direct initiation not allowed, use the factory method {@link self::instance()}
713      */
714     protected function __construct() {
715     }
717     /**
718      * Sorry, this is singleton
719      */
720     protected function __clone() {
721     }
723     /**
724      * Factory method for this class
725      *
726      * @return available_update_checker the singleton instance
727      */
728     public static function instance() {
729         if (is_null(self::$singletoninstance)) {
730             self::$singletoninstance = new self();
731         }
732         return self::$singletoninstance;
733     }
735     /**
736      * Reset any caches
737      * @param bool $phpunitreset
738      */
739     public static function reset_caches($phpunitreset = false) {
740         if ($phpunitreset) {
741             self::$singletoninstance = null;
742         }
743     }
745     /**
746      * Returns the timestamp of the last execution of {@link fetch()}
747      *
748      * @return int|null null if it has never been executed or we don't known
749      */
750     public function get_last_timefetched() {
752         $this->restore_response();
754         if (!empty($this->recentfetch)) {
755             return $this->recentfetch;
757         } else {
758             return null;
759         }
760     }
762     /**
763      * Fetches the available update status from the remote site
764      *
765      * @throws available_update_checker_exception
766      */
767     public function fetch() {
768         $response = $this->get_response();
769         $this->validate_response($response);
770         $this->store_response($response);
771     }
773     /**
774      * Returns the available update information for the given component
775      *
776      * This method returns null if the most recent response does not contain any information
777      * about it. The returned structure is an array of available updates for the given
778      * component. Each update info is an object with at least one property called
779      * 'version'. Other possible properties are 'release', 'maturity', 'url' and 'downloadurl'.
780      *
781      * For the 'core' component, the method returns real updates only (those with higher version).
782      * For all other components, the list of all known remote updates is returned and the caller
783      * (usually the {@link plugin_manager}) is supposed to make the actual comparison of versions.
784      *
785      * @param string $component frankenstyle
786      * @param array $options with supported keys 'minmaturity' and/or 'notifybuilds'
787      * @return null|array null or array of available_update_info objects
788      */
789     public function get_update_info($component, array $options = array()) {
791         if (!isset($options['minmaturity'])) {
792             $options['minmaturity'] = 0;
793         }
795         if (!isset($options['notifybuilds'])) {
796             $options['notifybuilds'] = false;
797         }
799         if ($component == 'core') {
800             $this->load_current_environment();
801         }
803         $this->restore_response();
805         if (empty($this->recentresponse['updates'][$component])) {
806             return null;
807         }
809         $updates = array();
810         foreach ($this->recentresponse['updates'][$component] as $info) {
811             $update = new available_update_info($component, $info);
812             if (isset($update->maturity) and ($update->maturity < $options['minmaturity'])) {
813                 continue;
814             }
815             if ($component == 'core') {
816                 if ($update->version <= $this->currentversion) {
817                     continue;
818                 }
819                 if (empty($options['notifybuilds']) and $this->is_same_release($update->release)) {
820                     continue;
821                 }
822             }
823             $updates[] = $update;
824         }
826         if (empty($updates)) {
827             return null;
828         }
830         return $updates;
831     }
833     /**
834      * The method being run via cron.php
835      */
836     public function cron() {
837         global $CFG;
839         if (!$this->cron_autocheck_enabled()) {
840             $this->cron_mtrace('Automatic check for available updates not enabled, skipping.');
841             return;
842         }
844         $now = $this->cron_current_timestamp();
846         if ($this->cron_has_fresh_fetch($now)) {
847             $this->cron_mtrace('Recently fetched info about available updates is still fresh enough, skipping.');
848             return;
849         }
851         if ($this->cron_has_outdated_fetch($now)) {
852             $this->cron_mtrace('Outdated or missing info about available updates, forced fetching ... ', '');
853             $this->cron_execute();
854             return;
855         }
857         $offset = $this->cron_execution_offset();
858         $start = mktime(1, 0, 0, date('n', $now), date('j', $now), date('Y', $now)); // 01:00 AM today local time
859         if ($now > $start + $offset) {
860             $this->cron_mtrace('Regular daily check for available updates ... ', '');
861             $this->cron_execute();
862             return;
863         }
864     }
866     /// end of public API //////////////////////////////////////////////////////
868     /**
869      * Makes cURL request to get data from the remote site
870      *
871      * @return string raw request result
872      * @throws available_update_checker_exception
873      */
874     protected function get_response() {
875         global $CFG;
876         require_once($CFG->libdir.'/filelib.php');
878         $curl = new curl(array('proxy' => true));
879         $response = $curl->post($this->prepare_request_url(), $this->prepare_request_params(), $this->prepare_request_options());
880         $curlerrno = $curl->get_errno();
881         if (!empty($curlerrno)) {
882             throw new available_update_checker_exception('err_response_curl', 'cURL error '.$curlerrno.': '.$curl->error);
883         }
884         $curlinfo = $curl->get_info();
885         if ($curlinfo['http_code'] != 200) {
886             throw new available_update_checker_exception('err_response_http_code', $curlinfo['http_code']);
887         }
888         return $response;
889     }
891     /**
892      * Makes sure the response is valid, has correct API format etc.
893      *
894      * @param string $response raw response as returned by the {@link self::get_response()}
895      * @throws available_update_checker_exception
896      */
897     protected function validate_response($response) {
899         $response = $this->decode_response($response);
901         if (empty($response)) {
902             throw new available_update_checker_exception('err_response_empty');
903         }
905         if (empty($response['status']) or $response['status'] !== 'OK') {
906             throw new available_update_checker_exception('err_response_status', $response['status']);
907         }
909         if (empty($response['apiver']) or $response['apiver'] !== '1.2') {
910             throw new available_update_checker_exception('err_response_format_version', $response['apiver']);
911         }
913         if (empty($response['forbranch']) or $response['forbranch'] !== moodle_major_version(true)) {
914             throw new available_update_checker_exception('err_response_target_version', $response['forbranch']);
915         }
916     }
918     /**
919      * Decodes the raw string response from the update notifications provider
920      *
921      * @param string $response as returned by {@link self::get_response()}
922      * @return array decoded response structure
923      */
924     protected function decode_response($response) {
925         return json_decode($response, true);
926     }
928     /**
929      * Stores the valid fetched response for later usage
930      *
931      * This implementation uses the config_plugins table as the permanent storage.
932      *
933      * @param string $response raw valid data returned by {@link self::get_response()}
934      */
935     protected function store_response($response) {
937         set_config('recentfetch', time(), 'core_plugin');
938         set_config('recentresponse', $response, 'core_plugin');
940         $this->restore_response(true);
941     }
943     /**
944      * Loads the most recent raw response record we have fetched
945      *
946      * After this method is called, $this->recentresponse is set to an array. If the
947      * array is empty, then either no data have been fetched yet or the fetched data
948      * do not have expected format (and thence they are ignored and a debugging
949      * message is displayed).
950      *
951      * This implementation uses the config_plugins table as the permanent storage.
952      *
953      * @param bool $forcereload reload even if it was already loaded
954      */
955     protected function restore_response($forcereload = false) {
957         if (!$forcereload and !is_null($this->recentresponse)) {
958             // we already have it, nothing to do
959             return;
960         }
962         $config = get_config('core_plugin');
964         if (!empty($config->recentresponse) and !empty($config->recentfetch)) {
965             try {
966                 $this->validate_response($config->recentresponse);
967                 $this->recentfetch = $config->recentfetch;
968                 $this->recentresponse = $this->decode_response($config->recentresponse);
969             } catch (available_update_checker_exception $e) {
970                 // The server response is not valid. Behave as if no data were fetched yet.
971                 // This may happen when the most recent update info (cached locally) has been
972                 // fetched with the previous branch of Moodle (like during an upgrade from 2.x
973                 // to 2.y) or when the API of the response has changed.
974                 $this->recentresponse = array();
975             }
977         } else {
978             $this->recentresponse = array();
979         }
980     }
982     /**
983      * Compares two raw {@link $recentresponse} records and returns the list of changed updates
984      *
985      * This method is used to populate potential update info to be sent to site admins.
986      *
987      * @param array $old
988      * @param array $new
989      * @throws available_update_checker_exception
990      * @return array parts of $new['updates'] that have changed
991      */
992     protected function compare_responses(array $old, array $new) {
994         if (empty($new)) {
995             return array();
996         }
998         if (!array_key_exists('updates', $new)) {
999             throw new available_update_checker_exception('err_response_format');
1000         }
1002         if (empty($old)) {
1003             return $new['updates'];
1004         }
1006         if (!array_key_exists('updates', $old)) {
1007             throw new available_update_checker_exception('err_response_format');
1008         }
1010         $changes = array();
1012         foreach ($new['updates'] as $newcomponent => $newcomponentupdates) {
1013             if (empty($old['updates'][$newcomponent])) {
1014                 $changes[$newcomponent] = $newcomponentupdates;
1015                 continue;
1016             }
1017             foreach ($newcomponentupdates as $newcomponentupdate) {
1018                 $inold = false;
1019                 foreach ($old['updates'][$newcomponent] as $oldcomponentupdate) {
1020                     if ($newcomponentupdate['version'] == $oldcomponentupdate['version']) {
1021                         $inold = true;
1022                     }
1023                 }
1024                 if (!$inold) {
1025                     if (!isset($changes[$newcomponent])) {
1026                         $changes[$newcomponent] = array();
1027                     }
1028                     $changes[$newcomponent][] = $newcomponentupdate;
1029                 }
1030             }
1031         }
1033         return $changes;
1034     }
1036     /**
1037      * Returns the URL to send update requests to
1038      *
1039      * During the development or testing, you can set $CFG->alternativeupdateproviderurl
1040      * to a custom URL that will be used. Otherwise the standard URL will be returned.
1041      *
1042      * @return string URL
1043      */
1044     protected function prepare_request_url() {
1045         global $CFG;
1047         if (!empty($CFG->config_php_settings['alternativeupdateproviderurl'])) {
1048             return $CFG->config_php_settings['alternativeupdateproviderurl'];
1049         } else {
1050             return 'https://download.moodle.org/api/1.2/updates.php';
1051         }
1052     }
1054     /**
1055      * Sets the properties currentversion, currentrelease, currentbranch and currentplugins
1056      *
1057      * @param bool $forcereload
1058      */
1059     protected function load_current_environment($forcereload=false) {
1060         global $CFG;
1062         if (!is_null($this->currentversion) and !$forcereload) {
1063             // nothing to do
1064             return;
1065         }
1067         $version = null;
1068         $release = null;
1070         require($CFG->dirroot.'/version.php');
1071         $this->currentversion = $version;
1072         $this->currentrelease = $release;
1073         $this->currentbranch = moodle_major_version(true);
1075         $pluginman = plugin_manager::instance();
1076         foreach ($pluginman->get_plugins() as $type => $plugins) {
1077             foreach ($plugins as $plugin) {
1078                 if (!$plugin->is_standard()) {
1079                     $this->currentplugins[$plugin->component] = $plugin->versiondisk;
1080                 }
1081             }
1082         }
1083     }
1085     /**
1086      * Returns the list of HTTP params to be sent to the updates provider URL
1087      *
1088      * @return array of (string)param => (string)value
1089      */
1090     protected function prepare_request_params() {
1091         global $CFG;
1093         $this->load_current_environment();
1094         $this->restore_response();
1096         $params = array();
1097         $params['format'] = 'json';
1099         if (isset($this->recentresponse['ticket'])) {
1100             $params['ticket'] = $this->recentresponse['ticket'];
1101         }
1103         if (isset($this->currentversion)) {
1104             $params['version'] = $this->currentversion;
1105         } else {
1106             throw new coding_exception('Main Moodle version must be already known here');
1107         }
1109         if (isset($this->currentbranch)) {
1110             $params['branch'] = $this->currentbranch;
1111         } else {
1112             throw new coding_exception('Moodle release must be already known here');
1113         }
1115         $plugins = array();
1116         foreach ($this->currentplugins as $plugin => $version) {
1117             $plugins[] = $plugin.'@'.$version;
1118         }
1119         if (!empty($plugins)) {
1120             $params['plugins'] = implode(',', $plugins);
1121         }
1123         return $params;
1124     }
1126     /**
1127      * Returns the list of cURL options to use when fetching available updates data
1128      *
1129      * @return array of (string)param => (string)value
1130      */
1131     protected function prepare_request_options() {
1132         global $CFG;
1134         $options = array(
1135             'CURLOPT_SSL_VERIFYHOST' => 2,      // this is the default in {@link curl} class but just in case
1136             'CURLOPT_SSL_VERIFYPEER' => true,
1137         );
1139         $cacertfile = $CFG->dataroot.'/moodleorgca.crt';
1140         if (is_readable($cacertfile)) {
1141             // Do not use CA certs provided by the operating system. Instead,
1142             // use this CA cert to verify the updates provider.
1143             $options['CURLOPT_CAINFO'] = $cacertfile;
1144         }
1146         return $options;
1147     }
1149     /**
1150      * Returns the current timestamp
1151      *
1152      * @return int the timestamp
1153      */
1154     protected function cron_current_timestamp() {
1155         return time();
1156     }
1158     /**
1159      * Output cron debugging info
1160      *
1161      * @see mtrace()
1162      * @param string $msg output message
1163      * @param string $eol end of line
1164      */
1165     protected function cron_mtrace($msg, $eol = PHP_EOL) {
1166         mtrace($msg, $eol);
1167     }
1169     /**
1170      * Decide if the autocheck feature is disabled in the server setting
1171      *
1172      * @return bool true if autocheck enabled, false if disabled
1173      */
1174     protected function cron_autocheck_enabled() {
1175         global $CFG;
1177         if (empty($CFG->updateautocheck)) {
1178             return false;
1179         } else {
1180             return true;
1181         }
1182     }
1184     /**
1185      * Decide if the recently fetched data are still fresh enough
1186      *
1187      * @param int $now current timestamp
1188      * @return bool true if no need to re-fetch, false otherwise
1189      */
1190     protected function cron_has_fresh_fetch($now) {
1191         $recent = $this->get_last_timefetched();
1193         if (empty($recent)) {
1194             return false;
1195         }
1197         if ($now < $recent) {
1198             $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1199             return true;
1200         }
1202         if ($now - $recent > 24 * HOURSECS) {
1203             return false;
1204         }
1206         return true;
1207     }
1209     /**
1210      * Decide if the fetch is outadated or even missing
1211      *
1212      * @param int $now current timestamp
1213      * @return bool false if no need to re-fetch, true otherwise
1214      */
1215     protected function cron_has_outdated_fetch($now) {
1216         $recent = $this->get_last_timefetched();
1218         if (empty($recent)) {
1219             return true;
1220         }
1222         if ($now < $recent) {
1223             $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1224             return false;
1225         }
1227         if ($now - $recent > 48 * HOURSECS) {
1228             return true;
1229         }
1231         return false;
1232     }
1234     /**
1235      * Returns the cron execution offset for this site
1236      *
1237      * The main {@link self::cron()} is supposed to run every night in some random time
1238      * between 01:00 and 06:00 AM (local time). The exact moment is defined by so called
1239      * execution offset, that is the amount of time after 01:00 AM. The offset value is
1240      * initially generated randomly and then used consistently at the site. This way, the
1241      * regular checks against the download.moodle.org server are spread in time.
1242      *
1243      * @return int the offset number of seconds from range 1 sec to 5 hours
1244      */
1245     protected function cron_execution_offset() {
1246         global $CFG;
1248         if (empty($CFG->updatecronoffset)) {
1249             set_config('updatecronoffset', rand(1, 5 * HOURSECS));
1250         }
1252         return $CFG->updatecronoffset;
1253     }
1255     /**
1256      * Fetch available updates info and eventually send notification to site admins
1257      */
1258     protected function cron_execute() {
1260         try {
1261             $this->restore_response();
1262             $previous = $this->recentresponse;
1263             $this->fetch();
1264             $this->restore_response(true);
1265             $current = $this->recentresponse;
1266             $changes = $this->compare_responses($previous, $current);
1267             $notifications = $this->cron_notifications($changes);
1268             $this->cron_notify($notifications);
1269             $this->cron_mtrace('done');
1270         } catch (available_update_checker_exception $e) {
1271             $this->cron_mtrace('FAILED!');
1272         }
1273     }
1275     /**
1276      * Given the list of changes in available updates, pick those to send to site admins
1277      *
1278      * @param array $changes as returned by {@link self::compare_responses()}
1279      * @return array of available_update_info objects to send to site admins
1280      */
1281     protected function cron_notifications(array $changes) {
1282         global $CFG;
1284         $notifications = array();
1285         $pluginman = plugin_manager::instance();
1286         $plugins = $pluginman->get_plugins(true);
1288         foreach ($changes as $component => $componentchanges) {
1289             if (empty($componentchanges)) {
1290                 continue;
1291             }
1292             $componentupdates = $this->get_update_info($component,
1293                 array('minmaturity' => $CFG->updateminmaturity, 'notifybuilds' => $CFG->updatenotifybuilds));
1294             if (empty($componentupdates)) {
1295                 continue;
1296             }
1297             // notify only about those $componentchanges that are present in $componentupdates
1298             // to respect the preferences
1299             foreach ($componentchanges as $componentchange) {
1300                 foreach ($componentupdates as $componentupdate) {
1301                     if ($componentupdate->version == $componentchange['version']) {
1302                         if ($component == 'core') {
1303                             // In case of 'core', we already know that the $componentupdate
1304                             // is a real update with higher version ({@see self::get_update_info()}).
1305                             // We just perform additional check for the release property as there
1306                             // can be two Moodle releases having the same version (e.g. 2.4.0 and 2.5dev shortly
1307                             // after the release). We can do that because we have the release info
1308                             // always available for the core.
1309                             if ((string)$componentupdate->release === (string)$componentchange['release']) {
1310                                 $notifications[] = $componentupdate;
1311                             }
1312                         } else {
1313                             // Use the plugin_manager to check if the detected $componentchange
1314                             // is a real update with higher version. That is, the $componentchange
1315                             // is present in the array of {@link available_update_info} objects
1316                             // returned by the plugin's available_updates() method.
1317                             list($plugintype, $pluginname) = normalize_component($component);
1318                             if (!empty($plugins[$plugintype][$pluginname])) {
1319                                 $availableupdates = $plugins[$plugintype][$pluginname]->available_updates();
1320                                 if (!empty($availableupdates)) {
1321                                     foreach ($availableupdates as $availableupdate) {
1322                                         if ($availableupdate->version == $componentchange['version']) {
1323                                             $notifications[] = $componentupdate;
1324                                         }
1325                                     }
1326                                 }
1327                             }
1328                         }
1329                     }
1330                 }
1331             }
1332         }
1334         return $notifications;
1335     }
1337     /**
1338      * Sends the given notifications to site admins via messaging API
1339      *
1340      * @param array $notifications array of available_update_info objects to send
1341      */
1342     protected function cron_notify(array $notifications) {
1343         global $CFG;
1345         if (empty($notifications)) {
1346             return;
1347         }
1349         $admins = get_admins();
1351         if (empty($admins)) {
1352             return;
1353         }
1355         $this->cron_mtrace('sending notifications ... ', '');
1357         $text = get_string('updatenotifications', 'core_admin') . PHP_EOL;
1358         $html = html_writer::tag('h1', get_string('updatenotifications', 'core_admin')) . PHP_EOL;
1360         $coreupdates = array();
1361         $pluginupdates = array();
1363         foreach ($notifications as $notification) {
1364             if ($notification->component == 'core') {
1365                 $coreupdates[] = $notification;
1366             } else {
1367                 $pluginupdates[] = $notification;
1368             }
1369         }
1371         if (!empty($coreupdates)) {
1372             $text .= PHP_EOL . get_string('updateavailable', 'core_admin') . PHP_EOL;
1373             $html .= html_writer::tag('h2', get_string('updateavailable', 'core_admin')) . PHP_EOL;
1374             $html .= html_writer::start_tag('ul') . PHP_EOL;
1375             foreach ($coreupdates as $coreupdate) {
1376                 $html .= html_writer::start_tag('li');
1377                 if (isset($coreupdate->release)) {
1378                     $text .= get_string('updateavailable_release', 'core_admin', $coreupdate->release);
1379                     $html .= html_writer::tag('strong', get_string('updateavailable_release', 'core_admin', $coreupdate->release));
1380                 }
1381                 if (isset($coreupdate->version)) {
1382                     $text .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
1383                     $html .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
1384                 }
1385                 if (isset($coreupdate->maturity)) {
1386                     $text .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
1387                     $html .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
1388                 }
1389                 $text .= PHP_EOL;
1390                 $html .= html_writer::end_tag('li') . PHP_EOL;
1391             }
1392             $text .= PHP_EOL;
1393             $html .= html_writer::end_tag('ul') . PHP_EOL;
1395             $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/index.php');
1396             $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
1397             $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/index.php', $CFG->wwwroot.'/'.$CFG->admin.'/index.php'));
1398             $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
1399         }
1401         if (!empty($pluginupdates)) {
1402             $text .= PHP_EOL . get_string('updateavailableforplugin', 'core_admin') . PHP_EOL;
1403             $html .= html_writer::tag('h2', get_string('updateavailableforplugin', 'core_admin')) . PHP_EOL;
1405             $html .= html_writer::start_tag('ul') . PHP_EOL;
1406             foreach ($pluginupdates as $pluginupdate) {
1407                 $html .= html_writer::start_tag('li');
1408                 $text .= get_string('pluginname', $pluginupdate->component);
1409                 $html .= html_writer::tag('strong', get_string('pluginname', $pluginupdate->component));
1411                 $text .= ' ('.$pluginupdate->component.')';
1412                 $html .= ' ('.$pluginupdate->component.')';
1414                 $text .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
1415                 $html .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
1417                 $text .= PHP_EOL;
1418                 $html .= html_writer::end_tag('li') . PHP_EOL;
1419             }
1420             $text .= PHP_EOL;
1421             $html .= html_writer::end_tag('ul') . PHP_EOL;
1423             $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php');
1424             $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
1425             $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/plugins.php', $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php'));
1426             $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
1427         }
1429         $a = array('siteurl' => $CFG->wwwroot);
1430         $text .= get_string('updatenotificationfooter', 'core_admin', $a) . PHP_EOL;
1431         $a = array('siteurl' => html_writer::link($CFG->wwwroot, $CFG->wwwroot));
1432         $html .= html_writer::tag('footer', html_writer::tag('p', get_string('updatenotificationfooter', 'core_admin', $a),
1433             array('style' => 'font-size:smaller; color:#333;')));
1435         foreach ($admins as $admin) {
1436             $message = new stdClass();
1437             $message->component         = 'moodle';
1438             $message->name              = 'availableupdate';
1439             $message->userfrom          = get_admin();
1440             $message->userto            = $admin;
1441             $message->subject           = get_string('updatenotificationsubject', 'core_admin', array('siteurl' => $CFG->wwwroot));
1442             $message->fullmessage       = $text;
1443             $message->fullmessageformat = FORMAT_PLAIN;
1444             $message->fullmessagehtml   = $html;
1445             $message->smallmessage      = get_string('updatenotifications', 'core_admin');
1446             $message->notification      = 1;
1447             message_send($message);
1448         }
1449     }
1451     /**
1452      * Compare two release labels and decide if they are the same
1453      *
1454      * @param string $remote release info of the available update
1455      * @param null|string $local release info of the local code, defaults to $release defined in version.php
1456      * @return boolean true if the releases declare the same minor+major version
1457      */
1458     protected function is_same_release($remote, $local=null) {
1460         if (is_null($local)) {
1461             $this->load_current_environment();
1462             $local = $this->currentrelease;
1463         }
1465         $pattern = '/^([0-9\.\+]+)([^(]*)/';
1467         preg_match($pattern, $remote, $remotematches);
1468         preg_match($pattern, $local, $localmatches);
1470         $remotematches[1] = str_replace('+', '', $remotematches[1]);
1471         $localmatches[1] = str_replace('+', '', $localmatches[1]);
1473         if ($remotematches[1] === $localmatches[1] and rtrim($remotematches[2]) === rtrim($localmatches[2])) {
1474             return true;
1475         } else {
1476             return false;
1477         }
1478     }
1482 /**
1483  * Defines the structure of objects returned by {@link available_update_checker::get_update_info()}
1484  */
1485 class available_update_info {
1487     /** @var string frankenstyle component name */
1488     public $component;
1489     /** @var int the available version of the component */
1490     public $version;
1491     /** @var string|null optional release name */
1492     public $release = null;
1493     /** @var int|null optional maturity info, eg {@link MATURITY_STABLE} */
1494     public $maturity = null;
1495     /** @var string|null optional URL of a page with more info about the update */
1496     public $url = null;
1497     /** @var string|null optional URL of a ZIP package that can be downloaded and installed */
1498     public $download = null;
1499     /** @var string|null of self::download is set, then this must be the MD5 hash of the ZIP */
1500     public $downloadmd5 = null;
1502     /**
1503      * Creates new instance of the class
1504      *
1505      * The $info array must provide at least the 'version' value and optionally all other
1506      * values to populate the object's properties.
1507      *
1508      * @param string $name the frankenstyle component name
1509      * @param array $info associative array with other properties
1510      */
1511     public function __construct($name, array $info) {
1512         $this->component = $name;
1513         foreach ($info as $k => $v) {
1514             if (property_exists('available_update_info', $k) and $k != 'component') {
1515                 $this->$k = $v;
1516             }
1517         }
1518     }
1522 /**
1523  * Implements a communication bridge to the mdeploy.php utility
1524  */
1525 class available_update_deployer {
1527     const HTTP_PARAM_PREFIX     = 'updteautodpldata_';  // Hey, even Google has not heard of such a prefix! So it MUST be safe :-p
1528     const HTTP_PARAM_CHECKER    = 'datapackagesize';    // Name of the parameter that holds the number of items in the received data items
1530     /** @var available_update_deployer holds the singleton instance */
1531     protected static $singletoninstance;
1532     /** @var moodle_url URL of a page that includes the deployer UI */
1533     protected $callerurl;
1534     /** @var moodle_url URL to return after the deployment */
1535     protected $returnurl;
1537     /**
1538      * Direct instantiation not allowed, use the factory method {@link self::instance()}
1539      */
1540     protected function __construct() {
1541     }
1543     /**
1544      * Sorry, this is singleton
1545      */
1546     protected function __clone() {
1547     }
1549     /**
1550      * Factory method for this class
1551      *
1552      * @return available_update_deployer the singleton instance
1553      */
1554     public static function instance() {
1555         if (is_null(self::$singletoninstance)) {
1556             self::$singletoninstance = new self();
1557         }
1558         return self::$singletoninstance;
1559     }
1561     /**
1562      * Reset caches used by this script
1563      *
1564      * @param bool $phpunitreset is this called as a part of PHPUnit reset?
1565      */
1566     public static function reset_caches($phpunitreset = false) {
1567         if ($phpunitreset) {
1568             self::$singletoninstance = null;
1569         }
1570     }
1572     /**
1573      * Is automatic deployment enabled?
1574      *
1575      * @return bool
1576      */
1577     public function enabled() {
1578         global $CFG;
1580         if (!empty($CFG->disableupdateautodeploy)) {
1581             // The feature is prohibited via config.php
1582             return false;
1583         }
1585         return get_config('updateautodeploy');
1586     }
1588     /**
1589      * Sets some base properties of the class to make it usable.
1590      *
1591      * @param moodle_url $callerurl the base URL of a script that will handle the class'es form data
1592      * @param moodle_url $returnurl the final URL to return to when the deployment is finished
1593      */
1594     public function initialize(moodle_url $callerurl, moodle_url $returnurl) {
1596         if (!$this->enabled()) {
1597             throw new coding_exception('Unable to initialize the deployer, the feature is not enabled.');
1598         }
1600         $this->callerurl = $callerurl;
1601         $this->returnurl = $returnurl;
1602     }
1604     /**
1605      * Has the deployer been initialized?
1606      *
1607      * Initialized deployer means that the following properties were set:
1608      * callerurl, returnurl
1609      *
1610      * @return bool
1611      */
1612     public function initialized() {
1614         if (!$this->enabled()) {
1615             return false;
1616         }
1618         if (empty($this->callerurl)) {
1619             return false;
1620         }
1622         if (empty($this->returnurl)) {
1623             return false;
1624         }
1626         return true;
1627     }
1629     /**
1630      * Returns a list of reasons why the deployment can not happen
1631      *
1632      * If the returned array is empty, the deployment seems to be possible. The returned
1633      * structure is an associative array with keys representing individual impediments.
1634      * Possible keys are: missingdownloadurl, missingdownloadmd5, notwritable.
1635      *
1636      * @param available_update_info $info
1637      * @return array
1638      */
1639     public function deployment_impediments(available_update_info $info) {
1641         $impediments = array();
1643         if (empty($info->download)) {
1644             $impediments['missingdownloadurl'] = true;
1645         }
1647         if (empty($info->downloadmd5)) {
1648             $impediments['missingdownloadmd5'] = true;
1649         }
1651         if (!empty($info->download) and !$this->update_downloadable($info->download)) {
1652             $impediments['notdownloadable'] = true;
1653         }
1655         if (!$this->component_writable($info->component)) {
1656             $impediments['notwritable'] = true;
1657         }
1659         return $impediments;
1660     }
1662     /**
1663      * Check to see if the current version of the plugin seems to be a checkout of an external repository.
1664      *
1665      * @param available_update_info $info
1666      * @return false|string
1667      */
1668     public function plugin_external_source(available_update_info $info) {
1670         $paths = get_plugin_types(true);
1671         list($plugintype, $pluginname) = normalize_component($info->component);
1672         $pluginroot = $paths[$plugintype].'/'.$pluginname;
1674         if (is_dir($pluginroot.'/.git')) {
1675             return 'git';
1676         }
1678         if (is_dir($pluginroot.'/CVS')) {
1679             return 'cvs';
1680         }
1682         if (is_dir($pluginroot.'/.svn')) {
1683             return 'svn';
1684         }
1686         return false;
1687     }
1689     /**
1690      * Prepares a renderable widget to confirm installation of an available update.
1691      *
1692      * @param available_update_info $info component version to deploy
1693      * @return renderable
1694      */
1695     public function make_confirm_widget(available_update_info $info) {
1697         if (!$this->initialized()) {
1698             throw new coding_exception('Illegal method call - deployer not initialized.');
1699         }
1701         $params = $this->data_to_params(array(
1702             'updateinfo' => (array)$info,   // see http://www.php.net/manual/en/language.types.array.php#language.types.array.casting
1703         ));
1705         $widget = new single_button(
1706             new moodle_url($this->callerurl, $params),
1707             get_string('updateavailableinstall', 'core_admin'),
1708             'post'
1709         );
1711         return $widget;
1712     }
1714     /**
1715      * Prepares a renderable widget to execute installation of an available update.
1716      *
1717      * @param available_update_info $info component version to deploy
1718      * @return renderable
1719      */
1720     public function make_execution_widget(available_update_info $info) {
1721         global $CFG;
1723         if (!$this->initialized()) {
1724             throw new coding_exception('Illegal method call - deployer not initialized.');
1725         }
1727         $pluginrootpaths = get_plugin_types(true);
1729         list($plugintype, $pluginname) = normalize_component($info->component);
1731         if (empty($pluginrootpaths[$plugintype])) {
1732             throw new coding_exception('Unknown plugin type root location', $plugintype);
1733         }
1735         list($passfile, $password) = $this->prepare_authorization();
1737         $upgradeurl = new moodle_url('/admin');
1739         $params = array(
1740             'upgrade' => true,
1741             'type' => $plugintype,
1742             'name' => $pluginname,
1743             'typeroot' => $pluginrootpaths[$plugintype],
1744             'package' => $info->download,
1745             'md5' => $info->downloadmd5,
1746             'dataroot' => $CFG->dataroot,
1747             'dirroot' => $CFG->dirroot,
1748             'passfile' => $passfile,
1749             'password' => $password,
1750             'returnurl' => $upgradeurl->out(true),
1751         );
1753         if (!empty($CFG->proxyhost)) {
1754             // MDL-36973 - Beware - we should call just !is_proxybypass() here. But currently, our
1755             // cURL wrapper class does not do it. So, to have consistent behaviour, we pass proxy
1756             // setting regardless the $CFG->proxybypass setting. Once the {@link curl} class is
1757             // fixed, the condition should be amended.
1758             if (true or !is_proxybypass($info->download)) {
1759                 if (empty($CFG->proxyport)) {
1760                     $params['proxy'] = $CFG->proxyhost;
1761                 } else {
1762                     $params['proxy'] = $CFG->proxyhost.':'.$CFG->proxyport;
1763                 }
1765                 if (!empty($CFG->proxyuser) and !empty($CFG->proxypassword)) {
1766                     $params['proxyuserpwd'] = $CFG->proxyuser.':'.$CFG->proxypassword;
1767                 }
1769                 if (!empty($CFG->proxytype)) {
1770                     $params['proxytype'] = $CFG->proxytype;
1771                 }
1772             }
1773         }
1775         $widget = new single_button(
1776             new moodle_url('/mdeploy.php', $params),
1777             get_string('updateavailableinstall', 'core_admin'),
1778             'post'
1779         );
1781         return $widget;
1782     }
1784     /**
1785      * Returns array of data objects passed to this tool.
1786      *
1787      * @return array
1788      */
1789     public function submitted_data() {
1791         $data = $this->params_to_data($_POST);
1793         if (empty($data) or empty($data[self::HTTP_PARAM_CHECKER])) {
1794             return false;
1795         }
1797         if (!empty($data['updateinfo']) and is_object($data['updateinfo'])) {
1798             $updateinfo = $data['updateinfo'];
1799             if (!empty($updateinfo->component) and !empty($updateinfo->version)) {
1800                 $data['updateinfo'] = new available_update_info($updateinfo->component, (array)$updateinfo);
1801             }
1802         }
1804         if (!empty($data['callerurl'])) {
1805             $data['callerurl'] = new moodle_url($data['callerurl']);
1806         }
1808         if (!empty($data['returnurl'])) {
1809             $data['returnurl'] = new moodle_url($data['returnurl']);
1810         }
1812         return $data;
1813     }
1815     /**
1816      * Handles magic getters and setters for protected properties.
1817      *
1818      * @param string $name method name, e.g. set_returnurl()
1819      * @param array $arguments arguments to be passed to the array
1820      */
1821     public function __call($name, array $arguments = array()) {
1823         if (substr($name, 0, 4) === 'set_') {
1824             $property = substr($name, 4);
1825             if (empty($property)) {
1826                 throw new coding_exception('Invalid property name (empty)');
1827             }
1828             if (empty($arguments)) {
1829                 $arguments = array(true); // Default value for flag-like properties.
1830             }
1831             // Make sure it is a protected property.
1832             $isprotected = false;
1833             $reflection = new ReflectionObject($this);
1834             foreach ($reflection->getProperties(ReflectionProperty::IS_PROTECTED) as $reflectionproperty) {
1835                 if ($reflectionproperty->getName() === $property) {
1836                     $isprotected = true;
1837                     break;
1838                 }
1839             }
1840             if (!$isprotected) {
1841                 throw new coding_exception('Unable to set property - it does not exist or it is not protected');
1842             }
1843             $value = reset($arguments);
1844             $this->$property = $value;
1845             return;
1846         }
1848         if (substr($name, 0, 4) === 'get_') {
1849             $property = substr($name, 4);
1850             if (empty($property)) {
1851                 throw new coding_exception('Invalid property name (empty)');
1852             }
1853             if (!empty($arguments)) {
1854                 throw new coding_exception('No parameter expected');
1855             }
1856             // Make sure it is a protected property.
1857             $isprotected = false;
1858             $reflection = new ReflectionObject($this);
1859             foreach ($reflection->getProperties(ReflectionProperty::IS_PROTECTED) as $reflectionproperty) {
1860                 if ($reflectionproperty->getName() === $property) {
1861                     $isprotected = true;
1862                     break;
1863                 }
1864             }
1865             if (!$isprotected) {
1866                 throw new coding_exception('Unable to get property - it does not exist or it is not protected');
1867             }
1868             return $this->$property;
1869         }
1870     }
1872     /**
1873      * Generates a random token and stores it in a file in moodledata directory.
1874      *
1875      * @return array of the (string)filename and (string)password in this order
1876      */
1877     public function prepare_authorization() {
1878         global $CFG;
1880         make_upload_directory('mdeploy/auth/');
1882         $attempts = 0;
1883         $success = false;
1885         while (!$success and $attempts < 5) {
1886             $attempts++;
1888             $passfile = $this->generate_passfile();
1889             $password = $this->generate_password();
1890             $now = time();
1892             $filepath = $CFG->dataroot.'/mdeploy/auth/'.$passfile;
1894             if (!file_exists($filepath)) {
1895                 $success = file_put_contents($filepath, $password . PHP_EOL . $now . PHP_EOL, LOCK_EX);
1896             }
1897         }
1899         if ($success) {
1900             return array($passfile, $password);
1902         } else {
1903             throw new moodle_exception('unable_prepare_authorization', 'core_plugin');
1904         }
1905     }
1907     // End of external API
1909     /**
1910      * Prepares an array of HTTP parameters that can be passed to another page.
1911      *
1912      * @param array|object $data associative array or an object holding the data, data JSON-able
1913      * @return array suitable as a param for moodle_url
1914      */
1915     protected function data_to_params($data) {
1917         // Append some our own data
1918         if (!empty($this->callerurl)) {
1919             $data['callerurl'] = $this->callerurl->out(false);
1920         }
1921         if (!empty($this->callerurl)) {
1922             $data['returnurl'] = $this->returnurl->out(false);
1923         }
1925         // Finally append the count of items in the package.
1926         $data[self::HTTP_PARAM_CHECKER] = count($data);
1928         // Generate params
1929         $params = array();
1930         foreach ($data as $name => $value) {
1931             $transname = self::HTTP_PARAM_PREFIX.$name;
1932             $transvalue = json_encode($value);
1933             $params[$transname] = $transvalue;
1934         }
1936         return $params;
1937     }
1939     /**
1940      * Converts HTTP parameters passed to the script into native PHP data
1941      *
1942      * @param array $params such as $_REQUEST or $_POST
1943      * @return array data passed for this class
1944      */
1945     protected function params_to_data(array $params) {
1947         if (empty($params)) {
1948             return array();
1949         }
1951         $data = array();
1952         foreach ($params as $name => $value) {
1953             if (strpos($name, self::HTTP_PARAM_PREFIX) === 0) {
1954                 $realname = substr($name, strlen(self::HTTP_PARAM_PREFIX));
1955                 $realvalue = json_decode($value);
1956                 $data[$realname] = $realvalue;
1957             }
1958         }
1960         return $data;
1961     }
1963     /**
1964      * Returns a random string to be used as a filename of the password storage.
1965      *
1966      * @return string
1967      */
1968     protected function generate_passfile() {
1969         return clean_param(uniqid('mdeploy_', true), PARAM_FILE);
1970     }
1972     /**
1973      * Returns a random string to be used as the authorization token
1974      *
1975      * @return string
1976      */
1977     protected function generate_password() {
1978         return complex_random_string();
1979     }
1981     /**
1982      * Checks if the given component's directory is writable
1983      *
1984      * For the purpose of the deployment, the web server process has to have
1985      * write access to all files in the component's directory (recursively) and for the
1986      * directory itself.
1987      *
1988      * @see worker::move_directory_source_precheck()
1989      * @param string $component normalized component name
1990      * @return boolean
1991      */
1992     protected function component_writable($component) {
1994         list($plugintype, $pluginname) = normalize_component($component);
1996         $directory = get_plugin_directory($plugintype, $pluginname);
1998         if (is_null($directory)) {
1999             throw new coding_exception('Unknown component location', $component);
2000         }
2002         return $this->directory_writable($directory);
2003     }
2005     /**
2006      * Checks if the mdeploy.php will be able to fetch the ZIP from the given URL
2007      *
2008      * This is mainly supposed to check if the transmission over HTTPS would
2009      * work. That is, if the CA certificates are present at the server.
2010      *
2011      * @param string $downloadurl the URL of the ZIP package to download
2012      * @return bool
2013      */
2014     protected function update_downloadable($downloadurl) {
2015         global $CFG;
2017         $curloptions = array(
2018             'CURLOPT_SSL_VERIFYHOST' => 2,      // this is the default in {@link curl} class but just in case
2019             'CURLOPT_SSL_VERIFYPEER' => true,
2020         );
2022         $cacertfile = $CFG->dataroot.'/moodleorgca.crt';
2023         if (is_readable($cacertfile)) {
2024             // Do not use CA certs provided by the operating system. Instead,
2025             // use this CA cert to verify the updates provider.
2026             $curloptions['CURLOPT_CAINFO'] = $cacertfile;
2027         }
2029         $curl = new curl(array('proxy' => true));
2030         $result = $curl->head($downloadurl, $curloptions);
2031         $errno = $curl->get_errno();
2032         if (empty($errno)) {
2033             return true;
2034         } else {
2035             return false;
2036         }
2037     }
2039     /**
2040      * Checks if the directory and all its contents (recursively) is writable
2041      *
2042      * @param string $path full path to a directory
2043      * @return boolean
2044      */
2045     private function directory_writable($path) {
2047         if (!is_writable($path)) {
2048             return false;
2049         }
2051         if (is_dir($path)) {
2052             $handle = opendir($path);
2053         } else {
2054             return false;
2055         }
2057         $result = true;
2059         while ($filename = readdir($handle)) {
2060             $filepath = $path.'/'.$filename;
2062             if ($filename === '.' or $filename === '..') {
2063                 continue;
2064             }
2066             if (is_dir($filepath)) {
2067                 $result = $result && $this->directory_writable($filepath);
2069             } else {
2070                 $result = $result && is_writable($filepath);
2071             }
2072         }
2074         closedir($handle);
2076         return $result;
2077     }
2081 /**
2082  * Factory class producing required subclasses of {@link plugininfo_base}
2083  */
2084 class plugininfo_default_factory {
2086     /**
2087      * Makes a new instance of the plugininfo class
2088      *
2089      * @param string $type the plugin type, eg. 'mod'
2090      * @param string $typerootdir full path to the location of all the plugins of this type
2091      * @param string $name the plugin name, eg. 'workshop'
2092      * @param string $namerootdir full path to the location of the plugin
2093      * @param string $typeclass the name of class that holds the info about the plugin
2094      * @return plugininfo_base the instance of $typeclass
2095      */
2096     public static function make($type, $typerootdir, $name, $namerootdir, $typeclass) {
2097         $plugin              = new $typeclass();
2098         $plugin->type        = $type;
2099         $plugin->typerootdir = $typerootdir;
2100         $plugin->name        = $name;
2101         $plugin->rootdir     = $namerootdir;
2103         $plugin->init_display_name();
2104         $plugin->load_disk_version();
2105         $plugin->load_db_version();
2106         $plugin->load_required_main_version();
2107         $plugin->init_is_standard();
2109         return $plugin;
2110     }
2114 /**
2115  * Base class providing access to the information about a plugin
2116  *
2117  * @property-read string component the component name, type_name
2118  */
2119 abstract class plugininfo_base {
2121     /** @var string the plugintype name, eg. mod, auth or workshopform */
2122     public $type;
2123     /** @var string full path to the location of all the plugins of this type */
2124     public $typerootdir;
2125     /** @var string the plugin name, eg. assignment, ldap */
2126     public $name;
2127     /** @var string the localized plugin name */
2128     public $displayname;
2129     /** @var string the plugin source, one of plugin_manager::PLUGIN_SOURCE_xxx constants */
2130     public $source;
2131     /** @var fullpath to the location of this plugin */
2132     public $rootdir;
2133     /** @var int|string the version of the plugin's source code */
2134     public $versiondisk;
2135     /** @var int|string the version of the installed plugin */
2136     public $versiondb;
2137     /** @var int|float|string required version of Moodle core  */
2138     public $versionrequires;
2139     /** @var array other plugins that this one depends on, lazy-loaded by {@link get_other_required_plugins()} */
2140     public $dependencies;
2141     /** @var int number of instances of the plugin - not supported yet */
2142     public $instances;
2143     /** @var int order of the plugin among other plugins of the same type - not supported yet */
2144     public $sortorder;
2145     /** @var array|null array of {@link available_update_info} for this plugin */
2146     public $availableupdates;
2148     /**
2149      * Gathers and returns the information about all plugins of the given type
2150      *
2151      * @param string $type the name of the plugintype, eg. mod, auth or workshopform
2152      * @param string $typerootdir full path to the location of the plugin dir
2153      * @param string $typeclass the name of the actually called class
2154      * @return array of plugintype classes, indexed by the plugin name
2155      */
2156     public static function get_plugins($type, $typerootdir, $typeclass) {
2158         // get the information about plugins at the disk
2159         $plugins = get_plugin_list($type);
2160         $ondisk = array();
2161         foreach ($plugins as $pluginname => $pluginrootdir) {
2162             $ondisk[$pluginname] = plugininfo_default_factory::make($type, $typerootdir,
2163                 $pluginname, $pluginrootdir, $typeclass);
2164         }
2165         return $ondisk;
2166     }
2168     /**
2169      * Sets {@link $displayname} property to a localized name of the plugin
2170      */
2171     public function init_display_name() {
2172         if (!get_string_manager()->string_exists('pluginname', $this->component)) {
2173             $this->displayname = '[pluginname,' . $this->component . ']';
2174         } else {
2175             $this->displayname = get_string('pluginname', $this->component);
2176         }
2177     }
2179     /**
2180      * Magic method getter, redirects to read only values.
2181      *
2182      * @param string $name
2183      * @return mixed
2184      */
2185     public function __get($name) {
2186         switch ($name) {
2187             case 'component': return $this->type . '_' . $this->name;
2189             default:
2190                 debugging('Invalid plugin property accessed! '.$name);
2191                 return null;
2192         }
2193     }
2195     /**
2196      * Return the full path name of a file within the plugin.
2197      *
2198      * No check is made to see if the file exists.
2199      *
2200      * @param string $relativepath e.g. 'version.php'.
2201      * @return string e.g. $CFG->dirroot . '/mod/quiz/version.php'.
2202      */
2203     public function full_path($relativepath) {
2204         if (empty($this->rootdir)) {
2205             return '';
2206         }
2207         return $this->rootdir . '/' . $relativepath;
2208     }
2210     /**
2211      * Load the data from version.php.
2212      *
2213      * @param bool $disablecache do not attempt to obtain data from the cache
2214      * @return stdClass the object called $plugin defined in version.php
2215      */
2216     protected function load_version_php($disablecache=false) {
2218         $cache = cache::make('core', 'plugininfo_base');
2220         $versionsphp = $cache->get('versions_php');
2222         if (!$disablecache and $versionsphp !== false and isset($versionsphp[$this->component])) {
2223             return $versionsphp[$this->component];
2224         }
2226         $versionfile = $this->full_path('version.php');
2228         $plugin = new stdClass();
2229         if (is_readable($versionfile)) {
2230             include($versionfile);
2231         }
2232         $versionsphp[$this->component] = $plugin;
2233         $cache->set('versions_php', $versionsphp);
2235         return $plugin;
2236     }
2238     /**
2239      * Sets {@link $versiondisk} property to a numerical value representing the
2240      * version of the plugin's source code.
2241      *
2242      * If the value is null after calling this method, either the plugin
2243      * does not use versioning (typically does not have any database
2244      * data) or is missing from disk.
2245      */
2246     public function load_disk_version() {
2247         $plugin = $this->load_version_php();
2248         if (isset($plugin->version)) {
2249             $this->versiondisk = $plugin->version;
2250         }
2251     }
2253     /**
2254      * Sets {@link $versionrequires} property to a numerical value representing
2255      * the version of Moodle core that this plugin requires.
2256      */
2257     public function load_required_main_version() {
2258         $plugin = $this->load_version_php();
2259         if (isset($plugin->requires)) {
2260             $this->versionrequires = $plugin->requires;
2261         }
2262     }
2264     /**
2265      * Initialise {@link $dependencies} to the list of other plugins (in any)
2266      * that this one requires to be installed.
2267      */
2268     protected function load_other_required_plugins() {
2269         $plugin = $this->load_version_php();
2270         if (!empty($plugin->dependencies)) {
2271             $this->dependencies = $plugin->dependencies;
2272         } else {
2273             $this->dependencies = array(); // By default, no dependencies.
2274         }
2275     }
2277     /**
2278      * Get the list of other plugins that this plugin requires to be installed.
2279      *
2280      * @return array with keys the frankenstyle plugin name, and values either
2281      *      a version string (like '2011101700') or the constant ANY_VERSION.
2282      */
2283     public function get_other_required_plugins() {
2284         if (is_null($this->dependencies)) {
2285             $this->load_other_required_plugins();
2286         }
2287         return $this->dependencies;
2288     }
2290     /**
2291      * Sets {@link $versiondb} property to a numerical value representing the
2292      * currently installed version of the plugin.
2293      *
2294      * If the value is null after calling this method, either the plugin
2295      * does not use versioning (typically does not have any database
2296      * data) or has not been installed yet.
2297      */
2298     public function load_db_version() {
2299         if ($ver = self::get_version_from_config_plugins($this->component)) {
2300             $this->versiondb = $ver;
2301         }
2302     }
2304     /**
2305      * Sets {@link $source} property to one of plugin_manager::PLUGIN_SOURCE_xxx
2306      * constants.
2307      *
2308      * If the property's value is null after calling this method, then
2309      * the type of the plugin has not been recognized and you should throw
2310      * an exception.
2311      */
2312     public function init_is_standard() {
2314         $standard = plugin_manager::standard_plugins_list($this->type);
2316         if ($standard !== false) {
2317             $standard = array_flip($standard);
2318             if (isset($standard[$this->name])) {
2319                 $this->source = plugin_manager::PLUGIN_SOURCE_STANDARD;
2320             } else if (!is_null($this->versiondb) and is_null($this->versiondisk)
2321                     and plugin_manager::is_deleted_standard_plugin($this->type, $this->name)) {
2322                 $this->source = plugin_manager::PLUGIN_SOURCE_STANDARD; // to be deleted
2323             } else {
2324                 $this->source = plugin_manager::PLUGIN_SOURCE_EXTENSION;
2325             }
2326         }
2327     }
2329     /**
2330      * Returns true if the plugin is shipped with the official distribution
2331      * of the current Moodle version, false otherwise.
2332      *
2333      * @return bool
2334      */
2335     public function is_standard() {
2336         return $this->source === plugin_manager::PLUGIN_SOURCE_STANDARD;
2337     }
2339     /**
2340      * Returns true if the the given Moodle version is enough to run this plugin
2341      *
2342      * @param string|int|double $moodleversion
2343      * @return bool
2344      */
2345     public function is_core_dependency_satisfied($moodleversion) {
2347         if (empty($this->versionrequires)) {
2348             return true;
2350         } else {
2351             return (double)$this->versionrequires <= (double)$moodleversion;
2352         }
2353     }
2355     /**
2356      * Returns the status of the plugin
2357      *
2358      * @return string one of plugin_manager::PLUGIN_STATUS_xxx constants
2359      */
2360     public function get_status() {
2362         if (is_null($this->versiondb) and is_null($this->versiondisk)) {
2363             return plugin_manager::PLUGIN_STATUS_NODB;
2365         } else if (is_null($this->versiondb) and !is_null($this->versiondisk)) {
2366             return plugin_manager::PLUGIN_STATUS_NEW;
2368         } else if (!is_null($this->versiondb) and is_null($this->versiondisk)) {
2369             if (plugin_manager::is_deleted_standard_plugin($this->type, $this->name)) {
2370                 return plugin_manager::PLUGIN_STATUS_DELETE;
2371             } else {
2372                 return plugin_manager::PLUGIN_STATUS_MISSING;
2373             }
2375         } else if ((string)$this->versiondb === (string)$this->versiondisk) {
2376             return plugin_manager::PLUGIN_STATUS_UPTODATE;
2378         } else if ($this->versiondb < $this->versiondisk) {
2379             return plugin_manager::PLUGIN_STATUS_UPGRADE;
2381         } else if ($this->versiondb > $this->versiondisk) {
2382             return plugin_manager::PLUGIN_STATUS_DOWNGRADE;
2384         } else {
2385             // $version = pi(); and similar funny jokes - hopefully Donald E. Knuth will never contribute to Moodle ;-)
2386             throw new coding_exception('Unable to determine plugin state, check the plugin versions');
2387         }
2388     }
2390     /**
2391      * Returns the information about plugin availability
2392      *
2393      * True means that the plugin is enabled. False means that the plugin is
2394      * disabled. Null means that the information is not available, or the
2395      * plugin does not support configurable availability or the availability
2396      * can not be changed.
2397      *
2398      * @return null|bool
2399      */
2400     public function is_enabled() {
2401         return null;
2402     }
2404     /**
2405      * Populates the property {@link $availableupdates} with the information provided by
2406      * available update checker
2407      *
2408      * @param available_update_checker $provider the class providing the available update info
2409      */
2410     public function check_available_updates(available_update_checker $provider) {
2411         global $CFG;
2413         if (isset($CFG->updateminmaturity)) {
2414             $minmaturity = $CFG->updateminmaturity;
2415         } else {
2416             // this can happen during the very first upgrade to 2.3
2417             $minmaturity = MATURITY_STABLE;
2418         }
2420         $this->availableupdates = $provider->get_update_info($this->component,
2421             array('minmaturity' => $minmaturity));
2422     }
2424     /**
2425      * If there are updates for this plugin available, returns them.
2426      *
2427      * Returns array of {@link available_update_info} objects, if some update
2428      * is available. Returns null if there is no update available or if the update
2429      * availability is unknown.
2430      *
2431      * @return array|null
2432      */
2433     public function available_updates() {
2435         if (empty($this->availableupdates) or !is_array($this->availableupdates)) {
2436             return null;
2437         }
2439         $updates = array();
2441         foreach ($this->availableupdates as $availableupdate) {
2442             if ($availableupdate->version > $this->versiondisk) {
2443                 $updates[] = $availableupdate;
2444             }
2445         }
2447         if (empty($updates)) {
2448             return null;
2449         }
2451         return $updates;
2452     }
2454     /**
2455      * Returns the node name used in admin settings menu for this plugin settings (if applicable)
2456      *
2457      * @return null|string node name or null if plugin does not create settings node (default)
2458      */
2459     public function get_settings_section_name() {
2460         return null;
2461     }
2463     /**
2464      * Returns the URL of the plugin settings screen
2465      *
2466      * Null value means that the plugin either does not have the settings screen
2467      * or its location is not available via this library.
2468      *
2469      * @return null|moodle_url
2470      */
2471     public function get_settings_url() {
2472         $section = $this->get_settings_section_name();
2473         if ($section === null) {
2474             return null;
2475         }
2476         $settings = admin_get_root()->locate($section);
2477         if ($settings && $settings instanceof admin_settingpage) {
2478             return new moodle_url('/admin/settings.php', array('section' => $section));
2479         } else if ($settings && $settings instanceof admin_externalpage) {
2480             return new moodle_url($settings->url);
2481         } else {
2482             return null;
2483         }
2484     }
2486     /**
2487      * Loads plugin settings to the settings tree
2488      *
2489      * This function usually includes settings.php file in plugins folder.
2490      * Alternatively it can create a link to some settings page (instance of admin_externalpage)
2491      *
2492      * @param part_of_admin_tree $adminroot
2493      * @param string $parentnodename
2494      * @param bool $hassiteconfig whether the current user has moodle/site:config capability
2495      */
2496     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
2497     }
2499     /**
2500      * Returns the URL of the screen where this plugin can be uninstalled
2501      *
2502      * Visiting that URL must be safe, that is a manual confirmation is needed
2503      * for actual uninstallation of the plugin. Null value means that the
2504      * plugin either does not support uninstallation, or does not require any
2505      * database cleanup or the location of the screen is not available via this
2506      * library.
2507      *
2508      * @return null|moodle_url
2509      */
2510     public function get_uninstall_url() {
2511         return null;
2512     }
2514     /**
2515      * Returns relative directory of the plugin with heading '/'
2516      *
2517      * @return string
2518      */
2519     public function get_dir() {
2520         global $CFG;
2522         return substr($this->rootdir, strlen($CFG->dirroot));
2523     }
2525     /**
2526      * Provides access to plugin versions from the {config_plugins} table
2527      *
2528      * @param string $plugin plugin name
2529      * @param bool $disablecache do not attempt to obtain data from the cache
2530      * @return int|bool the stored value or false if not found
2531      */
2532     protected function get_version_from_config_plugins($plugin, $disablecache=false) {
2533         global $DB;
2535         $cache = cache::make('core', 'plugininfo_base');
2537         $pluginversions = $cache->get('versions_db');
2539         if ($pluginversions === false or $disablecache) {
2540             try {
2541                 $pluginversions = $DB->get_records_menu('config_plugins', array('name' => 'version'), 'plugin', 'plugin,value');
2542             } catch (dml_exception $e) {
2543                 // before install
2544                 $pluginversions = array();
2545             }
2546             $cache->set('versions_db', $pluginversions);
2547         }
2549         if (isset($pluginversions[$plugin])) {
2550             return $pluginversions[$plugin];
2551         } else {
2552             return false;
2553         }
2554     }
2558 /**
2559  * General class for all plugin types that do not have their own class
2560  */
2561 class plugininfo_general extends plugininfo_base {
2565 /**
2566  * Class for page side blocks
2567  */
2568 class plugininfo_block extends plugininfo_base {
2570     public static function get_plugins($type, $typerootdir, $typeclass) {
2572         // get the information about blocks at the disk
2573         $blocks = parent::get_plugins($type, $typerootdir, $typeclass);
2575         // add blocks missing from disk
2576         $blocksinfo = self::get_blocks_info();
2577         foreach ($blocksinfo as $blockname => $blockinfo) {
2578             if (isset($blocks[$blockname])) {
2579                 continue;
2580             }
2581             $plugin                 = new $typeclass();
2582             $plugin->type           = $type;
2583             $plugin->typerootdir    = $typerootdir;
2584             $plugin->name           = $blockname;
2585             $plugin->rootdir        = null;
2586             $plugin->displayname    = $blockname;
2587             $plugin->versiondb      = $blockinfo->version;
2588             $plugin->init_is_standard();
2590             $blocks[$blockname]   = $plugin;
2591         }
2593         return $blocks;
2594     }
2596     /**
2597      * Magic method getter, redirects to read only values.
2598      *
2599      * For block plugins pretends the object has 'visible' property for compatibility
2600      * with plugins developed for Moodle version below 2.4
2601      *
2602      * @param string $name
2603      * @return mixed
2604      */
2605     public function __get($name) {
2606         if ($name === 'visible') {
2607             debugging('This is now an instance of plugininfo_block, please use $block->is_enabled() instead of $block->visible', DEBUG_DEVELOPER);
2608             return ($this->is_enabled() !== false);
2609         }
2610         return parent::__get($name);
2611     }
2613     public function init_display_name() {
2615         if (get_string_manager()->string_exists('pluginname', 'block_' . $this->name)) {
2616             $this->displayname = get_string('pluginname', 'block_' . $this->name);
2618         } else if (($block = block_instance($this->name)) !== false) {
2619             $this->displayname = $block->get_title();
2621         } else {
2622             parent::init_display_name();
2623         }
2624     }
2626     public function load_db_version() {
2627         global $DB;
2629         $blocksinfo = self::get_blocks_info();
2630         if (isset($blocksinfo[$this->name]->version)) {
2631             $this->versiondb = $blocksinfo[$this->name]->version;
2632         }
2633     }
2635     public function is_enabled() {
2637         $blocksinfo = self::get_blocks_info();
2638         if (isset($blocksinfo[$this->name]->visible)) {
2639             if ($blocksinfo[$this->name]->visible) {
2640                 return true;
2641             } else {
2642                 return false;
2643             }
2644         } else {
2645             return parent::is_enabled();
2646         }
2647     }
2649     public function get_settings_section_name() {
2650         return 'blocksetting' . $this->name;
2651     }
2653     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
2654         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
2655         $ADMIN = $adminroot; // may be used in settings.php
2656         $block = $this; // also can be used inside settings.php
2657         $section = $this->get_settings_section_name();
2659         if (!$hassiteconfig || (($blockinstance = block_instance($this->name)) === false)) {
2660             return;
2661         }
2663         $settings = null;
2664         if ($blockinstance->has_config()) {
2665             if (file_exists($this->full_path('settings.php'))) {
2666                 $settings = new admin_settingpage($section, $this->displayname,
2667                         'moodle/site:config', $this->is_enabled() === false);
2668                 include($this->full_path('settings.php')); // this may also set $settings to null
2669             } else {
2670                 $blocksinfo = self::get_blocks_info();
2671                 $settingsurl = new moodle_url('/admin/block.php', array('block' => $blocksinfo[$this->name]->id));
2672                 $settings = new admin_externalpage($section, $this->displayname,
2673                         $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
2674             }
2675         }
2676         if ($settings) {
2677             $ADMIN->add($parentnodename, $settings);
2678         }
2679     }
2681     public function get_uninstall_url() {
2683         $blocksinfo = self::get_blocks_info();
2684         return new moodle_url('/admin/blocks.php', array('delete' => $blocksinfo[$this->name]->id, 'sesskey' => sesskey()));
2685     }
2687     /**
2688      * Provides access to the records in {block} table
2689      *
2690      * @param bool $disablecache do not attempt to obtain data from the cache
2691      * @return array array of stdClasses
2692      */
2693     protected static function get_blocks_info($disablecache=false) {
2694         global $DB;
2696         $cache = cache::make('core', 'plugininfo_block');
2698         $blocktypes = $cache->get('blocktypes');
2700         if ($blocktypes === false or $disablecache) {
2701             try {
2702                 $blocktypes = $DB->get_records('block', null, 'name', 'name,id,version,visible');
2703             } catch (dml_exception $e) {
2704                 // before install
2705                 $blocktypes = array();
2706             }
2707             $cache->set('blocktypes', $blocktypes);
2708         }
2710         return $blocktypes;
2711     }
2715 /**
2716  * Class for text filters
2717  */
2718 class plugininfo_filter extends plugininfo_base {
2720     public static function get_plugins($type, $typerootdir, $typeclass) {
2721         global $CFG, $DB;
2723         $filters = array();
2725         // get the list of filters in /filter location
2726         $installed = filter_get_all_installed();
2728         foreach ($installed as $name => $displayname) {
2729             $plugin                 = new $typeclass();
2730             $plugin->type           = $type;
2731             $plugin->typerootdir    = $typerootdir;
2732             $plugin->name           = $name;
2733             $plugin->rootdir        = "$CFG->dirroot/filter/$name";
2734             $plugin->displayname    = $displayname;
2736             $plugin->load_disk_version();
2737             $plugin->load_db_version();
2738             $plugin->load_required_main_version();
2739             $plugin->init_is_standard();
2741             $filters[$plugin->name] = $plugin;
2742         }
2744         // Do not mess with filter registration here!
2746         $globalstates = self::get_global_states();
2748         // make sure that all registered filters are installed, just in case
2749         foreach ($globalstates as $name => $info) {
2750             if (!isset($filters[$name])) {
2751                 // oops, there is a record in filter_active but the filter is not installed
2752                 $plugin                 = new $typeclass();
2753                 $plugin->type           = $type;
2754                 $plugin->typerootdir    = $typerootdir;
2755                 $plugin->name           = $name;
2756                 $plugin->rootdir        = "$CFG->dirroot/filter/$name";
2757                 $plugin->displayname    = $name;
2759                 $plugin->load_db_version();
2761                 if (is_null($plugin->versiondb)) {
2762                     // this is a hack to stimulate 'Missing from disk' error
2763                     // because $plugin->versiondisk will be null !== false
2764                     $plugin->versiondb = false;
2765                 }
2767                 $filters[$plugin->name] = $plugin;
2768             }
2769         }
2771         return $filters;
2772     }
2774     public function init_display_name() {
2775         // do nothing, the name is set in self::get_plugins()
2776     }
2778     public function is_enabled() {
2780         $globalstates = self::get_global_states();
2782         foreach ($globalstates as $name => $info) {
2783             if ($name === $this->name) {
2784                 if ($info->active == TEXTFILTER_DISABLED) {
2785                     return false;
2786                 } else {
2787                     // it may be 'On' or 'Off, but available'
2788                     return null;
2789                 }
2790             }
2791         }
2793         return null;
2794     }
2796     public function get_settings_section_name() {
2797         return 'filtersetting' . $this->name;
2798     }
2800     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
2801         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
2802         $ADMIN = $adminroot; // may be used in settings.php
2803         $filter = $this; // also can be used inside settings.php
2805         $settings = null;
2806         if ($hassiteconfig && file_exists($this->full_path('filtersettings.php'))) {
2807             $section = $this->get_settings_section_name();
2808             $settings = new admin_settingpage($section, $this->displayname,
2809                     'moodle/site:config', $this->is_enabled() === false);
2810             include($this->full_path('filtersettings.php')); // this may also set $settings to null
2811         }
2812         if ($settings) {
2813             $ADMIN->add($parentnodename, $settings);
2814         }
2815     }
2817     public function get_uninstall_url() {
2818         return new moodle_url('/admin/filters.php', array('sesskey' => sesskey(), 'filterpath' => $this->name, 'action' => 'delete'));
2819     }
2821     /**
2822      * Provides access to the results of {@link filter_get_global_states()}
2823      * but indexed by the normalized filter name
2824      *
2825      * The legacy filter name is available as ->legacyname property.
2826      *
2827      * @param bool $disablecache do not attempt to obtain data from the cache
2828      * @return array
2829      */
2830     protected static function get_global_states($disablecache=false) {
2831         global $DB;
2833         $cache = cache::make('core', 'plugininfo_filter');
2835         $globalstates = $cache->get('globalstates');
2837         if ($globalstates === false or $disablecache) {
2839             if (!$DB->get_manager()->table_exists('filter_active')) {
2840                 // Not installed yet.
2841                 $cache->set('globalstates', array());
2842                 return array();
2843             }
2845             $globalstates = array();
2847             foreach (filter_get_global_states() as $name => $info) {
2848                 if (strpos($name, '/') !== false) {
2849                     // Skip existing before upgrade to new names.
2850                     continue;
2851                 }
2853                 $filterinfo = new stdClass();
2854                 $filterinfo->active = $info->active;
2855                 $filterinfo->sortorder = $info->sortorder;
2856                 $globalstates[$name] = $filterinfo;
2857             }
2859             $cache->set('globalstates', $globalstates);
2860         }
2862         return $globalstates;
2863     }
2867 /**
2868  * Class for activity modules
2869  */
2870 class plugininfo_mod extends plugininfo_base {
2872     public static function get_plugins($type, $typerootdir, $typeclass) {
2874         // get the information about plugins at the disk
2875         $modules = parent::get_plugins($type, $typerootdir, $typeclass);
2877         // add modules missing from disk
2878         $modulesinfo = self::get_modules_info();
2879         foreach ($modulesinfo as $modulename => $moduleinfo) {
2880             if (isset($modules[$modulename])) {
2881                 continue;
2882             }
2883             $plugin                 = new $typeclass();
2884             $plugin->type           = $type;
2885             $plugin->typerootdir    = $typerootdir;
2886             $plugin->name           = $modulename;
2887             $plugin->rootdir        = null;
2888             $plugin->displayname    = $modulename;
2889             $plugin->versiondb      = $moduleinfo->version;
2890             $plugin->init_is_standard();
2892             $modules[$modulename]   = $plugin;
2893         }
2895         return $modules;
2896     }
2898     /**
2899      * Magic method getter, redirects to read only values.
2900      *
2901      * For module plugins we pretend the object has 'visible' property for compatibility
2902      * with plugins developed for Moodle version below 2.4
2903      *
2904      * @param string $name
2905      * @return mixed
2906      */
2907     public function __get($name) {
2908         if ($name === 'visible') {
2909             debugging('This is now an instance of plugininfo_mod, please use $module->is_enabled() instead of $module->visible', DEBUG_DEVELOPER);
2910             return ($this->is_enabled() !== false);
2911         }
2912         return parent::__get($name);
2913     }
2915     public function init_display_name() {
2916         if (get_string_manager()->string_exists('pluginname', $this->component)) {
2917             $this->displayname = get_string('pluginname', $this->component);
2918         } else {
2919             $this->displayname = get_string('modulename', $this->component);
2920         }
2921     }
2923     /**
2924      * Load the data from version.php.
2925      *
2926      * @param bool $disablecache do not attempt to obtain data from the cache
2927      * @return object the data object defined in version.php.
2928      */
2929     protected function load_version_php($disablecache=false) {
2931         $cache = cache::make('core', 'plugininfo_base');
2933         $versionsphp = $cache->get('versions_php');
2935         if (!$disablecache and $versionsphp !== false and isset($versionsphp[$this->component])) {
2936             return $versionsphp[$this->component];
2937         }
2939         $versionfile = $this->full_path('version.php');
2941         $module = new stdClass();
2942         $plugin = new stdClass();
2943         if (is_readable($versionfile)) {
2944             include($versionfile);
2945         }
2946         if (!isset($module->version) and isset($plugin->version)) {
2947             $module = $plugin;
2948         }
2949         $versionsphp[$this->component] = $module;
2950         $cache->set('versions_php', $versionsphp);
2952         return $module;
2953     }
2955     public function load_db_version() {
2956         global $DB;
2958         $modulesinfo = self::get_modules_info();
2959         if (isset($modulesinfo[$this->name]->version)) {
2960             $this->versiondb = $modulesinfo[$this->name]->version;
2961         }
2962     }
2964     public function is_enabled() {
2966         $modulesinfo = self::get_modules_info();
2967         if (isset($modulesinfo[$this->name]->visible)) {
2968             if ($modulesinfo[$this->name]->visible) {
2969                 return true;
2970             } else {
2971                 return false;
2972             }
2973         } else {
2974             return parent::is_enabled();
2975         }
2976     }
2978     public function get_settings_section_name() {
2979         return 'modsetting' . $this->name;
2980     }
2982     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
2983         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
2984         $ADMIN = $adminroot; // may be used in settings.php
2985         $module = $this; // also can be used inside settings.php
2986         $section = $this->get_settings_section_name();
2988         $modulesinfo = self::get_modules_info();
2989         $settings = null;
2990         if ($hassiteconfig && isset($modulesinfo[$this->name]) && file_exists($this->full_path('settings.php'))) {
2991             $settings = new admin_settingpage($section, $this->displayname,
2992                     'moodle/site:config', $this->is_enabled() === false);
2993             include($this->full_path('settings.php')); // this may also set $settings to null
2994         }
2995         if ($settings) {
2996             $ADMIN->add($parentnodename, $settings);
2997         }
2998     }
3000     public function get_uninstall_url() {
3002         if ($this->name !== 'forum') {
3003             return new moodle_url('/admin/modules.php', array('delete' => $this->name, 'sesskey' => sesskey()));
3004         } else {
3005             return null;
3006         }
3007     }
3009     /**
3010      * Provides access to the records in {modules} table
3011      *
3012      * @param bool $disablecache do not attempt to obtain data from the cache
3013      * @return array array of stdClasses
3014      */
3015     protected static function get_modules_info($disablecache=false) {
3016         global $DB;
3018         $cache = cache::make('core', 'plugininfo_mod');
3020         $modulesinfo = $cache->get('modulesinfo');
3022         if ($modulesinfo === false or $disablecache) {
3023             try {
3024                 $modulesinfo = $DB->get_records('modules', null, 'name', 'name,id,version,visible');
3025             } catch (dml_exception $e) {
3026                 // before install
3027                 $modulesinfo = array();
3028             }
3029             $cache->set('modulesinfo', $modulesinfo);
3030         }
3032         return $modulesinfo;
3033     }
3037 /**
3038  * Class for question behaviours.
3039  */
3040 class plugininfo_qbehaviour extends plugininfo_base {
3042     public function get_uninstall_url() {
3043         return new moodle_url('/admin/qbehaviours.php',
3044                 array('delete' => $this->name, 'sesskey' => sesskey()));
3045     }
3049 /**
3050  * Class for question types
3051  */
3052 class plugininfo_qtype extends plugininfo_base {
3054     public function get_uninstall_url() {
3055         return new moodle_url('/admin/qtypes.php',
3056                 array('delete' => $this->name, 'sesskey' => sesskey()));
3057     }
3059     public function get_settings_section_name() {
3060         return 'qtypesetting' . $this->name;
3061     }
3063     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3064         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3065         $ADMIN = $adminroot; // may be used in settings.php
3066         $qtype = $this; // also can be used inside settings.php
3067         $section = $this->get_settings_section_name();
3069         $settings = null;
3070         $systemcontext = context_system::instance();
3071         if (($hassiteconfig || has_capability('moodle/question:config', $systemcontext)) &&
3072                 file_exists($this->full_path('settings.php'))) {
3073             $settings = new admin_settingpage($section, $this->displayname,
3074                     'moodle/question:config', $this->is_enabled() === false);
3075             include($this->full_path('settings.php')); // this may also set $settings to null
3076         }
3077         if ($settings) {
3078             $ADMIN->add($parentnodename, $settings);
3079         }
3080     }
3084 /**
3085  * Class for authentication plugins
3086  */
3087 class plugininfo_auth extends plugininfo_base {
3089     public function is_enabled() {
3090         global $CFG;
3092         if (in_array($this->name, array('nologin', 'manual'))) {
3093             // these two are always enabled and can't be disabled
3094             return null;
3095         }
3097         $enabled = array_flip(explode(',', $CFG->auth));
3099         return isset($enabled[$this->name]);
3100     }
3102     public function get_settings_section_name() {
3103         return 'authsetting' . $this->name;
3104     }
3106     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3107         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3108         $ADMIN = $adminroot; // may be used in settings.php
3109         $auth = $this; // also to be used inside settings.php
3110         $section = $this->get_settings_section_name();
3112         $settings = null;
3113         if ($hassiteconfig) {
3114             if (file_exists($this->full_path('settings.php'))) {
3115                 // TODO: finish implementation of common settings - locking, etc.
3116                 $settings = new admin_settingpage($section, $this->displayname,
3117                         'moodle/site:config', $this->is_enabled() === false);
3118                 include($this->full_path('settings.php')); // this may also set $settings to null
3119             } else {
3120                 $settingsurl = new moodle_url('/admin/auth_config.php', array('auth' => $this->name));
3121                 $settings = new admin_externalpage($section, $this->displayname,
3122                         $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
3123             }
3124         }
3125         if ($settings) {
3126             $ADMIN->add($parentnodename, $settings);
3127         }
3128     }
3132 /**
3133  * Class for enrolment plugins
3134  */
3135 class plugininfo_enrol extends plugininfo_base {
3137     public function is_enabled() {
3138         global $CFG;
3140         // We do not actually need whole enrolment classes here so we do not call
3141         // {@link enrol_get_plugins()}. Note that this may produce slightly different
3142         // results, for example if the enrolment plugin does not contain lib.php
3143         // but it is listed in $CFG->enrol_plugins_enabled
3145         $enabled = array_flip(explode(',', $CFG->enrol_plugins_enabled));
3147         return isset($enabled[$this->name]);
3148     }
3150     public function get_settings_section_name() {
3151         if (file_exists($this->full_path('settings.php'))) {
3152             return 'enrolsettings' . $this->name;
3153         } else {
3154             return null;
3155         }
3156     }
3158     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3159         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3161         if (!$hassiteconfig or !file_exists($this->full_path('settings.php'))) {
3162             return;
3163         }
3164         $section = $this->get_settings_section_name();
3166         $ADMIN = $adminroot; // may be used in settings.php
3167         $enrol = $this; // also can be used inside settings.php
3168         $settings = new admin_settingpage($section, $this->displayname,
3169                 'moodle/site:config', $this->is_enabled() === false);
3171         include($this->full_path('settings.php')); // This may also set $settings to null!
3173         if ($settings) {
3174             $ADMIN->add($parentnodename, $settings);
3175         }
3176     }
3178     public function get_uninstall_url() {
3179         return new moodle_url('/admin/enrol.php', array('action' => 'uninstall', 'enrol' => $this->name, 'sesskey' => sesskey()));
3180     }
3184 /**
3185  * Class for messaging processors
3186  */
3187 class plugininfo_message extends plugininfo_base {
3189     public function get_settings_section_name() {
3190         return 'messagesetting' . $this->name;
3191     }
3193     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3194         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3195         $ADMIN = $adminroot; // may be used in settings.php
3196         if (!$hassiteconfig) {
3197             return;
3198         }
3199         $section = $this->get_settings_section_name();
3201         $settings = null;
3202         $processors = get_message_processors();
3203         if (isset($processors[$this->name])) {
3204             $processor = $processors[$this->name];
3205             if ($processor->available && $processor->hassettings) {
3206                 $settings = new admin_settingpage($section, $this->displayname,
3207                         'moodle/site:config', $this->is_enabled() === false);
3208                 include($this->full_path('settings.php')); // this may also set $settings to null
3209             }
3210         }
3211         if ($settings) {
3212             $ADMIN->add($parentnodename, $settings);
3213         }
3214     }
3216     /**
3217      * @see plugintype_interface::is_enabled()
3218      */
3219     public function is_enabled() {
3220         $processors = get_message_processors();
3221         if (isset($processors[$this->name])) {
3222             return $processors[$this->name]->configured && $processors[$this->name]->enabled;
3223         } else {
3224             return parent::is_enabled();
3225         }
3226     }
3228     /**
3229      * @see plugintype_interface::get_uninstall_url()
3230      */
3231     public function get_uninstall_url() {
3232         $processors = get_message_processors();
3233         if (isset($processors[$this->name])) {
3234             return new moodle_url('/admin/message.php', array('uninstall' => $processors[$this->name]->id, 'sesskey' => sesskey()));
3235         } else {
3236             return parent::get_uninstall_url();
3237         }
3238     }
3242 /**
3243  * Class for repositories
3244  */
3245 class plugininfo_repository extends plugininfo_base {
3247     public function is_enabled() {
3249         $enabled = self::get_enabled_repositories();
3251         return isset($enabled[$this->name]);
3252     }
3254     public function get_settings_section_name() {
3255         return 'repositorysettings'.$this->name;
3256     }
3258     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3259         if ($hassiteconfig && $this->is_enabled()) {
3260             // completely no access to repository setting when it is not enabled
3261             $sectionname = $this->get_settings_section_name();
3262             $settingsurl = new moodle_url('/admin/repository.php',
3263                     array('sesskey' => sesskey(), 'action' => 'edit', 'repos' => $this->name));
3264             $settings = new admin_externalpage($sectionname, $this->displayname,
3265                     $settingsurl, 'moodle/site:config', false);
3266             $adminroot->add($parentnodename, $settings);
3267         }
3268     }
3270     /**
3271      * Provides access to the records in {repository} table
3272      *
3273      * @param bool $disablecache do not attempt to obtain data from the cache
3274      * @return array array of stdClasses
3275      */
3276     protected static function get_enabled_repositories($disablecache=false) {
3277         global $DB;
3279         $cache = cache::make('core', 'plugininfo_repository');
3281         $enabled = $cache->get('enabled');
3283         if ($enabled === false or $disablecache) {
3284             $enabled = $DB->get_records('repository', null, 'type', 'type,visible,sortorder');
3285             $cache->set('enabled', $enabled);
3286         }
3288         return $enabled;
3289     }
3293 /**
3294  * Class for portfolios
3295  */
3296 class plugininfo_portfolio extends plugininfo_base {
3298     public function is_enabled() {
3300         $enabled = self::get_enabled_portfolios();
3302         return isset($enabled[$this->name]);
3303     }
3305     /**
3306      * Returns list of enabled portfolio plugins
3307      *
3308      * Portfolio plugin is enabled if there is at least one record in the {portfolio_instance}
3309      * table for it.
3310      *
3311      * @param bool $disablecache do not attempt to obtain data from the cache
3312      * @return array array of stdClasses with properties plugin and visible indexed by plugin
3313      */
3314     protected static function get_enabled_portfolios($disablecache=false) {
3315         global $DB;
3317         $cache = cache::make('core', 'plugininfo_portfolio');
3319         $enabled = $cache->get('enabled');
3321         if ($enabled === false or $disablecache) {
3322             $enabled = array();
3323             $instances = $DB->get_recordset('portfolio_instance', null, '', 'plugin,visible');
3324             foreach ($instances as $instance) {
3325                 if (isset($enabled[$instance->plugin])) {
3326                     if ($instance->visible) {
3327                         $enabled[$instance->plugin]->visible = $instance->visible;
3328                     }
3329                 } else {
3330                     $enabled[$instance->plugin] = $instance;
3331                 }
3332             }
3333             $instances->close();
3334             $cache->set('enabled', $enabled);
3335         }
3337         return $enabled;
3338     }
3342 /**
3343  * Class for themes
3344  */
3345 class plugininfo_theme extends plugininfo_base {
3347     public function is_enabled() {
3348         global $CFG;
3350         if ((!empty($CFG->theme) and $CFG->theme === $this->name) or
3351             (!empty($CFG->themelegacy) and $CFG->themelegacy === $this->name)) {
3352             return true;
3353         } else {
3354             return parent::is_enabled();
3355         }
3356     }
3360 /**
3361  * Class representing an MNet service
3362  */
3363 class plugininfo_mnetservice extends plugininfo_base {
3365     public function is_enabled() {
3366         global $CFG;
3368         if (empty($CFG->mnet_dispatcher_mode) || $CFG->mnet_dispatcher_mode !== 'strict') {
3369             return false;
3370         } else {
3371             return parent::is_enabled();
3372         }
3373     }
3377 /**
3378  * Class for admin tool plugins
3379  */
3380 class plugininfo_tool extends plugininfo_base {
3382     public function get_uninstall_url() {
3383         return new moodle_url('/admin/tools.php', array('delete' => $this->name, 'sesskey' => sesskey()));
3384     }
3388 /**
3389  * Class for admin tool plugins
3390  */
3391 class plugininfo_report extends plugininfo_base {
3393     public function get_uninstall_url() {
3394         return new moodle_url('/admin/reports.php', array('delete' => $this->name, 'sesskey' => sesskey()));
3395     }
3399 /**
3400  * Class for local plugins
3401  */
3402 class plugininfo_local extends plugininfo_base {
3404     public function get_uninstall_url() {
3405         return new moodle_url('/admin/localplugins.php', array('delete' => $this->name, 'sesskey' => sesskey()));
3406     }
3409 /**
3410  * Class for HTML editors
3411  */
3412 class plugininfo_editor extends plugininfo_base {
3414     public function get_settings_section_name() {
3415         return 'editorsettings' . $this->name;
3416     }
3418     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3419         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3420         $ADMIN = $adminroot; // may be used in settings.php
3421         $editor = $this; // also can be used inside settings.php
3422         $section = $this->get_settings_section_name();
3424         $settings = null;
3425         if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3426             $settings = new admin_settingpage($section, $this->displayname,
3427                     'moodle/site:config', $this->is_enabled() === false);
3428             include($this->full_path('settings.php')); // this may also set $settings to null
3429         }
3430         if ($settings) {
3431             $ADMIN->add($parentnodename, $settings);
3432         }
3433     }
3435     /**
3436      * Returns the information about plugin availability
3437      *
3438      * True means that the plugin is enabled. False means that the plugin is
3439      * disabled. Null means that the information is not available, or the
3440      * plugin does not support configurable availability or the availability
3441      * can not be changed.
3442      *
3443      * @return null|bool
3444      */
3445     public function is_enabled() {
3446         global $CFG;
3447         if (empty($CFG->texteditors)) {
3448             $CFG->texteditors = 'tinymce,textarea';
3449         }
3450         if (in_array($this->name, explode(',', $CFG->texteditors))) {
3451             return true;
3452         }
3453         return false;
3454     }
3457 /**
3458  * Class for plagiarism plugins
3459  */
3460 class plugininfo_plagiarism extends plugininfo_base {
3462     public function get_settings_section_name() {
3463         return 'plagiarism'. $this->name;
3464     }
3466     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3467         // plagiarism plugin just redirect to settings.php in the plugins directory
3468         if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3469             $section = $this->get_settings_section_name();
3470             $settingsurl = new moodle_url($this->get_dir().'/settings.php');
3471             $settings = new admin_externalpage($section, $this->displayname,
3472                     $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
3473             $adminroot->add($parentnodename, $settings);
3474         }
3475     }
3478 /**
3479  * Class for webservice protocols
3480  */
3481 class plugininfo_webservice extends plugininfo_base {
3483     public function get_settings_section_name() {
3484         return 'webservicesetting' . $this->name;
3485     }
3487     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3488         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3489         $ADMIN = $adminroot; // may be used in settings.php
3490         $webservice = $this; // also can be used inside settings.php
3491         $section = $this->get_settings_section_name();
3493         $settings = null;
3494         if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3495             $settings = new admin_settingpage($section, $this->displayname,
3496                     'moodle/site:config', $this->is_enabled() === false);
3497             include($this->full_path('settings.php')); // this may also set $settings to null
3498         }
3499         if ($settings) {
3500             $ADMIN->add($parentnodename, $settings);
3501         }
3502     }
3504     public function is_enabled() {
3505         global $CFG;
3506         if (empty($CFG->enablewebservices)) {
3507             return false;
3508         }
3509         $active_webservices = empty($CFG->webserviceprotocols) ? array() : explode(',', $CFG->webserviceprotocols);
3510         if (in_array($this->name, $active_webservices)) {
3511             return true;
3512         }
3513         return false;
3514     }
3516     public function get_uninstall_url() {
3517         return new moodle_url('/admin/webservice/protocols.php',
3518                 array('sesskey' => sesskey(), 'action' => 'uninstall', 'webservice' => $this->name));
3519     }
3522 /**
3523  * Class for course formats
3524  */
3525 class plugininfo_format extends plugininfo_base {
3527     /**
3528      * Gathers and returns the information about all plugins of the given type
3529      *
3530      * @param string $type the name of the plugintype, eg. mod, auth or workshopform
3531      * @param string $typerootdir full path to the location of the plugin dir
3532      * @param string $typeclass the name of the actually called class
3533      * @return array of plugintype classes, indexed by the plugin name
3534      */
3535     public static function get_plugins($type, $typerootdir, $typeclass) {
3536         global $CFG;
3537         $formats = parent::get_plugins($type, $typerootdir, $typeclass);
3538         require_once($CFG->dirroot.'/course/lib.php');
3539         $order = get_sorted_course_formats();
3540         $sortedformats = array();
3541         foreach ($order as $formatname) {
3542             $sortedformats[$formatname] = $formats[$formatname];
3543         }
3544         return $sortedformats;
3545     }
3547     public function get_settings_section_name() {
3548         return 'formatsetting' . $this->name;
3549     }
3551     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3552         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3553         $ADMIN = $adminroot; // also may be used in settings.php
3554         $section = $this->get_settings_section_name();
3556         $settings = null;
3557         if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3558             $settings = new admin_settingpage($section, $this->displayname,
3559                     'moodle/site:config', $this->is_enabled() === false);
3560             include($this->full_path('settings.php')); // this may also set $settings to null
3561         }
3562         if ($settings) {
3563             $ADMIN->add($parentnodename, $settings);
3564         }
3565     }
3567     public function is_enabled() {
3568         return !get_config($this->component, 'disabled');
3569     }
3571     public function get_uninstall_url() {
3572         if ($this->name !== get_config('moodlecourse', 'format') && $this->name !== 'site') {
3573             return new moodle_url('/admin/courseformats.php',
3574                     array('sesskey' => sesskey(), 'action' => 'uninstall', 'format' => $this->name));
3575         }
3576         return parent::get_uninstall_url();
3577     }