9211a5bf2d54ab08f0a6affeced5fc4ae0da807d
[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 a tree of known plugins and information about them
101      *
102      * @param bool $disablecache force reload, cache can be used otherwise
103      * @return array 2D array. The first keys are plugin type names (e.g. qtype);
104      *      the second keys are the plugin local name (e.g. multichoice); and
105      *      the values are the corresponding objects extending {@link plugininfo_base}
106      */
107     public function get_plugins($disablecache=false) {
108         global $CFG;
110         if ($disablecache or is_null($this->pluginsinfo)) {
111             // Hack: include mod and editor subplugin management classes first,
112             //       the adminlib.php is supposed to contain extra admin settings too.
113             require_once($CFG->libdir.'/adminlib.php');
114             foreach(array('mod', 'editor') as $type) {
115                 foreach (get_plugin_list($type) as $dir) {
116                     if (file_exists("$dir/adminlib.php")) {
117                         include_once("$dir/adminlib.php");
118                     }
119                 }
120             }
121             $this->pluginsinfo = array();
122             $plugintypes = get_plugin_types();
123             $plugintypes = $this->reorder_plugin_types($plugintypes);
124             foreach ($plugintypes as $plugintype => $plugintyperootdir) {
125                 if (in_array($plugintype, array('base', 'general'))) {
126                     throw new coding_exception('Illegal usage of reserved word for plugin type');
127                 }
128                 if (class_exists('plugininfo_' . $plugintype)) {
129                     $plugintypeclass = 'plugininfo_' . $plugintype;
130                 } else {
131                     $plugintypeclass = 'plugininfo_general';
132                 }
133                 if (!in_array('plugininfo_base', class_parents($plugintypeclass))) {
134                     throw new coding_exception('Class ' . $plugintypeclass . ' must extend plugininfo_base');
135                 }
136                 $plugins = call_user_func(array($plugintypeclass, 'get_plugins'), $plugintype, $plugintyperootdir, $plugintypeclass);
137                 $this->pluginsinfo[$plugintype] = $plugins;
138             }
140             if (empty($CFG->disableupdatenotifications) and !during_initial_install()) {
141                 // append the information about available updates provided by {@link available_update_checker()}
142                 $provider = available_update_checker::instance();
143                 foreach ($this->pluginsinfo as $plugintype => $plugins) {
144                     foreach ($plugins as $plugininfoholder) {
145                         $plugininfoholder->check_available_updates($provider);
146                     }
147                 }
148             }
149         }
151         return $this->pluginsinfo;
152     }
154     /**
155      * Returns list of plugins that define their subplugins and the information
156      * about them from the db/subplugins.php file.
157      *
158      * At the moment, only activity modules and editors can define subplugins.
159      *
160      * @param bool $disablecache force reload, cache can be used otherwise
161      * @return array with keys like 'mod_quiz', and values the data from the
162      *      corresponding db/subplugins.php file.
163      */
164     public function get_subplugins($disablecache=false) {
166         if ($disablecache or is_null($this->subpluginsinfo)) {
167             $this->subpluginsinfo = array();
168             foreach (array('mod', 'editor') as $type) {
169                 $owners = get_plugin_list($type);
170                 foreach ($owners as $component => $ownerdir) {
171                     $componentsubplugins = array();
172                     if (file_exists($ownerdir . '/db/subplugins.php')) {
173                         $subplugins = array();
174                         include($ownerdir . '/db/subplugins.php');
175                         foreach ($subplugins as $subplugintype => $subplugintyperootdir) {
176                             $subplugin = new stdClass();
177                             $subplugin->type = $subplugintype;
178                             $subplugin->typerootdir = $subplugintyperootdir;
179                             $componentsubplugins[$subplugintype] = $subplugin;
180                         }
181                         $this->subpluginsinfo[$type . '_' . $component] = $componentsubplugins;
182                     }
183                 }
184             }
185         }
187         return $this->subpluginsinfo;
188     }
190     /**
191      * Returns the name of the plugin that defines the given subplugin type
192      *
193      * If the given subplugin type is not actually a subplugin, returns false.
194      *
195      * @param string $subplugintype the name of subplugin type, eg. workshopform or quiz
196      * @return false|string the name of the parent plugin, eg. mod_workshop
197      */
198     public function get_parent_of_subplugin($subplugintype) {
200         $parent = false;
201         foreach ($this->get_subplugins() as $pluginname => $subplugintypes) {
202             if (isset($subplugintypes[$subplugintype])) {
203                 $parent = $pluginname;
204                 break;
205             }
206         }
208         return $parent;
209     }
211     /**
212      * Returns a localized name of a given plugin
213      *
214      * @param string $plugin name of the plugin, eg mod_workshop or auth_ldap
215      * @return string
216      */
217     public function plugin_name($plugin) {
218         list($type, $name) = normalize_component($plugin);
219         return $this->pluginsinfo[$type][$name]->displayname;
220     }
222     /**
223      * Returns a localized name of a plugin type in plural form
224      *
225      * Most plugin types define their names in core_plugin lang file. In case of subplugins,
226      * we try to ask the parent plugin for the name. In the worst case, we will return
227      * the value of the passed $type parameter.
228      *
229      * @param string $type the type of the plugin, e.g. mod or workshopform
230      * @return string
231      */
232     public function plugintype_name_plural($type) {
234         if (get_string_manager()->string_exists('type_' . $type . '_plural', 'core_plugin')) {
235             // for most plugin types, their names are defined in core_plugin lang file
236             return get_string('type_' . $type . '_plural', 'core_plugin');
238         } else if ($parent = $this->get_parent_of_subplugin($type)) {
239             // if this is a subplugin, try to ask the parent plugin for the name
240             if (get_string_manager()->string_exists('subplugintype_' . $type . '_plural', $parent)) {
241                 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type . '_plural', $parent);
242             } else {
243                 return $this->plugin_name($parent) . ' / ' . $type;
244             }
246         } else {
247             return $type;
248         }
249     }
251     /**
252      * @param string $component frankenstyle component name.
253      * @return plugininfo_base|null the corresponding plugin information.
254      */
255     public function get_plugin_info($component) {
256         list($type, $name) = normalize_component($component);
257         $plugins = $this->get_plugins();
258         if (isset($plugins[$type][$name])) {
259             return $plugins[$type][$name];
260         } else {
261             return null;
262         }
263     }
265     /**
266      * Get a list of any other plugins that require this one.
267      * @param string $component frankenstyle component name.
268      * @return array of frankensyle component names that require this one.
269      */
270     public function other_plugins_that_require($component) {
271         $others = array();
272         foreach ($this->get_plugins() as $type => $plugins) {
273             foreach ($plugins as $plugin) {
274                 $required = $plugin->get_other_required_plugins();
275                 if (isset($required[$component])) {
276                     $others[] = $plugin->component;
277                 }
278             }
279         }
280         return $others;
281     }
283     /**
284      * Check a dependencies list against the list of installed plugins.
285      * @param array $dependencies compenent name to required version or ANY_VERSION.
286      * @return bool true if all the dependencies are satisfied.
287      */
288     public function are_dependencies_satisfied($dependencies) {
289         foreach ($dependencies as $component => $requiredversion) {
290             $otherplugin = $this->get_plugin_info($component);
291             if (is_null($otherplugin)) {
292                 return false;
293             }
295             if ($requiredversion != ANY_VERSION and $otherplugin->versiondisk < $requiredversion) {
296                 return false;
297             }
298         }
300         return true;
301     }
303     /**
304      * Checks all dependencies for all installed plugins
305      *
306      * This is used by install and upgrade. The array passed by reference as the second
307      * argument is populated with the list of plugins that have failed dependencies (note that
308      * a single plugin can appear multiple times in the $failedplugins).
309      *
310      * @param int $moodleversion the version from version.php.
311      * @param array $failedplugins to return the list of plugins with non-satisfied dependencies
312      * @return bool true if all the dependencies are satisfied for all plugins.
313      */
314     public function all_plugins_ok($moodleversion, &$failedplugins = array()) {
316         $return = true;
317         foreach ($this->get_plugins() as $type => $plugins) {
318             foreach ($plugins as $plugin) {
320                 if (!$plugin->is_core_dependency_satisfied($moodleversion)) {
321                     $return = false;
322                     $failedplugins[] = $plugin->component;
323                 }
325                 if (!$this->are_dependencies_satisfied($plugin->get_other_required_plugins())) {
326                     $return = false;
327                     $failedplugins[] = $plugin->component;
328                 }
329             }
330         }
332         return $return;
333     }
335     /**
336      * Checks if there are some plugins with a known available update
337      *
338      * @return bool true if there is at least one available update
339      */
340     public function some_plugins_updatable() {
341         foreach ($this->get_plugins() as $type => $plugins) {
342             foreach ($plugins as $plugin) {
343                 if ($plugin->available_updates()) {
344                     return true;
345                 }
346             }
347         }
349         return false;
350     }
352     /**
353      * Defines a list of all plugins that were originally shipped in the standard Moodle distribution,
354      * but are not anymore and are deleted during upgrades.
355      *
356      * The main purpose of this list is to hide missing plugins during upgrade.
357      *
358      * @param string $type plugin type
359      * @param string $name plugin name
360      * @return bool
361      */
362     public static function is_deleted_standard_plugin($type, $name) {
363         static $plugins = array(
364             // do not add 1.9-2.2 plugin removals here
365         );
367         if (!isset($plugins[$type])) {
368             return false;
369         }
370         return in_array($name, $plugins[$type]);
371     }
373     /**
374      * Defines a white list of all plugins shipped in the standard Moodle distribution
375      *
376      * @param string $type
377      * @return false|array array of standard plugins or false if the type is unknown
378      */
379     public static function standard_plugins_list($type) {
380         static $standard_plugins = array(
382             'assignment' => array(
383                 'offline', 'online', 'upload', 'uploadsingle'
384             ),
386             'assignsubmission' => array(
387                 'comments', 'file', 'onlinetext'
388             ),
390             'assignfeedback' => array(
391                 'comments', 'file', 'offline'
392             ),
394             'auth' => array(
395                 'cas', 'db', 'email', 'fc', 'imap', 'ldap', 'manual', 'mnet',
396                 'nntp', 'nologin', 'none', 'pam', 'pop3', 'radius',
397                 'shibboleth', 'webservice'
398             ),
400             'block' => array(
401                 'activity_modules', 'admin_bookmarks', 'blog_menu',
402                 'blog_recent', 'blog_tags', 'calendar_month',
403                 'calendar_upcoming', 'comments', 'community',
404                 'completionstatus', 'course_list', 'course_overview',
405                 'course_summary', 'feedback', 'glossary_random', 'html',
406                 'login', 'mentees', 'messages', 'mnet_hosts', 'myprofile',
407                 'navigation', 'news_items', 'online_users', 'participants',
408                 'private_files', 'quiz_results', 'recent_activity',
409                 'rss_client', 'search_forums', 'section_links',
410                 'selfcompletion', 'settings', 'site_main_menu',
411                 'social_activities', 'tag_flickr', 'tag_youtube', 'tags'
412             ),
414             'booktool' => array(
415                 'exportimscp', 'importhtml', 'print'
416             ),
418             'cachelock' => array(
419                 'file'
420             ),
422             'cachestore' => array(
423                 'file', 'memcache', 'memcached', 'mongodb', 'session', 'static'
424             ),
426             'coursereport' => array(
427                 //deprecated!
428             ),
430             'datafield' => array(
431                 'checkbox', 'date', 'file', 'latlong', 'menu', 'multimenu',
432                 'number', 'picture', 'radiobutton', 'text', 'textarea', 'url'
433             ),
435             'datapreset' => array(
436                 'imagegallery'
437             ),
439             'editor' => array(
440                 'textarea', 'tinymce'
441             ),
443             'enrol' => array(
444                 'authorize', 'category', 'cohort', 'database', 'flatfile',
445                 'guest', 'imsenterprise', 'ldap', 'manual', 'meta', 'mnet',
446                 'paypal', 'self'
447             ),
449             'filter' => array(
450                 'activitynames', 'algebra', 'censor', 'emailprotect',
451                 'emoticon', 'mediaplugin', 'multilang', 'tex', 'tidy',
452                 'urltolink', 'data', 'glossary'
453             ),
455             'format' => array(
456                 'scorm', 'social', 'topics', 'weeks'
457             ),
459             'gradeexport' => array(
460                 'ods', 'txt', 'xls', 'xml'
461             ),
463             'gradeimport' => array(
464                 'csv', 'xml'
465             ),
467             'gradereport' => array(
468                 'grader', 'outcomes', 'overview', 'user'
469             ),
471             'gradingform' => array(
472                 'rubric', 'guide'
473             ),
475             'local' => array(
476             ),
478             'message' => array(
479                 'email', 'jabber', 'popup'
480             ),
482             'mnetservice' => array(
483                 'enrol'
484             ),
486             'mod' => array(
487                 'assign', 'assignment', 'book', 'chat', 'choice', 'data', 'feedback', 'folder',
488                 'forum', 'glossary', 'imscp', 'label', 'lesson', 'lti', 'page',
489                 'quiz', 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop'
490             ),
492             'plagiarism' => array(
493             ),
495             'portfolio' => array(
496                 'boxnet', 'download', 'flickr', 'googledocs', 'mahara', 'picasa'
497             ),
499             'profilefield' => array(
500                 'checkbox', 'datetime', 'menu', 'text', 'textarea'
501             ),
503             'qbehaviour' => array(
504                 'adaptive', 'adaptivenopenalty', 'deferredcbm',
505                 'deferredfeedback', 'immediatecbm', 'immediatefeedback',
506                 'informationitem', 'interactive', 'interactivecountback',
507                 'manualgraded', 'missing'
508             ),
510             'qformat' => array(
511                 'aiken', 'blackboard', 'blackboard_six', 'examview', 'gift',
512                 'learnwise', 'missingword', 'multianswer', 'webct',
513                 'xhtml', 'xml'
514             ),
516             'qtype' => array(
517                 'calculated', 'calculatedmulti', 'calculatedsimple',
518                 'description', 'essay', 'match', 'missingtype', 'multianswer',
519                 'multichoice', 'numerical', 'random', 'randomsamatch',
520                 'shortanswer', 'truefalse'
521             ),
523             'quiz' => array(
524                 'grading', 'overview', 'responses', 'statistics'
525             ),
527             'quizaccess' => array(
528                 'delaybetweenattempts', 'ipaddress', 'numattempts', 'openclosedate',
529                 'password', 'safebrowser', 'securewindow', 'timelimit'
530             ),
532             'report' => array(
533                 'backups', 'completion', 'configlog', 'courseoverview',
534                 'log', 'loglive', 'outline', 'participation', 'progress', 'questioninstances', 'security', 'stats'
535             ),
537             'repository' => array(
538                 'alfresco', 'boxnet', 'coursefiles', 'dropbox', 'equella', 'filesystem',
539                 'flickr', 'flickr_public', 'googledocs', 'local', 'merlot',
540                 'picasa', 'recent', 's3', 'upload', 'url', 'user', 'webdav',
541                 'wikimedia', 'youtube'
542             ),
544             'scormreport' => array(
545                 'basic',
546                 'interactions',
547                 'graphs'
548             ),
550             'tinymce' => array(
551                 'dragmath', 'moodleemoticon', 'moodleimage', 'moodlemedia', 'moodlenolink', 'spellchecker',
552             ),
554             'theme' => array(
555                 'afterburner', 'anomaly', 'arialist', 'base', 'binarius',
556                 'boxxie', 'brick', 'canvas', 'formal_white', 'formfactor',
557                 'fusion', 'leatherbound', 'magazine', 'mymobile', 'nimble',
558                 'nonzero', 'overlay', 'serenity', 'sky_high', 'splash',
559                 'standard', 'standardold'
560             ),
562             'tool' => array(
563                 'assignmentupgrade', 'capability', 'customlang', 'dbtransfer', 'generator',
564                 'health', 'innodb', 'langimport', 'multilangupgrade', 'phpunit', 'profiling',
565                 'qeupgradehelper', 'replace', 'spamcleaner', 'timezoneimport', 'unittest',
566                 'uploaduser', 'unsuproles', 'xmldb'
567             ),
569             'webservice' => array(
570                 'amf', 'rest', 'soap', 'xmlrpc'
571             ),
573             'workshopallocation' => array(
574                 'manual', 'random', 'scheduled'
575             ),
577             'workshopeval' => array(
578                 'best'
579             ),
581             'workshopform' => array(
582                 'accumulative', 'comments', 'numerrors', 'rubric'
583             )
584         );
586         if (isset($standard_plugins[$type])) {
587             return $standard_plugins[$type];
588         } else {
589             return false;
590         }
591     }
593     /**
594      * Reorders plugin types into a sequence to be displayed
595      *
596      * For technical reasons, plugin types returned by {@link get_plugin_types()} are
597      * in a certain order that does not need to fit the expected order for the display.
598      * Particularly, activity modules should be displayed first as they represent the
599      * real heart of Moodle. They should be followed by other plugin types that are
600      * used to build the courses (as that is what one expects from LMS). After that,
601      * other supportive plugin types follow.
602      *
603      * @param array $types associative array
604      * @return array same array with altered order of items
605      */
606     protected function reorder_plugin_types(array $types) {
607         $fix = array(
608             'mod'        => $types['mod'],
609             'block'      => $types['block'],
610             'qtype'      => $types['qtype'],
611             'qbehaviour' => $types['qbehaviour'],
612             'qformat'    => $types['qformat'],
613             'filter'     => $types['filter'],
614             'enrol'      => $types['enrol'],
615         );
616         foreach ($types as $type => $path) {
617             if (!isset($fix[$type])) {
618                 $fix[$type] = $path;
619             }
620         }
621         return $fix;
622     }
626 /**
627  * General exception thrown by the {@link available_update_checker} class
628  */
629 class available_update_checker_exception extends moodle_exception {
631     /**
632      * @param string $errorcode exception description identifier
633      * @param mixed $debuginfo debugging data to display
634      */
635     public function __construct($errorcode, $debuginfo=null) {
636         parent::__construct($errorcode, 'core_plugin', '', null, print_r($debuginfo, true));
637     }
641 /**
642  * Singleton class that handles checking for available updates
643  */
644 class available_update_checker {
646     /** @var available_update_checker holds the singleton instance */
647     protected static $singletoninstance;
648     /** @var null|int the timestamp of when the most recent response was fetched */
649     protected $recentfetch = null;
650     /** @var null|array the recent response from the update notification provider */
651     protected $recentresponse = null;
652     /** @var null|string the numerical version of the local Moodle code */
653     protected $currentversion = null;
654     /** @var null|string the release info of the local Moodle code */
655     protected $currentrelease = null;
656     /** @var null|string branch of the local Moodle code */
657     protected $currentbranch = null;
658     /** @var array of (string)frankestyle => (string)version list of additional plugins deployed at this site */
659     protected $currentplugins = array();
661     /**
662      * Direct initiation not allowed, use the factory method {@link self::instance()}
663      */
664     protected function __construct() {
665     }
667     /**
668      * Sorry, this is singleton
669      */
670     protected function __clone() {
671     }
673     /**
674      * Factory method for this class
675      *
676      * @return available_update_checker the singleton instance
677      */
678     public static function instance() {
679         if (is_null(self::$singletoninstance)) {
680             self::$singletoninstance = new self();
681         }
682         return self::$singletoninstance;
683     }
685     /**
686      * Reset any caches
687      * @param bool $phpunitreset
688      */
689     public static function reset_caches($phpunitreset = false) {
690         if ($phpunitreset) {
691             self::$singletoninstance = null;
692         }
693     }
695     /**
696      * Returns the timestamp of the last execution of {@link fetch()}
697      *
698      * @return int|null null if it has never been executed or we don't known
699      */
700     public function get_last_timefetched() {
702         $this->restore_response();
704         if (!empty($this->recentfetch)) {
705             return $this->recentfetch;
707         } else {
708             return null;
709         }
710     }
712     /**
713      * Fetches the available update status from the remote site
714      *
715      * @throws available_update_checker_exception
716      */
717     public function fetch() {
718         $response = $this->get_response();
719         $this->validate_response($response);
720         $this->store_response($response);
721     }
723     /**
724      * Returns the available update information for the given component
725      *
726      * This method returns null if the most recent response does not contain any information
727      * about it. The returned structure is an array of available updates for the given
728      * component. Each update info is an object with at least one property called
729      * 'version'. Other possible properties are 'release', 'maturity', 'url' and 'downloadurl'.
730      *
731      * For the 'core' component, the method returns real updates only (those with higher version).
732      * For all other components, the list of all known remote updates is returned and the caller
733      * (usually the {@link plugin_manager}) is supposed to make the actual comparison of versions.
734      *
735      * @param string $component frankenstyle
736      * @param array $options with supported keys 'minmaturity' and/or 'notifybuilds'
737      * @return null|array null or array of available_update_info objects
738      */
739     public function get_update_info($component, array $options = array()) {
741         if (!isset($options['minmaturity'])) {
742             $options['minmaturity'] = 0;
743         }
745         if (!isset($options['notifybuilds'])) {
746             $options['notifybuilds'] = false;
747         }
749         if ($component == 'core') {
750             $this->load_current_environment();
751         }
753         $this->restore_response();
755         if (empty($this->recentresponse['updates'][$component])) {
756             return null;
757         }
759         $updates = array();
760         foreach ($this->recentresponse['updates'][$component] as $info) {
761             $update = new available_update_info($component, $info);
762             if (isset($update->maturity) and ($update->maturity < $options['minmaturity'])) {
763                 continue;
764             }
765             if ($component == 'core') {
766                 if ($update->version <= $this->currentversion) {
767                     continue;
768                 }
769                 if (empty($options['notifybuilds']) and $this->is_same_release($update->release)) {
770                     continue;
771                 }
772             }
773             $updates[] = $update;
774         }
776         if (empty($updates)) {
777             return null;
778         }
780         return $updates;
781     }
783     /**
784      * The method being run via cron.php
785      */
786     public function cron() {
787         global $CFG;
789         if (!$this->cron_autocheck_enabled()) {
790             $this->cron_mtrace('Automatic check for available updates not enabled, skipping.');
791             return;
792         }
794         $now = $this->cron_current_timestamp();
796         if ($this->cron_has_fresh_fetch($now)) {
797             $this->cron_mtrace('Recently fetched info about available updates is still fresh enough, skipping.');
798             return;
799         }
801         if ($this->cron_has_outdated_fetch($now)) {
802             $this->cron_mtrace('Outdated or missing info about available updates, forced fetching ... ', '');
803             $this->cron_execute();
804             return;
805         }
807         $offset = $this->cron_execution_offset();
808         $start = mktime(1, 0, 0, date('n', $now), date('j', $now), date('Y', $now)); // 01:00 AM today local time
809         if ($now > $start + $offset) {
810             $this->cron_mtrace('Regular daily check for available updates ... ', '');
811             $this->cron_execute();
812             return;
813         }
814     }
816     /// end of public API //////////////////////////////////////////////////////
818     /**
819      * Makes cURL request to get data from the remote site
820      *
821      * @return string raw request result
822      * @throws available_update_checker_exception
823      */
824     protected function get_response() {
825         global $CFG;
826         require_once($CFG->libdir.'/filelib.php');
828         $curl = new curl(array('proxy' => true));
829         $response = $curl->post($this->prepare_request_url(), $this->prepare_request_params(), $this->prepare_request_options());
830         $curlerrno = $curl->get_errno();
831         if (!empty($curlerrno)) {
832             throw new available_update_checker_exception('err_response_curl', 'cURL error '.$curlerrno.': '.$curl->error);
833         }
834         $curlinfo = $curl->get_info();
835         if ($curlinfo['http_code'] != 200) {
836             throw new available_update_checker_exception('err_response_http_code', $curlinfo['http_code']);
837         }
838         return $response;
839     }
841     /**
842      * Makes sure the response is valid, has correct API format etc.
843      *
844      * @param string $response raw response as returned by the {@link self::get_response()}
845      * @throws available_update_checker_exception
846      */
847     protected function validate_response($response) {
849         $response = $this->decode_response($response);
851         if (empty($response)) {
852             throw new available_update_checker_exception('err_response_empty');
853         }
855         if (empty($response['status']) or $response['status'] !== 'OK') {
856             throw new available_update_checker_exception('err_response_status', $response['status']);
857         }
859         if (empty($response['apiver']) or $response['apiver'] !== '1.1') {
860             throw new available_update_checker_exception('err_response_format_version', $response['apiver']);
861         }
863         if (empty($response['forbranch']) or $response['forbranch'] !== moodle_major_version(true)) {
864             throw new available_update_checker_exception('err_response_target_version', $response['forbranch']);
865         }
866     }
868     /**
869      * Decodes the raw string response from the update notifications provider
870      *
871      * @param string $response as returned by {@link self::get_response()}
872      * @return array decoded response structure
873      */
874     protected function decode_response($response) {
875         return json_decode($response, true);
876     }
878     /**
879      * Stores the valid fetched response for later usage
880      *
881      * This implementation uses the config_plugins table as the permanent storage.
882      *
883      * @param string $response raw valid data returned by {@link self::get_response()}
884      */
885     protected function store_response($response) {
887         set_config('recentfetch', time(), 'core_plugin');
888         set_config('recentresponse', $response, 'core_plugin');
890         $this->restore_response(true);
891     }
893     /**
894      * Loads the most recent raw response record we have fetched
895      *
896      * After this method is called, $this->recentresponse is set to an array. If the
897      * array is empty, then either no data have been fetched yet or the fetched data
898      * do not have expected format (and thence they are ignored and a debugging
899      * message is displayed).
900      *
901      * This implementation uses the config_plugins table as the permanent storage.
902      *
903      * @param bool $forcereload reload even if it was already loaded
904      */
905     protected function restore_response($forcereload = false) {
907         if (!$forcereload and !is_null($this->recentresponse)) {
908             // we already have it, nothing to do
909             return;
910         }
912         $config = get_config('core_plugin');
914         if (!empty($config->recentresponse) and !empty($config->recentfetch)) {
915             try {
916                 $this->validate_response($config->recentresponse);
917                 $this->recentfetch = $config->recentfetch;
918                 $this->recentresponse = $this->decode_response($config->recentresponse);
919             } catch (available_update_checker_exception $e) {
920                 // The server response is not valid. Behave as if no data were fetched yet.
921                 // This may happen when the most recent update info (cached locally) has been
922                 // fetched with the previous branch of Moodle (like during an upgrade from 2.x
923                 // to 2.y) or when the API of the response has changed.
924                 $this->recentresponse = array();
925             }
927         } else {
928             $this->recentresponse = array();
929         }
930     }
932     /**
933      * Compares two raw {@link $recentresponse} records and returns the list of changed updates
934      *
935      * This method is used to populate potential update info to be sent to site admins.
936      *
937      * @param array $old
938      * @param array $new
939      * @throws available_update_checker_exception
940      * @return array parts of $new['updates'] that have changed
941      */
942     protected function compare_responses(array $old, array $new) {
944         if (empty($new)) {
945             return array();
946         }
948         if (!array_key_exists('updates', $new)) {
949             throw new available_update_checker_exception('err_response_format');
950         }
952         if (empty($old)) {
953             return $new['updates'];
954         }
956         if (!array_key_exists('updates', $old)) {
957             throw new available_update_checker_exception('err_response_format');
958         }
960         $changes = array();
962         foreach ($new['updates'] as $newcomponent => $newcomponentupdates) {
963             if (empty($old['updates'][$newcomponent])) {
964                 $changes[$newcomponent] = $newcomponentupdates;
965                 continue;
966             }
967             foreach ($newcomponentupdates as $newcomponentupdate) {
968                 $inold = false;
969                 foreach ($old['updates'][$newcomponent] as $oldcomponentupdate) {
970                     if ($newcomponentupdate['version'] == $oldcomponentupdate['version']) {
971                         $inold = true;
972                     }
973                 }
974                 if (!$inold) {
975                     if (!isset($changes[$newcomponent])) {
976                         $changes[$newcomponent] = array();
977                     }
978                     $changes[$newcomponent][] = $newcomponentupdate;
979                 }
980             }
981         }
983         return $changes;
984     }
986     /**
987      * Returns the URL to send update requests to
988      *
989      * During the development or testing, you can set $CFG->alternativeupdateproviderurl
990      * to a custom URL that will be used. Otherwise the standard URL will be returned.
991      *
992      * @return string URL
993      */
994     protected function prepare_request_url() {
995         global $CFG;
997         if (!empty($CFG->config_php_settings['alternativeupdateproviderurl'])) {
998             return $CFG->config_php_settings['alternativeupdateproviderurl'];
999         } else {
1000             return 'https://download.moodle.org/api/1.1/updates.php';
1001         }
1002     }
1004     /**
1005      * Sets the properties currentversion, currentrelease, currentbranch and currentplugins
1006      *
1007      * @param bool $forcereload
1008      */
1009     protected function load_current_environment($forcereload=false) {
1010         global $CFG;
1012         if (!is_null($this->currentversion) and !$forcereload) {
1013             // nothing to do
1014             return;
1015         }
1017         $version = null;
1018         $release = null;
1020         require($CFG->dirroot.'/version.php');
1021         $this->currentversion = $version;
1022         $this->currentrelease = $release;
1023         $this->currentbranch = moodle_major_version(true);
1025         $pluginman = plugin_manager::instance();
1026         foreach ($pluginman->get_plugins() as $type => $plugins) {
1027             foreach ($plugins as $plugin) {
1028                 if (!$plugin->is_standard()) {
1029                     $this->currentplugins[$plugin->component] = $plugin->versiondisk;
1030                 }
1031             }
1032         }
1033     }
1035     /**
1036      * Returns the list of HTTP params to be sent to the updates provider URL
1037      *
1038      * @return array of (string)param => (string)value
1039      */
1040     protected function prepare_request_params() {
1041         global $CFG;
1043         $this->load_current_environment();
1044         $this->restore_response();
1046         $params = array();
1047         $params['format'] = 'json';
1049         if (isset($this->recentresponse['ticket'])) {
1050             $params['ticket'] = $this->recentresponse['ticket'];
1051         }
1053         if (isset($this->currentversion)) {
1054             $params['version'] = $this->currentversion;
1055         } else {
1056             throw new coding_exception('Main Moodle version must be already known here');
1057         }
1059         if (isset($this->currentbranch)) {
1060             $params['branch'] = $this->currentbranch;
1061         } else {
1062             throw new coding_exception('Moodle release must be already known here');
1063         }
1065         $plugins = array();
1066         foreach ($this->currentplugins as $plugin => $version) {
1067             $plugins[] = $plugin.'@'.$version;
1068         }
1069         if (!empty($plugins)) {
1070             $params['plugins'] = implode(',', $plugins);
1071         }
1073         return $params;
1074     }
1076     /**
1077      * Returns the list of cURL options to use when fetching available updates data
1078      *
1079      * @return array of (string)param => (string)value
1080      */
1081     protected function prepare_request_options() {
1082         global $CFG;
1084         $options = array(
1085             'CURLOPT_SSL_VERIFYHOST' => 2,      // this is the default in {@link curl} class but just in case
1086             'CURLOPT_SSL_VERIFYPEER' => true,
1087         );
1089         $cacertfile = $CFG->dataroot.'/moodleorgca.crt';
1090         if (is_readable($cacertfile)) {
1091             // Do not use CA certs provided by the operating system. Instead,
1092             // use this CA cert to verify the updates provider.
1093             $options['CURLOPT_CAINFO'] = $cacertfile;
1094         }
1096         return $options;
1097     }
1099     /**
1100      * Returns the current timestamp
1101      *
1102      * @return int the timestamp
1103      */
1104     protected function cron_current_timestamp() {
1105         return time();
1106     }
1108     /**
1109      * Output cron debugging info
1110      *
1111      * @see mtrace()
1112      * @param string $msg output message
1113      * @param string $eol end of line
1114      */
1115     protected function cron_mtrace($msg, $eol = PHP_EOL) {
1116         mtrace($msg, $eol);
1117     }
1119     /**
1120      * Decide if the autocheck feature is disabled in the server setting
1121      *
1122      * @return bool true if autocheck enabled, false if disabled
1123      */
1124     protected function cron_autocheck_enabled() {
1125         global $CFG;
1127         if (empty($CFG->updateautocheck)) {
1128             return false;
1129         } else {
1130             return true;
1131         }
1132     }
1134     /**
1135      * Decide if the recently fetched data are still fresh enough
1136      *
1137      * @param int $now current timestamp
1138      * @return bool true if no need to re-fetch, false otherwise
1139      */
1140     protected function cron_has_fresh_fetch($now) {
1141         $recent = $this->get_last_timefetched();
1143         if (empty($recent)) {
1144             return false;
1145         }
1147         if ($now < $recent) {
1148             $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1149             return true;
1150         }
1152         if ($now - $recent > 24 * HOURSECS) {
1153             return false;
1154         }
1156         return true;
1157     }
1159     /**
1160      * Decide if the fetch is outadated or even missing
1161      *
1162      * @param int $now current timestamp
1163      * @return bool false if no need to re-fetch, true otherwise
1164      */
1165     protected function cron_has_outdated_fetch($now) {
1166         $recent = $this->get_last_timefetched();
1168         if (empty($recent)) {
1169             return true;
1170         }
1172         if ($now < $recent) {
1173             $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1174             return false;
1175         }
1177         if ($now - $recent > 48 * HOURSECS) {
1178             return true;
1179         }
1181         return false;
1182     }
1184     /**
1185      * Returns the cron execution offset for this site
1186      *
1187      * The main {@link self::cron()} is supposed to run every night in some random time
1188      * between 01:00 and 06:00 AM (local time). The exact moment is defined by so called
1189      * execution offset, that is the amount of time after 01:00 AM. The offset value is
1190      * initially generated randomly and then used consistently at the site. This way, the
1191      * regular checks against the download.moodle.org server are spread in time.
1192      *
1193      * @return int the offset number of seconds from range 1 sec to 5 hours
1194      */
1195     protected function cron_execution_offset() {
1196         global $CFG;
1198         if (empty($CFG->updatecronoffset)) {
1199             set_config('updatecronoffset', rand(1, 5 * HOURSECS));
1200         }
1202         return $CFG->updatecronoffset;
1203     }
1205     /**
1206      * Fetch available updates info and eventually send notification to site admins
1207      */
1208     protected function cron_execute() {
1210         try {
1211             $this->restore_response();
1212             $previous = $this->recentresponse;
1213             $this->fetch();
1214             $this->restore_response(true);
1215             $current = $this->recentresponse;
1216             $changes = $this->compare_responses($previous, $current);
1217             $notifications = $this->cron_notifications($changes);
1218             $this->cron_notify($notifications);
1219             $this->cron_mtrace('done');
1220         } catch (available_update_checker_exception $e) {
1221             $this->cron_mtrace('FAILED!');
1222         }
1223     }
1225     /**
1226      * Given the list of changes in available updates, pick those to send to site admins
1227      *
1228      * @param array $changes as returned by {@link self::compare_responses()}
1229      * @return array of available_update_info objects to send to site admins
1230      */
1231     protected function cron_notifications(array $changes) {
1232         global $CFG;
1234         $notifications = array();
1235         $pluginman = plugin_manager::instance();
1236         $plugins = $pluginman->get_plugins(true);
1238         foreach ($changes as $component => $componentchanges) {
1239             if (empty($componentchanges)) {
1240                 continue;
1241             }
1242             $componentupdates = $this->get_update_info($component,
1243                 array('minmaturity' => $CFG->updateminmaturity, 'notifybuilds' => $CFG->updatenotifybuilds));
1244             if (empty($componentupdates)) {
1245                 continue;
1246             }
1247             // notify only about those $componentchanges that are present in $componentupdates
1248             // to respect the preferences
1249             foreach ($componentchanges as $componentchange) {
1250                 foreach ($componentupdates as $componentupdate) {
1251                     if ($componentupdate->version == $componentchange['version']) {
1252                         if ($component == 'core') {
1253                             // in case of 'core' this is enough, we already know that the
1254                             // $componentupdate is a real update with higher version
1255                             $notifications[] = $componentupdate;
1256                         } else {
1257                             // use the plugin_manager to check if the reported $componentchange
1258                             // is a real update with higher version. such a real update must be
1259                             // present in the 'availableupdates' property of one of the component's
1260                             // available_update_info object
1261                             list($plugintype, $pluginname) = normalize_component($component);
1262                             if (!empty($plugins[$plugintype][$pluginname]->availableupdates)) {
1263                                 foreach ($plugins[$plugintype][$pluginname]->availableupdates as $availableupdate) {
1264                                     if ($availableupdate->version == $componentchange['version']) {
1265                                         $notifications[] = $componentupdate;
1266                                     }
1267                                 }
1268                             }
1269                         }
1270                     }
1271                 }
1272             }
1273         }
1275         return $notifications;
1276     }
1278     /**
1279      * Sends the given notifications to site admins via messaging API
1280      *
1281      * @param array $notifications array of available_update_info objects to send
1282      */
1283     protected function cron_notify(array $notifications) {
1284         global $CFG;
1286         if (empty($notifications)) {
1287             return;
1288         }
1290         $admins = get_admins();
1292         if (empty($admins)) {
1293             return;
1294         }
1296         $this->cron_mtrace('sending notifications ... ', '');
1298         $text = get_string('updatenotifications', 'core_admin') . PHP_EOL;
1299         $html = html_writer::tag('h1', get_string('updatenotifications', 'core_admin')) . PHP_EOL;
1301         $coreupdates = array();
1302         $pluginupdates = array();
1304         foreach ($notifications as $notification) {
1305             if ($notification->component == 'core') {
1306                 $coreupdates[] = $notification;
1307             } else {
1308                 $pluginupdates[] = $notification;
1309             }
1310         }
1312         if (!empty($coreupdates)) {
1313             $text .= PHP_EOL . get_string('updateavailable', 'core_admin') . PHP_EOL;
1314             $html .= html_writer::tag('h2', get_string('updateavailable', 'core_admin')) . PHP_EOL;
1315             $html .= html_writer::start_tag('ul') . PHP_EOL;
1316             foreach ($coreupdates as $coreupdate) {
1317                 $html .= html_writer::start_tag('li');
1318                 if (isset($coreupdate->release)) {
1319                     $text .= get_string('updateavailable_release', 'core_admin', $coreupdate->release);
1320                     $html .= html_writer::tag('strong', get_string('updateavailable_release', 'core_admin', $coreupdate->release));
1321                 }
1322                 if (isset($coreupdate->version)) {
1323                     $text .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
1324                     $html .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
1325                 }
1326                 if (isset($coreupdate->maturity)) {
1327                     $text .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
1328                     $html .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
1329                 }
1330                 $text .= PHP_EOL;
1331                 $html .= html_writer::end_tag('li') . PHP_EOL;
1332             }
1333             $text .= PHP_EOL;
1334             $html .= html_writer::end_tag('ul') . PHP_EOL;
1336             $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/index.php');
1337             $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
1338             $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/index.php', $CFG->wwwroot.'/'.$CFG->admin.'/index.php'));
1339             $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
1340         }
1342         if (!empty($pluginupdates)) {
1343             $text .= PHP_EOL . get_string('updateavailableforplugin', 'core_admin') . PHP_EOL;
1344             $html .= html_writer::tag('h2', get_string('updateavailableforplugin', 'core_admin')) . PHP_EOL;
1346             $html .= html_writer::start_tag('ul') . PHP_EOL;
1347             foreach ($pluginupdates as $pluginupdate) {
1348                 $html .= html_writer::start_tag('li');
1349                 $text .= get_string('pluginname', $pluginupdate->component);
1350                 $html .= html_writer::tag('strong', get_string('pluginname', $pluginupdate->component));
1352                 $text .= ' ('.$pluginupdate->component.')';
1353                 $html .= ' ('.$pluginupdate->component.')';
1355                 $text .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
1356                 $html .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
1358                 $text .= PHP_EOL;
1359                 $html .= html_writer::end_tag('li') . PHP_EOL;
1360             }
1361             $text .= PHP_EOL;
1362             $html .= html_writer::end_tag('ul') . PHP_EOL;
1364             $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php');
1365             $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
1366             $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/plugins.php', $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php'));
1367             $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
1368         }
1370         $a = array('siteurl' => $CFG->wwwroot);
1371         $text .= get_string('updatenotificationfooter', 'core_admin', $a) . PHP_EOL;
1372         $a = array('siteurl' => html_writer::link($CFG->wwwroot, $CFG->wwwroot));
1373         $html .= html_writer::tag('footer', html_writer::tag('p', get_string('updatenotificationfooter', 'core_admin', $a),
1374             array('style' => 'font-size:smaller; color:#333;')));
1376         foreach ($admins as $admin) {
1377             $message = new stdClass();
1378             $message->component         = 'moodle';
1379             $message->name              = 'availableupdate';
1380             $message->userfrom          = get_admin();
1381             $message->userto            = $admin;
1382             $message->subject           = get_string('updatenotificationsubject', 'core_admin', array('siteurl' => $CFG->wwwroot));
1383             $message->fullmessage       = $text;
1384             $message->fullmessageformat = FORMAT_PLAIN;
1385             $message->fullmessagehtml   = $html;
1386             $message->smallmessage      = get_string('updatenotifications', 'core_admin');
1387             $message->notification      = 1;
1388             message_send($message);
1389         }
1390     }
1392     /**
1393      * Compare two release labels and decide if they are the same
1394      *
1395      * @param string $remote release info of the available update
1396      * @param null|string $local release info of the local code, defaults to $release defined in version.php
1397      * @return boolean true if the releases declare the same minor+major version
1398      */
1399     protected function is_same_release($remote, $local=null) {
1401         if (is_null($local)) {
1402             $this->load_current_environment();
1403             $local = $this->currentrelease;
1404         }
1406         $pattern = '/^([0-9\.\+]+)([^(]*)/';
1408         preg_match($pattern, $remote, $remotematches);
1409         preg_match($pattern, $local, $localmatches);
1411         $remotematches[1] = str_replace('+', '', $remotematches[1]);
1412         $localmatches[1] = str_replace('+', '', $localmatches[1]);
1414         if ($remotematches[1] === $localmatches[1] and rtrim($remotematches[2]) === rtrim($localmatches[2])) {
1415             return true;
1416         } else {
1417             return false;
1418         }
1419     }
1423 /**
1424  * Defines the structure of objects returned by {@link available_update_checker::get_update_info()}
1425  */
1426 class available_update_info {
1428     /** @var string frankenstyle component name */
1429     public $component;
1430     /** @var int the available version of the component */
1431     public $version;
1432     /** @var string|null optional release name */
1433     public $release = null;
1434     /** @var int|null optional maturity info, eg {@link MATURITY_STABLE} */
1435     public $maturity = null;
1436     /** @var string|null optional URL of a page with more info about the update */
1437     public $url = null;
1438     /** @var string|null optional URL of a ZIP package that can be downloaded and installed */
1439     public $download = null;
1440     /** @var string|null of self::download is set, then this must be the MD5 hash of the ZIP */
1441     public $downloadmd5 = null;
1443     /**
1444      * Creates new instance of the class
1445      *
1446      * The $info array must provide at least the 'version' value and optionally all other
1447      * values to populate the object's properties.
1448      *
1449      * @param string $name the frankenstyle component name
1450      * @param array $info associative array with other properties
1451      */
1452     public function __construct($name, array $info) {
1453         $this->component = $name;
1454         foreach ($info as $k => $v) {
1455             if (property_exists('available_update_info', $k) and $k != 'component') {
1456                 $this->$k = $v;
1457             }
1458         }
1459     }
1463 /**
1464  * Implements a communication bridge to the mdeploy.php utility
1465  */
1466 class available_update_deployer {
1468     const HTTP_PARAM_PREFIX     = 'updteautodpldata_';  // Hey, even Google has not heard of such a prefix! So it MUST be safe :-p
1469     const HTTP_PARAM_CHECKER    = 'datapackagesize';    // Name of the parameter that holds the number of items in the received data items
1471     /** @var available_update_deployer holds the singleton instance */
1472     protected static $singletoninstance;
1473     /** @var moodle_url URL of a page that includes the deployer UI */
1474     protected $callerurl;
1475     /** @var moodle_url URL to return after the deployment */
1476     protected $returnurl;
1478     /**
1479      * Direct instantiation not allowed, use the factory method {@link self::instance()}
1480      */
1481     protected function __construct() {
1482     }
1484     /**
1485      * Sorry, this is singleton
1486      */
1487     protected function __clone() {
1488     }
1490     /**
1491      * Factory method for this class
1492      *
1493      * @return available_update_deployer the singleton instance
1494      */
1495     public static function instance() {
1496         if (is_null(self::$singletoninstance)) {
1497             self::$singletoninstance = new self();
1498         }
1499         return self::$singletoninstance;
1500     }
1502     /**
1503      * Reset caches used by this script
1504      *
1505      * @param bool $phpunitreset is this called as a part of PHPUnit reset?
1506      */
1507     public static function reset_caches($phpunitreset = false) {
1508         if ($phpunitreset) {
1509             self::$singletoninstance = null;
1510         }
1511     }
1513     /**
1514      * Is automatic deployment enabled?
1515      *
1516      * @return bool
1517      */
1518     public function enabled() {
1519         global $CFG;
1521         if (!empty($CFG->disableupdateautodeploy)) {
1522             // The feature is prohibited via config.php
1523             return false;
1524         }
1526         return get_config('updateautodeploy');
1527     }
1529     /**
1530      * Sets some base properties of the class to make it usable.
1531      *
1532      * @param moodle_url $callerurl the base URL of a script that will handle the class'es form data
1533      * @param moodle_url $returnurl the final URL to return to when the deployment is finished
1534      */
1535     public function initialize(moodle_url $callerurl, moodle_url $returnurl) {
1537         if (!$this->enabled()) {
1538             throw new coding_exception('Unable to initialize the deployer, the feature is not enabled.');
1539         }
1541         $this->callerurl = $callerurl;
1542         $this->returnurl = $returnurl;
1543     }
1545     /**
1546      * Has the deployer been initialized?
1547      *
1548      * Initialized deployer means that the following properties were set:
1549      * callerurl, returnurl
1550      *
1551      * @return bool
1552      */
1553     public function initialized() {
1555         if (!$this->enabled()) {
1556             return false;
1557         }
1559         if (empty($this->callerurl)) {
1560             return false;
1561         }
1563         if (empty($this->returnurl)) {
1564             return false;
1565         }
1567         return true;
1568     }
1570     /**
1571      * Returns a list of reasons why the deployment can not happen
1572      *
1573      * If the returned array is empty, the deployment seems to be possible. The returned
1574      * structure is an associative array with keys representing individual impediments.
1575      * Possible keys are: missingdownloadurl, missingdownloadmd5, notwritable.
1576      *
1577      * @param available_update_info $info
1578      * @return array
1579      */
1580     public function deployment_impediments(available_update_info $info) {
1582         $impediments = array();
1584         if (empty($info->download)) {
1585             $impediments['missingdownloadurl'] = true;
1586         }
1588         if (empty($info->downloadmd5)) {
1589             $impediments['missingdownloadmd5'] = true;
1590         }
1592         if (!empty($info->download) and !$this->update_downloadable($info->download)) {
1593             $impediments['notdownloadable'] = true;
1594         }
1596         if (!$this->component_writable($info->component)) {
1597             $impediments['notwritable'] = true;
1598         }
1600         return $impediments;
1601     }
1603     /**
1604      * Check to see if the current version of the plugin seems to be a checkout of an external repository.
1605      *
1606      * @param available_update_info $info
1607      * @return false|string
1608      */
1609     public function plugin_external_source(available_update_info $info) {
1611         $paths = get_plugin_types(true);
1612         list($plugintype, $pluginname) = normalize_component($info->component);
1613         $pluginroot = $paths[$plugintype].'/'.$pluginname;
1615         if (is_dir($pluginroot.'/.git')) {
1616             return 'git';
1617         }
1619         if (is_dir($pluginroot.'/CVS')) {
1620             return 'cvs';
1621         }
1623         if (is_dir($pluginroot.'/.svn')) {
1624             return 'svn';
1625         }
1627         return false;
1628     }
1630     /**
1631      * Prepares a renderable widget to confirm installation of an available update.
1632      *
1633      * @param available_update_info $info component version to deploy
1634      * @return renderable
1635      */
1636     public function make_confirm_widget(available_update_info $info) {
1638         if (!$this->initialized()) {
1639             throw new coding_exception('Illegal method call - deployer not initialized.');
1640         }
1642         $params = $this->data_to_params(array(
1643             'updateinfo' => (array)$info,   // see http://www.php.net/manual/en/language.types.array.php#language.types.array.casting
1644         ));
1646         $widget = new single_button(
1647             new moodle_url($this->callerurl, $params),
1648             get_string('updateavailableinstall', 'core_admin'),
1649             'post'
1650         );
1652         return $widget;
1653     }
1655     /**
1656      * Prepares a renderable widget to execute installation of an available update.
1657      *
1658      * @param available_update_info $info component version to deploy
1659      * @return renderable
1660      */
1661     public function make_execution_widget(available_update_info $info) {
1662         global $CFG;
1664         if (!$this->initialized()) {
1665             throw new coding_exception('Illegal method call - deployer not initialized.');
1666         }
1668         $pluginrootpaths = get_plugin_types(true);
1670         list($plugintype, $pluginname) = normalize_component($info->component);
1672         if (empty($pluginrootpaths[$plugintype])) {
1673             throw new coding_exception('Unknown plugin type root location', $plugintype);
1674         }
1676         list($passfile, $password) = $this->prepare_authorization();
1678         $upgradeurl = new moodle_url('/admin');
1680         $params = array(
1681             'upgrade' => true,
1682             'type' => $plugintype,
1683             'name' => $pluginname,
1684             'typeroot' => $pluginrootpaths[$plugintype],
1685             'package' => $info->download,
1686             'md5' => $info->downloadmd5,
1687             'dataroot' => $CFG->dataroot,
1688             'dirroot' => $CFG->dirroot,
1689             'passfile' => $passfile,
1690             'password' => $password,
1691             'returnurl' => $upgradeurl->out(true),
1692         );
1694         if (!empty($CFG->proxyhost)) {
1695             // Beware - we should call just !is_proxybypass() here. But currently, our cURL wrapper
1696             // class does not do it. So, to have consistent behaviour, we pass proxy setting
1697             // regardless the $CFG->proxybypass setting. Once the {@link curl} class is fixed,
1698             // the condition should be amended.
1699             if (true or !is_proxybypass($info->download)) {
1700                 if (empty($CFG->proxyport)) {
1701                     $params['proxy'] = $CFG->proxyhost;
1702                 } else {
1703                     $params['proxy'] = $CFG->proxyhost.':'.$CFG->proxyport;
1704                 }
1706                 if (!empty($CFG->proxyuser) and !empty($CFG->proxypassword)) {
1707                     $params['proxyuserpwd'] = $CFG->proxyuser.':'.$CFG->proxypassword;
1708                 }
1710                 if (!empty($CFG->proxytype)) {
1711                     $params['proxytype'] = $CFG->proxytype;
1712                 }
1713             }
1714         }
1716         $widget = new single_button(
1717             new moodle_url('/mdeploy.php', $params),
1718             get_string('updateavailableinstall', 'core_admin'),
1719             'post'
1720         );
1722         return $widget;
1723     }
1725     /**
1726      * Returns array of data objects passed to this tool.
1727      *
1728      * @return array
1729      */
1730     public function submitted_data() {
1732         $data = $this->params_to_data($_POST);
1734         if (empty($data) or empty($data[self::HTTP_PARAM_CHECKER])) {
1735             return false;
1736         }
1738         if (!empty($data['updateinfo']) and is_object($data['updateinfo'])) {
1739             $updateinfo = $data['updateinfo'];
1740             if (!empty($updateinfo->component) and !empty($updateinfo->version)) {
1741                 $data['updateinfo'] = new available_update_info($updateinfo->component, (array)$updateinfo);
1742             }
1743         }
1745         if (!empty($data['callerurl'])) {
1746             $data['callerurl'] = new moodle_url($data['callerurl']);
1747         }
1749         if (!empty($data['returnurl'])) {
1750             $data['returnurl'] = new moodle_url($data['returnurl']);
1751         }
1753         return $data;
1754     }
1756     /**
1757      * Handles magic getters and setters for protected properties.
1758      *
1759      * @param string $name method name, e.g. set_returnurl()
1760      * @param array $arguments arguments to be passed to the array
1761      */
1762     public function __call($name, array $arguments = array()) {
1764         if (substr($name, 0, 4) === 'set_') {
1765             $property = substr($name, 4);
1766             if (empty($property)) {
1767                 throw new coding_exception('Invalid property name (empty)');
1768             }
1769             if (empty($arguments)) {
1770                 $arguments = array(true); // Default value for flag-like properties.
1771             }
1772             // Make sure it is a protected property.
1773             $isprotected = false;
1774             $reflection = new ReflectionObject($this);
1775             foreach ($reflection->getProperties(ReflectionProperty::IS_PROTECTED) as $reflectionproperty) {
1776                 if ($reflectionproperty->getName() === $property) {
1777                     $isprotected = true;
1778                     break;
1779                 }
1780             }
1781             if (!$isprotected) {
1782                 throw new coding_exception('Unable to set property - it does not exist or it is not protected');
1783             }
1784             $value = reset($arguments);
1785             $this->$property = $value;
1786             return;
1787         }
1789         if (substr($name, 0, 4) === 'get_') {
1790             $property = substr($name, 4);
1791             if (empty($property)) {
1792                 throw new coding_exception('Invalid property name (empty)');
1793             }
1794             if (!empty($arguments)) {
1795                 throw new coding_exception('No parameter expected');
1796             }
1797             // Make sure it is a protected property.
1798             $isprotected = false;
1799             $reflection = new ReflectionObject($this);
1800             foreach ($reflection->getProperties(ReflectionProperty::IS_PROTECTED) as $reflectionproperty) {
1801                 if ($reflectionproperty->getName() === $property) {
1802                     $isprotected = true;
1803                     break;
1804                 }
1805             }
1806             if (!$isprotected) {
1807                 throw new coding_exception('Unable to get property - it does not exist or it is not protected');
1808             }
1809             return $this->$property;
1810         }
1811     }
1813     /**
1814      * Generates a random token and stores it in a file in moodledata directory.
1815      *
1816      * @return array of the (string)filename and (string)password in this order
1817      */
1818     public function prepare_authorization() {
1819         global $CFG;
1821         make_upload_directory('mdeploy/auth/');
1823         $attempts = 0;
1824         $success = false;
1826         while (!$success and $attempts < 5) {
1827             $attempts++;
1829             $passfile = $this->generate_passfile();
1830             $password = $this->generate_password();
1831             $now = time();
1833             $filepath = $CFG->dataroot.'/mdeploy/auth/'.$passfile;
1835             if (!file_exists($filepath)) {
1836                 $success = file_put_contents($filepath, $password . PHP_EOL . $now . PHP_EOL, LOCK_EX);
1837             }
1838         }
1840         if ($success) {
1841             return array($passfile, $password);
1843         } else {
1844             throw new moodle_exception('unable_prepare_authorization', 'core_plugin');
1845         }
1846     }
1848     // End of external API
1850     /**
1851      * Prepares an array of HTTP parameters that can be passed to another page.
1852      *
1853      * @param array|object $data associative array or an object holding the data, data JSON-able
1854      * @return array suitable as a param for moodle_url
1855      */
1856     protected function data_to_params($data) {
1858         // Append some our own data
1859         if (!empty($this->callerurl)) {
1860             $data['callerurl'] = $this->callerurl->out(false);
1861         }
1862         if (!empty($this->callerurl)) {
1863             $data['returnurl'] = $this->returnurl->out(false);
1864         }
1866         // Finally append the count of items in the package.
1867         $data[self::HTTP_PARAM_CHECKER] = count($data);
1869         // Generate params
1870         $params = array();
1871         foreach ($data as $name => $value) {
1872             $transname = self::HTTP_PARAM_PREFIX.$name;
1873             $transvalue = json_encode($value);
1874             $params[$transname] = $transvalue;
1875         }
1877         return $params;
1878     }
1880     /**
1881      * Converts HTTP parameters passed to the script into native PHP data
1882      *
1883      * @param array $params such as $_REQUEST or $_POST
1884      * @return array data passed for this class
1885      */
1886     protected function params_to_data(array $params) {
1888         if (empty($params)) {
1889             return array();
1890         }
1892         $data = array();
1893         foreach ($params as $name => $value) {
1894             if (strpos($name, self::HTTP_PARAM_PREFIX) === 0) {
1895                 $realname = substr($name, strlen(self::HTTP_PARAM_PREFIX));
1896                 $realvalue = json_decode($value);
1897                 $data[$realname] = $realvalue;
1898             }
1899         }
1901         return $data;
1902     }
1904     /**
1905      * Returns a random string to be used as a filename of the password storage.
1906      *
1907      * @return string
1908      */
1909     protected function generate_passfile() {
1910         return clean_param(uniqid('mdeploy_', true), PARAM_FILE);
1911     }
1913     /**
1914      * Returns a random string to be used as the authorization token
1915      *
1916      * @return string
1917      */
1918     protected function generate_password() {
1919         return complex_random_string();
1920     }
1922     /**
1923      * Checks if the given component's directory is writable
1924      *
1925      * For the purpose of the deployment, the web server process has to have
1926      * write access to all files in the component's directory (recursively) and for the
1927      * directory itself.
1928      *
1929      * @see worker::move_directory_source_precheck()
1930      * @param string $component normalized component name
1931      * @return boolean
1932      */
1933     protected function component_writable($component) {
1935         list($plugintype, $pluginname) = normalize_component($component);
1937         $directory = get_plugin_directory($plugintype, $pluginname);
1939         if (is_null($directory)) {
1940             throw new coding_exception('Unknown component location', $component);
1941         }
1943         return $this->directory_writable($directory);
1944     }
1946     /**
1947      * Checks if the mdeploy.php will be able to fetch the ZIP from the given URL
1948      *
1949      * This is mainly supposed to check if the transmission over HTTPS would
1950      * work. That is, if the CA certificates are present at the server.
1951      *
1952      * @param string $downloadurl the URL of the ZIP package to download
1953      * @return bool
1954      */
1955     protected function update_downloadable($downloadurl) {
1956         global $CFG;
1958         $curloptions = array(
1959             'CURLOPT_SSL_VERIFYHOST' => 2,      // this is the default in {@link curl} class but just in case
1960             'CURLOPT_SSL_VERIFYPEER' => true,
1961         );
1963         $cacertfile = $CFG->dataroot.'/moodleorgca.crt';
1964         if (is_readable($cacertfile)) {
1965             // Do not use CA certs provided by the operating system. Instead,
1966             // use this CA cert to verify the updates provider.
1967             $curloptions['CURLOPT_CAINFO'] = $cacertfile;
1968         }
1970         $curl = new curl(array('proxy' => true));
1971         $result = $curl->head($downloadurl, $curloptions);
1972         $errno = $curl->get_errno();
1973         if (empty($errno)) {
1974             return true;
1975         } else {
1976             return false;
1977         }
1978     }
1980     /**
1981      * Checks if the directory and all its contents (recursively) is writable
1982      *
1983      * @param string $path full path to a directory
1984      * @return boolean
1985      */
1986     private function directory_writable($path) {
1988         if (!is_writable($path)) {
1989             return false;
1990         }
1992         if (is_dir($path)) {
1993             $handle = opendir($path);
1994         } else {
1995             return false;
1996         }
1998         $result = true;
2000         while ($filename = readdir($handle)) {
2001             $filepath = $path.'/'.$filename;
2003             if ($filename === '.' or $filename === '..') {
2004                 continue;
2005             }
2007             if (is_dir($filepath)) {
2008                 $result = $result && $this->directory_writable($filepath);
2010             } else {
2011                 $result = $result && is_writable($filepath);
2012             }
2013         }
2015         closedir($handle);
2017         return $result;
2018     }
2022 /**
2023  * Factory class producing required subclasses of {@link plugininfo_base}
2024  */
2025 class plugininfo_default_factory {
2027     /**
2028      * Makes a new instance of the plugininfo class
2029      *
2030      * @param string $type the plugin type, eg. 'mod'
2031      * @param string $typerootdir full path to the location of all the plugins of this type
2032      * @param string $name the plugin name, eg. 'workshop'
2033      * @param string $namerootdir full path to the location of the plugin
2034      * @param string $typeclass the name of class that holds the info about the plugin
2035      * @return plugininfo_base the instance of $typeclass
2036      */
2037     public static function make($type, $typerootdir, $name, $namerootdir, $typeclass) {
2038         $plugin              = new $typeclass();
2039         $plugin->type        = $type;
2040         $plugin->typerootdir = $typerootdir;
2041         $plugin->name        = $name;
2042         $plugin->rootdir     = $namerootdir;
2044         $plugin->init_display_name();
2045         $plugin->load_disk_version();
2046         $plugin->load_db_version();
2047         $plugin->load_required_main_version();
2048         $plugin->init_is_standard();
2050         return $plugin;
2051     }
2055 /**
2056  * Base class providing access to the information about a plugin
2057  *
2058  * @property-read string component the component name, type_name
2059  */
2060 abstract class plugininfo_base {
2062     /** @var string the plugintype name, eg. mod, auth or workshopform */
2063     public $type;
2064     /** @var string full path to the location of all the plugins of this type */
2065     public $typerootdir;
2066     /** @var string the plugin name, eg. assignment, ldap */
2067     public $name;
2068     /** @var string the localized plugin name */
2069     public $displayname;
2070     /** @var string the plugin source, one of plugin_manager::PLUGIN_SOURCE_xxx constants */
2071     public $source;
2072     /** @var fullpath to the location of this plugin */
2073     public $rootdir;
2074     /** @var int|string the version of the plugin's source code */
2075     public $versiondisk;
2076     /** @var int|string the version of the installed plugin */
2077     public $versiondb;
2078     /** @var int|float|string required version of Moodle core  */
2079     public $versionrequires;
2080     /** @var array other plugins that this one depends on, lazy-loaded by {@link get_other_required_plugins()} */
2081     public $dependencies;
2082     /** @var int number of instances of the plugin - not supported yet */
2083     public $instances;
2084     /** @var int order of the plugin among other plugins of the same type - not supported yet */
2085     public $sortorder;
2086     /** @var array|null array of {@link available_update_info} for this plugin */
2087     public $availableupdates;
2089     /**
2090      * Gathers and returns the information about all plugins of the given type
2091      *
2092      * @param string $type the name of the plugintype, eg. mod, auth or workshopform
2093      * @param string $typerootdir full path to the location of the plugin dir
2094      * @param string $typeclass the name of the actually called class
2095      * @return array of plugintype classes, indexed by the plugin name
2096      */
2097     public static function get_plugins($type, $typerootdir, $typeclass) {
2099         // get the information about plugins at the disk
2100         $plugins = get_plugin_list($type);
2101         $ondisk = array();
2102         foreach ($plugins as $pluginname => $pluginrootdir) {
2103             $ondisk[$pluginname] = plugininfo_default_factory::make($type, $typerootdir,
2104                 $pluginname, $pluginrootdir, $typeclass);
2105         }
2106         return $ondisk;
2107     }
2109     /**
2110      * Sets {@link $displayname} property to a localized name of the plugin
2111      */
2112     public function init_display_name() {
2113         if (!get_string_manager()->string_exists('pluginname', $this->component)) {
2114             $this->displayname = '[pluginname,' . $this->component . ']';
2115         } else {
2116             $this->displayname = get_string('pluginname', $this->component);
2117         }
2118     }
2120     /**
2121      * Magic method getter, redirects to read only values.
2122      *
2123      * @param string $name
2124      * @return mixed
2125      */
2126     public function __get($name) {
2127         switch ($name) {
2128             case 'component': return $this->type . '_' . $this->name;
2130             default:
2131                 debugging('Invalid plugin property accessed! '.$name);
2132                 return null;
2133         }
2134     }
2136     /**
2137      * Return the full path name of a file within the plugin.
2138      *
2139      * No check is made to see if the file exists.
2140      *
2141      * @param string $relativepath e.g. 'version.php'.
2142      * @return string e.g. $CFG->dirroot . '/mod/quiz/version.php'.
2143      */
2144     public function full_path($relativepath) {
2145         if (empty($this->rootdir)) {
2146             return '';
2147         }
2148         return $this->rootdir . '/' . $relativepath;
2149     }
2151     /**
2152      * Load the data from version.php.
2153      *
2154      * @return stdClass the object called $plugin defined in version.php
2155      */
2156     protected function load_version_php() {
2157         $versionfile = $this->full_path('version.php');
2159         $plugin = new stdClass();
2160         if (is_readable($versionfile)) {
2161             include($versionfile);
2162         }
2163         return $plugin;
2164     }
2166     /**
2167      * Sets {@link $versiondisk} property to a numerical value representing the
2168      * version of the plugin's source code.
2169      *
2170      * If the value is null after calling this method, either the plugin
2171      * does not use versioning (typically does not have any database
2172      * data) or is missing from disk.
2173      */
2174     public function load_disk_version() {
2175         $plugin = $this->load_version_php();
2176         if (isset($plugin->version)) {
2177             $this->versiondisk = $plugin->version;
2178         }
2179     }
2181     /**
2182      * Sets {@link $versionrequires} property to a numerical value representing
2183      * the version of Moodle core that this plugin requires.
2184      */
2185     public function load_required_main_version() {
2186         $plugin = $this->load_version_php();
2187         if (isset($plugin->requires)) {
2188             $this->versionrequires = $plugin->requires;
2189         }
2190     }
2192     /**
2193      * Initialise {@link $dependencies} to the list of other plugins (in any)
2194      * that this one requires to be installed.
2195      */
2196     protected function load_other_required_plugins() {
2197         $plugin = $this->load_version_php();
2198         if (!empty($plugin->dependencies)) {
2199             $this->dependencies = $plugin->dependencies;
2200         } else {
2201             $this->dependencies = array(); // By default, no dependencies.
2202         }
2203     }
2205     /**
2206      * Get the list of other plugins that this plugin requires to be installed.
2207      *
2208      * @return array with keys the frankenstyle plugin name, and values either
2209      *      a version string (like '2011101700') or the constant ANY_VERSION.
2210      */
2211     public function get_other_required_plugins() {
2212         if (is_null($this->dependencies)) {
2213             $this->load_other_required_plugins();
2214         }
2215         return $this->dependencies;
2216     }
2218     /**
2219      * Sets {@link $versiondb} property to a numerical value representing the
2220      * currently installed version of the plugin.
2221      *
2222      * If the value is null after calling this method, either the plugin
2223      * does not use versioning (typically does not have any database
2224      * data) or has not been installed yet.
2225      */
2226     public function load_db_version() {
2227         if ($ver = self::get_version_from_config_plugins($this->component)) {
2228             $this->versiondb = $ver;
2229         }
2230     }
2232     /**
2233      * Sets {@link $source} property to one of plugin_manager::PLUGIN_SOURCE_xxx
2234      * constants.
2235      *
2236      * If the property's value is null after calling this method, then
2237      * the type of the plugin has not been recognized and you should throw
2238      * an exception.
2239      */
2240     public function init_is_standard() {
2242         $standard = plugin_manager::standard_plugins_list($this->type);
2244         if ($standard !== false) {
2245             $standard = array_flip($standard);
2246             if (isset($standard[$this->name])) {
2247                 $this->source = plugin_manager::PLUGIN_SOURCE_STANDARD;
2248             } else if (!is_null($this->versiondb) and is_null($this->versiondisk)
2249                     and plugin_manager::is_deleted_standard_plugin($this->type, $this->name)) {
2250                 $this->source = plugin_manager::PLUGIN_SOURCE_STANDARD; // to be deleted
2251             } else {
2252                 $this->source = plugin_manager::PLUGIN_SOURCE_EXTENSION;
2253             }
2254         }
2255     }
2257     /**
2258      * Returns true if the plugin is shipped with the official distribution
2259      * of the current Moodle version, false otherwise.
2260      *
2261      * @return bool
2262      */
2263     public function is_standard() {
2264         return $this->source === plugin_manager::PLUGIN_SOURCE_STANDARD;
2265     }
2267     /**
2268      * Returns true if the the given Moodle version is enough to run this plugin
2269      *
2270      * @param string|int|double $moodleversion
2271      * @return bool
2272      */
2273     public function is_core_dependency_satisfied($moodleversion) {
2275         if (empty($this->versionrequires)) {
2276             return true;
2278         } else {
2279             return (double)$this->versionrequires <= (double)$moodleversion;
2280         }
2281     }
2283     /**
2284      * Returns the status of the plugin
2285      *
2286      * @return string one of plugin_manager::PLUGIN_STATUS_xxx constants
2287      */
2288     public function get_status() {
2290         if (is_null($this->versiondb) and is_null($this->versiondisk)) {
2291             return plugin_manager::PLUGIN_STATUS_NODB;
2293         } else if (is_null($this->versiondb) and !is_null($this->versiondisk)) {
2294             return plugin_manager::PLUGIN_STATUS_NEW;
2296         } else if (!is_null($this->versiondb) and is_null($this->versiondisk)) {
2297             if (plugin_manager::is_deleted_standard_plugin($this->type, $this->name)) {
2298                 return plugin_manager::PLUGIN_STATUS_DELETE;
2299             } else {
2300                 return plugin_manager::PLUGIN_STATUS_MISSING;
2301             }
2303         } else if ((string)$this->versiondb === (string)$this->versiondisk) {
2304             return plugin_manager::PLUGIN_STATUS_UPTODATE;
2306         } else if ($this->versiondb < $this->versiondisk) {
2307             return plugin_manager::PLUGIN_STATUS_UPGRADE;
2309         } else if ($this->versiondb > $this->versiondisk) {
2310             return plugin_manager::PLUGIN_STATUS_DOWNGRADE;
2312         } else {
2313             // $version = pi(); and similar funny jokes - hopefully Donald E. Knuth will never contribute to Moodle ;-)
2314             throw new coding_exception('Unable to determine plugin state, check the plugin versions');
2315         }
2316     }
2318     /**
2319      * Returns the information about plugin availability
2320      *
2321      * True means that the plugin is enabled. False means that the plugin is
2322      * disabled. Null means that the information is not available, or the
2323      * plugin does not support configurable availability or the availability
2324      * can not be changed.
2325      *
2326      * @return null|bool
2327      */
2328     public function is_enabled() {
2329         return null;
2330     }
2332     /**
2333      * Populates the property {@link $availableupdates} with the information provided by
2334      * available update checker
2335      *
2336      * @param available_update_checker $provider the class providing the available update info
2337      */
2338     public function check_available_updates(available_update_checker $provider) {
2339         global $CFG;
2341         if (isset($CFG->updateminmaturity)) {
2342             $minmaturity = $CFG->updateminmaturity;
2343         } else {
2344             // this can happen during the very first upgrade to 2.3
2345             $minmaturity = MATURITY_STABLE;
2346         }
2348         $this->availableupdates = $provider->get_update_info($this->component,
2349             array('minmaturity' => $minmaturity));
2350     }
2352     /**
2353      * If there are updates for this plugin available, returns them.
2354      *
2355      * Returns array of {@link available_update_info} objects, if some update
2356      * is available. Returns null if there is no update available or if the update
2357      * availability is unknown.
2358      *
2359      * @return array|null
2360      */
2361     public function available_updates() {
2363         if (empty($this->availableupdates) or !is_array($this->availableupdates)) {
2364             return null;
2365         }
2367         $updates = array();
2369         foreach ($this->availableupdates as $availableupdate) {
2370             if ($availableupdate->version > $this->versiondisk) {
2371                 $updates[] = $availableupdate;
2372             }
2373         }
2375         if (empty($updates)) {
2376             return null;
2377         }
2379         return $updates;
2380     }
2382     /**
2383      * Returns the node name used in admin settings menu for this plugin settings (if applicable)
2384      *
2385      * @return null|string node name or null if plugin does not create settings node (default)
2386      */
2387     public function get_settings_section_name() {
2388         return null;
2389     }
2391     /**
2392      * Returns the URL of the plugin settings screen
2393      *
2394      * Null value means that the plugin either does not have the settings screen
2395      * or its location is not available via this library.
2396      *
2397      * @return null|moodle_url
2398      */
2399     public function get_settings_url() {
2400         $section = $this->get_settings_section_name();
2401         if ($section === null) {
2402             return null;
2403         }
2404         $settings = admin_get_root()->locate($section);
2405         if ($settings && $settings instanceof admin_settingpage) {
2406             return new moodle_url('/admin/settings.php', array('section' => $section));
2407         } else if ($settings && $settings instanceof admin_externalpage) {
2408             return new moodle_url($settings->url);
2409         } else {
2410             return null;
2411         }
2412     }
2414     /**
2415      * Loads plugin settings to the settings tree
2416      *
2417      * This function usually includes settings.php file in plugins folder.
2418      * Alternatively it can create a link to some settings page (instance of admin_externalpage)
2419      *
2420      * @param part_of_admin_tree $adminroot
2421      * @param string $parentnodename
2422      * @param bool $hassiteconfig whether the current user has moodle/site:config capability
2423      */
2424     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
2425     }
2427     /**
2428      * Returns the URL of the screen where this plugin can be uninstalled
2429      *
2430      * Visiting that URL must be safe, that is a manual confirmation is needed
2431      * for actual uninstallation of the plugin. Null value means that the
2432      * plugin either does not support uninstallation, or does not require any
2433      * database cleanup or the location of the screen is not available via this
2434      * library.
2435      *
2436      * @return null|moodle_url
2437      */
2438     public function get_uninstall_url() {
2439         return null;
2440     }
2442     /**
2443      * Returns relative directory of the plugin with heading '/'
2444      *
2445      * @return string
2446      */
2447     public function get_dir() {
2448         global $CFG;
2450         return substr($this->rootdir, strlen($CFG->dirroot));
2451     }
2453     /**
2454      * Provides access to plugin versions from {config_plugins}
2455      *
2456      * @param string $plugin plugin name
2457      * @param double $disablecache optional, defaults to false
2458      * @return int|false the stored value or false if not found
2459      */
2460     protected function get_version_from_config_plugins($plugin, $disablecache=false) {
2461         global $DB;
2462         static $pluginversions = null;
2464         if (is_null($pluginversions) or $disablecache) {
2465             try {
2466                 $pluginversions = $DB->get_records_menu('config_plugins', array('name' => 'version'), 'plugin', 'plugin,value');
2467             } catch (dml_exception $e) {
2468                 // before install
2469                 $pluginversions = array();
2470             }
2471         }
2473         if (!array_key_exists($plugin, $pluginversions)) {
2474             return false;
2475         }
2477         return $pluginversions[$plugin];
2478     }
2482 /**
2483  * General class for all plugin types that do not have their own class
2484  */
2485 class plugininfo_general extends plugininfo_base {
2489 /**
2490  * Class for page side blocks
2491  */
2492 class plugininfo_block extends plugininfo_base {
2494     public static function get_plugins($type, $typerootdir, $typeclass) {
2496         // get the information about blocks at the disk
2497         $blocks = parent::get_plugins($type, $typerootdir, $typeclass);
2499         // add blocks missing from disk
2500         $blocksinfo = self::get_blocks_info();
2501         foreach ($blocksinfo as $blockname => $blockinfo) {
2502             if (isset($blocks[$blockname])) {
2503                 continue;
2504             }
2505             $plugin                 = new $typeclass();
2506             $plugin->type           = $type;
2507             $plugin->typerootdir    = $typerootdir;
2508             $plugin->name           = $blockname;
2509             $plugin->rootdir        = null;
2510             $plugin->displayname    = $blockname;
2511             $plugin->versiondb      = $blockinfo->version;
2512             $plugin->init_is_standard();
2514             $blocks[$blockname]   = $plugin;
2515         }
2517         return $blocks;
2518     }
2520     /**
2521      * Magic method getter, redirects to read only values.
2522      *
2523      * For block plugins pretends the object has 'visible' property for compatibility
2524      * with plugins developed for Moodle version below 2.4
2525      *
2526      * @param string $name
2527      * @return mixed
2528      */
2529     public function __get($name) {
2530         if ($name === 'visible') {
2531             debugging('This is now an instance of plugininfo_block, please use $block->is_enabled() instead of $block->visible', DEBUG_DEVELOPER);
2532             return ($this->is_enabled() !== false);
2533         }
2534         return parent::__get($name);
2535     }
2537     public function init_display_name() {
2539         if (get_string_manager()->string_exists('pluginname', 'block_' . $this->name)) {
2540             $this->displayname = get_string('pluginname', 'block_' . $this->name);
2542         } else if (($block = block_instance($this->name)) !== false) {
2543             $this->displayname = $block->get_title();
2545         } else {
2546             parent::init_display_name();
2547         }
2548     }
2550     public function load_db_version() {
2551         global $DB;
2553         $blocksinfo = self::get_blocks_info();
2554         if (isset($blocksinfo[$this->name]->version)) {
2555             $this->versiondb = $blocksinfo[$this->name]->version;
2556         }
2557     }
2559     public function is_enabled() {
2561         $blocksinfo = self::get_blocks_info();
2562         if (isset($blocksinfo[$this->name]->visible)) {
2563             if ($blocksinfo[$this->name]->visible) {
2564                 return true;
2565             } else {
2566                 return false;
2567             }
2568         } else {
2569             return parent::is_enabled();
2570         }
2571     }
2573     public function get_settings_section_name() {
2574         return 'blocksetting' . $this->name;
2575     }
2577     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
2578         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
2579         $ADMIN = $adminroot; // may be used in settings.php
2580         $block = $this; // also can be used inside settings.php
2581         $section = $this->get_settings_section_name();
2583         if (!$hassiteconfig || (($blockinstance = block_instance($this->name)) === false)) {
2584             return;
2585         }
2587         $settings = null;
2588         if ($blockinstance->has_config()) {
2589             if (file_exists($this->full_path('settings.php'))) {
2590                 $settings = new admin_settingpage($section, $this->displayname,
2591                         'moodle/site:config', $this->is_enabled() === false);
2592                 include($this->full_path('settings.php')); // this may also set $settings to null
2593             } else {
2594                 $blocksinfo = self::get_blocks_info();
2595                 $settingsurl = new moodle_url('/admin/block.php', array('block' => $blocksinfo[$this->name]->id));
2596                 $settings = new admin_externalpage($section, $this->displayname,
2597                         $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
2598             }
2599         }
2600         if ($settings) {
2601             $ADMIN->add($parentnodename, $settings);
2602         }
2603     }
2605     public function get_uninstall_url() {
2607         $blocksinfo = self::get_blocks_info();
2608         return new moodle_url('/admin/blocks.php', array('delete' => $blocksinfo[$this->name]->id, 'sesskey' => sesskey()));
2609     }
2611     /**
2612      * Provides access to the records in {block} table
2613      *
2614      * @param bool $disablecache do not use internal static cache
2615      * @return array array of stdClasses
2616      */
2617     protected static function get_blocks_info($disablecache=false) {
2618         global $DB;
2619         static $blocksinfocache = null;
2621         if (is_null($blocksinfocache) or $disablecache) {
2622             try {
2623                 $blocksinfocache = $DB->get_records('block', null, 'name', 'name,id,version,visible');
2624             } catch (dml_exception $e) {
2625                 // before install
2626                 $blocksinfocache = array();
2627             }
2628         }
2630         return $blocksinfocache;
2631     }
2635 /**
2636  * Class for text filters
2637  */
2638 class plugininfo_filter extends plugininfo_base {
2640     public static function get_plugins($type, $typerootdir, $typeclass) {
2641         global $CFG, $DB;
2643         $filters = array();
2645         // get the list of filters from both /filter and /mod location
2646         $installed = filter_get_all_installed();
2648         foreach ($installed as $filterlegacyname => $displayname) {
2649             $plugin                 = new $typeclass();
2650             $plugin->type           = $type;
2651             $plugin->typerootdir    = $typerootdir;
2652             $plugin->name           = self::normalize_legacy_name($filterlegacyname);
2653             $plugin->rootdir        = $CFG->dirroot . '/' . $filterlegacyname;
2654             $plugin->displayname    = $displayname;
2656             $plugin->load_disk_version();
2657             $plugin->load_db_version();
2658             $plugin->load_required_main_version();
2659             $plugin->init_is_standard();
2661             $filters[$plugin->name] = $plugin;
2662         }
2664         $globalstates = self::get_global_states();
2666         if ($DB->get_manager()->table_exists('filter_active')) {
2667             // if we're upgrading from 1.9, the table does not exist yet
2668             // if it does, make sure that all installed filters are registered
2669             $needsreload  = false;
2670             foreach (array_keys($installed) as $filterlegacyname) {
2671                 if (!isset($globalstates[self::normalize_legacy_name($filterlegacyname)])) {
2672                     filter_set_global_state($filterlegacyname, TEXTFILTER_DISABLED);
2673                     $needsreload = true;
2674                 }
2675             }
2676             if ($needsreload) {
2677                 $globalstates = self::get_global_states(true);
2678             }
2679         }
2681         // make sure that all registered filters are installed, just in case
2682         foreach ($globalstates as $name => $info) {
2683             if (!isset($filters[$name])) {
2684                 // oops, there is a record in filter_active but the filter is not installed
2685                 $plugin                 = new $typeclass();
2686                 $plugin->type           = $type;
2687                 $plugin->typerootdir    = $typerootdir;
2688                 $plugin->name           = $name;
2689                 $plugin->rootdir        = $CFG->dirroot . '/' . $info->legacyname;
2690                 $plugin->displayname    = $info->legacyname;
2692                 $plugin->load_db_version();
2694                 if (is_null($plugin->versiondb)) {
2695                     // this is a hack to stimulate 'Missing from disk' error
2696                     // because $plugin->versiondisk will be null !== false
2697                     $plugin->versiondb = false;
2698                 }
2700                 $filters[$plugin->name] = $plugin;
2701             }
2702         }
2704         return $filters;
2705     }
2707     public function init_display_name() {
2708         // do nothing, the name is set in self::get_plugins()
2709     }
2711     /**
2712      * @see load_version_php()
2713      */
2714     protected function load_version_php() {
2715         if (strpos($this->name, 'mod_') === 0) {
2716             // filters bundled with modules do not have a version.php and so
2717             // do not provide their own versioning information.
2718             return new stdClass();
2719         }
2720         return parent::load_version_php();
2721     }
2723     public function is_enabled() {
2725         $globalstates = self::get_global_states();
2727         foreach ($globalstates as $filterlegacyname => $info) {
2728             $name = self::normalize_legacy_name($filterlegacyname);
2729             if ($name === $this->name) {
2730                 if ($info->active == TEXTFILTER_DISABLED) {
2731                     return false;
2732                 } else {
2733                     // it may be 'On' or 'Off, but available'
2734                     return null;
2735                 }
2736             }
2737         }
2739         return null;
2740     }
2742     public function get_settings_section_name() {
2743         $globalstates = self::get_global_states();
2744         if (!isset($globalstates[$this->name])) {
2745             return parent::get_settings_section_name();
2746         }
2747         $legacyname = $globalstates[$this->name]->legacyname;
2748         return 'filtersetting' . str_replace('/', '', $legacyname);
2749     }
2751     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
2752         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
2753         $ADMIN = $adminroot; // may be used in settings.php
2754         $filter = $this; // also can be used inside settings.php
2756         $globalstates = self::get_global_states();
2757         $settings = null;
2758         if ($hassiteconfig && isset($globalstates[$this->name]) && file_exists($this->full_path('filtersettings.php'))) {
2759             $section = $this->get_settings_section_name();
2760             $settings = new admin_settingpage($section, $this->displayname,
2761                     'moodle/site:config', $this->is_enabled() === false);
2762             include($this->full_path('filtersettings.php')); // this may also set $settings to null
2763         }
2764         if ($settings) {
2765             $ADMIN->add($parentnodename, $settings);
2766         }
2767     }
2769     public function get_uninstall_url() {
2771         if (strpos($this->name, 'mod_') === 0) {
2772             return null;
2773         } else {
2774             $globalstates = self::get_global_states();
2775             $legacyname = $globalstates[$this->name]->legacyname;
2776             return new moodle_url('/admin/filters.php', array('sesskey' => sesskey(), 'filterpath' => $legacyname, 'action' => 'delete'));
2777         }
2778     }
2780     /**
2781      * Convert legacy filter names like 'filter/foo' or 'mod/bar' into frankenstyle
2782      *
2783      * @param string $legacyfiltername legacy filter name
2784      * @return string frankenstyle-like name
2785      */
2786     protected static function normalize_legacy_name($legacyfiltername) {
2788         $name = str_replace('/', '_', $legacyfiltername);
2789         if (strpos($name, 'filter_') === 0) {
2790             $name = substr($name, 7);
2791             if (empty($name)) {
2792                 throw new coding_exception('Unable to determine filter name: ' . $legacyfiltername);
2793             }
2794         }
2796         return $name;
2797     }
2799     /**
2800      * Provides access to the results of {@link filter_get_global_states()}
2801      * but indexed by the normalized filter name
2802      *
2803      * The legacy filter name is available as ->legacyname property.
2804      *
2805      * @param bool $disablecache
2806      * @return array
2807      */
2808     protected static function get_global_states($disablecache=false) {
2809         global $DB;
2810         static $globalstatescache = null;
2812         if ($disablecache or is_null($globalstatescache)) {
2814             if (!$DB->get_manager()->table_exists('filter_active')) {
2815                 // we're upgrading from 1.9 and the table used by {@link filter_get_global_states()}
2816                 // does not exist yet
2817                 $globalstatescache = array();
2819             } else {
2820                 foreach (filter_get_global_states() as $legacyname => $info) {
2821                     $name                       = self::normalize_legacy_name($legacyname);
2822                     $filterinfo                 = new stdClass();
2823                     $filterinfo->legacyname     = $legacyname;
2824                     $filterinfo->active         = $info->active;
2825                     $filterinfo->sortorder      = $info->sortorder;
2826                     $globalstatescache[$name]   = $filterinfo;
2827                 }
2828             }
2829         }
2831         return $globalstatescache;
2832     }
2836 /**
2837  * Class for activity modules
2838  */
2839 class plugininfo_mod extends plugininfo_base {
2841     public static function get_plugins($type, $typerootdir, $typeclass) {
2843         // get the information about plugins at the disk
2844         $modules = parent::get_plugins($type, $typerootdir, $typeclass);
2846         // add modules missing from disk
2847         $modulesinfo = self::get_modules_info();
2848         foreach ($modulesinfo as $modulename => $moduleinfo) {
2849             if (isset($modules[$modulename])) {
2850                 continue;
2851             }
2852             $plugin                 = new $typeclass();
2853             $plugin->type           = $type;
2854             $plugin->typerootdir    = $typerootdir;
2855             $plugin->name           = $modulename;
2856             $plugin->rootdir        = null;
2857             $plugin->displayname    = $modulename;
2858             $plugin->versiondb      = $moduleinfo->version;
2859             $plugin->init_is_standard();
2861             $modules[$modulename]   = $plugin;
2862         }
2864         return $modules;
2865     }
2867     /**
2868      * Magic method getter, redirects to read only values.
2869      *
2870      * For module plugins we pretend the object has 'visible' property for compatibility
2871      * with plugins developed for Moodle version below 2.4
2872      *
2873      * @param string $name
2874      * @return mixed
2875      */
2876     public function __get($name) {
2877         if ($name === 'visible') {
2878             debugging('This is now an instance of plugininfo_mod, please use $module->is_enabled() instead of $module->visible', DEBUG_DEVELOPER);
2879             return ($this->is_enabled() !== false);
2880         }
2881         return parent::__get($name);
2882     }
2884     public function init_display_name() {
2885         if (get_string_manager()->string_exists('pluginname', $this->component)) {
2886             $this->displayname = get_string('pluginname', $this->component);
2887         } else {
2888             $this->displayname = get_string('modulename', $this->component);
2889         }
2890     }
2892     /**
2893      * Load the data from version.php.
2894      * @return object the data object defined in version.php.
2895      */
2896     protected function load_version_php() {
2897         $versionfile = $this->full_path('version.php');
2899         $module = new stdClass();
2900         if (is_readable($versionfile)) {
2901             include($versionfile);
2902         }
2903         return $module;
2904     }
2906     public function load_db_version() {
2907         global $DB;
2909         $modulesinfo = self::get_modules_info();
2910         if (isset($modulesinfo[$this->name]->version)) {
2911             $this->versiondb = $modulesinfo[$this->name]->version;
2912         }
2913     }
2915     public function is_enabled() {
2917         $modulesinfo = self::get_modules_info();
2918         if (isset($modulesinfo[$this->name]->visible)) {
2919             if ($modulesinfo[$this->name]->visible) {
2920                 return true;
2921             } else {
2922                 return false;
2923             }
2924         } else {
2925             return parent::is_enabled();
2926         }
2927     }
2929     public function get_settings_section_name() {
2930         return 'modsetting' . $this->name;
2931     }
2933     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
2934         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
2935         $ADMIN = $adminroot; // may be used in settings.php
2936         $module = $this; // also can be used inside settings.php
2937         $section = $this->get_settings_section_name();
2939         $modulesinfo = self::get_modules_info();
2940         $settings = null;
2941         if ($hassiteconfig && isset($modulesinfo[$this->name]) && file_exists($this->full_path('settings.php'))) {
2942             $settings = new admin_settingpage($section, $this->displayname,
2943                     'moodle/site:config', $this->is_enabled() === false);
2944             include($this->full_path('settings.php')); // this may also set $settings to null
2945         }
2946         if ($settings) {
2947             $ADMIN->add($parentnodename, $settings);
2948         }
2949     }
2951     public function get_uninstall_url() {
2953         if ($this->name !== 'forum') {
2954             return new moodle_url('/admin/modules.php', array('delete' => $this->name, 'sesskey' => sesskey()));
2955         } else {
2956             return null;
2957         }
2958     }
2960     /**
2961      * Provides access to the records in {modules} table
2962      *
2963      * @param bool $disablecache do not use internal static cache
2964      * @return array array of stdClasses
2965      */
2966     protected static function get_modules_info($disablecache=false) {
2967         global $DB;
2968         static $modulesinfocache = null;
2970         if (is_null($modulesinfocache) or $disablecache) {
2971             try {
2972                 $modulesinfocache = $DB->get_records('modules', null, 'name', 'name,id,version,visible');
2973             } catch (dml_exception $e) {
2974                 // before install
2975                 $modulesinfocache = array();
2976             }
2977         }
2979         return $modulesinfocache;
2980     }
2984 /**
2985  * Class for question behaviours.
2986  */
2987 class plugininfo_qbehaviour extends plugininfo_base {
2989     public function get_uninstall_url() {
2990         return new moodle_url('/admin/qbehaviours.php',
2991                 array('delete' => $this->name, 'sesskey' => sesskey()));
2992     }
2996 /**
2997  * Class for question types
2998  */
2999 class plugininfo_qtype extends plugininfo_base {
3001     public function get_uninstall_url() {
3002         return new moodle_url('/admin/qtypes.php',
3003                 array('delete' => $this->name, 'sesskey' => sesskey()));
3004     }
3006     public function get_settings_section_name() {
3007         return 'qtypesetting' . $this->name;
3008     }
3010     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3011         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3012         $ADMIN = $adminroot; // may be used in settings.php
3013         $qtype = $this; // also can be used inside settings.php
3014         $section = $this->get_settings_section_name();
3016         $settings = null;
3017         $systemcontext = context_system::instance();
3018         if (($hassiteconfig || has_capability('moodle/question:config', $systemcontext)) &&
3019                 file_exists($this->full_path('settings.php'))) {
3020             $settings = new admin_settingpage($section, $this->displayname,
3021                     'moodle/question:config', $this->is_enabled() === false);
3022             include($this->full_path('settings.php')); // this may also set $settings to null
3023         }
3024         if ($settings) {
3025             $ADMIN->add($parentnodename, $settings);
3026         }
3027     }
3031 /**
3032  * Class for authentication plugins
3033  */
3034 class plugininfo_auth extends plugininfo_base {
3036     public function is_enabled() {
3037         global $CFG;
3038         /** @var null|array list of enabled authentication plugins */
3039         static $enabled = null;
3041         if (in_array($this->name, array('nologin', 'manual'))) {
3042             // these two are always enabled and can't be disabled
3043             return null;
3044         }
3046         if (is_null($enabled)) {
3047             $enabled = array_flip(explode(',', $CFG->auth));
3048         }
3050         return isset($enabled[$this->name]);
3051     }
3053     public function get_settings_section_name() {
3054         return 'authsetting' . $this->name;
3055     }
3057     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3058         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3059         $ADMIN = $adminroot; // may be used in settings.php
3060         $auth = $this; // also to be used inside settings.php
3061         $section = $this->get_settings_section_name();
3063         $settings = null;
3064         if ($hassiteconfig) {
3065             if (file_exists($this->full_path('settings.php'))) {
3066                 // TODO: finish implementation of common settings - locking, etc.
3067                 $settings = new admin_settingpage($section, $this->displayname,
3068                         'moodle/site:config', $this->is_enabled() === false);
3069                 include($this->full_path('settings.php')); // this may also set $settings to null
3070             } else {
3071                 $settingsurl = new moodle_url('/admin/auth_config.php', array('auth' => $this->name));
3072                 $settings = new admin_externalpage($section, $this->displayname,
3073                         $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
3074             }
3075         }
3076         if ($settings) {
3077             $ADMIN->add($parentnodename, $settings);
3078         }
3079     }
3083 /**
3084  * Class for enrolment plugins
3085  */
3086 class plugininfo_enrol extends plugininfo_base {
3088     public function is_enabled() {
3089         global $CFG;
3090         /** @var null|array list of enabled enrolment plugins */
3091         static $enabled = null;
3093         // We do not actually need whole enrolment classes here so we do not call
3094         // {@link enrol_get_plugins()}. Note that this may produce slightly different
3095         // results, for example if the enrolment plugin does not contain lib.php
3096         // but it is listed in $CFG->enrol_plugins_enabled
3098         if (is_null($enabled)) {
3099             $enabled = array_flip(explode(',', $CFG->enrol_plugins_enabled));
3100         }
3102         return isset($enabled[$this->name]);
3103     }
3105     public function get_settings_section_name() {
3106         return 'enrolsettings' . $this->name;
3107     }
3109     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3110         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3111         $ADMIN = $adminroot; // may be used in settings.php
3112         $enrol = $this; // also can be used inside settings.php
3113         $section = $this->get_settings_section_name();
3115         $settings = null;
3116         if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3117             $settings = new admin_settingpage($section, $this->displayname,
3118                     'moodle/site:config', $this->is_enabled() === false);
3119             include($this->full_path('settings.php')); // this may also set $settings to null
3120         }
3121         if ($settings) {
3122             $ADMIN->add($parentnodename, $settings);
3123         }
3124     }
3126     public function get_uninstall_url() {
3127         return new moodle_url('/admin/enrol.php', array('action' => 'uninstall', 'enrol' => $this->name, 'sesskey' => sesskey()));
3128     }
3132 /**
3133  * Class for messaging processors
3134  */
3135 class plugininfo_message extends plugininfo_base {
3137     public function get_settings_section_name() {
3138         return 'messagesetting' . $this->name;
3139     }
3141     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3142         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3143         $ADMIN = $adminroot; // may be used in settings.php
3144         if (!$hassiteconfig) {
3145             return;
3146         }
3147         $section = $this->get_settings_section_name();
3149         $settings = null;
3150         $processors = get_message_processors();
3151         if (isset($processors[$this->name])) {
3152             $processor = $processors[$this->name];
3153             if ($processor->available && $processor->hassettings) {
3154                 $settings = new admin_settingpage($section, $this->displayname,
3155                         'moodle/site:config', $this->is_enabled() === false);
3156                 include($this->full_path('settings.php')); // this may also set $settings to null
3157             }
3158         }
3159         if ($settings) {
3160             $ADMIN->add($parentnodename, $settings);
3161         }
3162     }
3164     /**
3165      * @see plugintype_interface::is_enabled()
3166      */
3167     public function is_enabled() {
3168         $processors = get_message_processors();
3169         if (isset($processors[$this->name])) {
3170             return $processors[$this->name]->configured && $processors[$this->name]->enabled;
3171         } else {
3172             return parent::is_enabled();
3173         }
3174     }
3176     /**
3177      * @see plugintype_interface::get_uninstall_url()
3178      */
3179     public function get_uninstall_url() {
3180         $processors = get_message_processors();
3181         if (isset($processors[$this->name])) {
3182             return new moodle_url('/admin/message.php', array('uninstall' => $processors[$this->name]->id, 'sesskey' => sesskey()));
3183         } else {
3184             return parent::get_uninstall_url();
3185         }
3186     }
3190 /**
3191  * Class for repositories
3192  */
3193 class plugininfo_repository extends plugininfo_base {
3195     public function is_enabled() {
3197         $enabled = self::get_enabled_repositories();
3199         return isset($enabled[$this->name]);
3200     }
3202     public function get_settings_section_name() {
3203         return 'repositorysettings'.$this->name;
3204     }
3206     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3207         if ($hassiteconfig && $this->is_enabled()) {
3208             // completely no access to repository setting when it is not enabled
3209             $sectionname = $this->get_settings_section_name();
3210             $settingsurl = new moodle_url('/admin/repository.php',
3211                     array('sesskey' => sesskey(), 'action' => 'edit', 'repos' => $this->name));
3212             $settings = new admin_externalpage($sectionname, $this->displayname,
3213                     $settingsurl, 'moodle/site:config', false);
3214             $adminroot->add($parentnodename, $settings);
3215         }
3216     }
3218     /**
3219      * Provides access to the records in {repository} table
3220      *
3221      * @param bool $disablecache do not use internal static cache
3222      * @return array array of stdClasses
3223      */
3224     protected static function get_enabled_repositories($disablecache=false) {
3225         global $DB;
3226         static $repositories = null;
3228         if (is_null($repositories) or $disablecache) {
3229             $repositories = $DB->get_records('repository', null, 'type', 'type,visible,sortorder');
3230         }
3232         return $repositories;
3233     }
3237 /**
3238  * Class for portfolios
3239  */
3240 class plugininfo_portfolio extends plugininfo_base {
3242     public function is_enabled() {
3244         $enabled = self::get_enabled_portfolios();
3246         return isset($enabled[$this->name]);
3247     }
3249     /**
3250      * Provides access to the records in {portfolio_instance} table
3251      *
3252      * @param bool $disablecache do not use internal static cache
3253      * @return array array of stdClasses
3254      */
3255     protected static function get_enabled_portfolios($disablecache=false) {
3256         global $DB;
3257         static $portfolios = null;
3259         if (is_null($portfolios) or $disablecache) {
3260             $portfolios = array();
3261             $instances  = $DB->get_recordset('portfolio_instance', null, 'plugin');
3262             foreach ($instances as $instance) {
3263                 if (isset($portfolios[$instance->plugin])) {
3264                     if ($instance->visible) {
3265                         $portfolios[$instance->plugin]->visible = $instance->visible;
3266                     }
3267                 } else {
3268                     $portfolios[$instance->plugin] = $instance;
3269                 }
3270             }
3271         }
3273         return $portfolios;
3274     }
3278 /**
3279  * Class for themes
3280  */
3281 class plugininfo_theme extends plugininfo_base {
3283     public function is_enabled() {
3284         global $CFG;
3286         if ((!empty($CFG->theme) and $CFG->theme === $this->name) or
3287             (!empty($CFG->themelegacy) and $CFG->themelegacy === $this->name)) {
3288             return true;
3289         } else {
3290             return parent::is_enabled();
3291         }
3292     }
3296 /**
3297  * Class representing an MNet service
3298  */
3299 class plugininfo_mnetservice extends plugininfo_base {
3301     public function is_enabled() {
3302         global $CFG;
3304         if (empty($CFG->mnet_dispatcher_mode) || $CFG->mnet_dispatcher_mode !== 'strict') {
3305             return false;
3306         } else {
3307             return parent::is_enabled();
3308         }
3309     }
3313 /**
3314  * Class for admin tool plugins
3315  */
3316 class plugininfo_tool extends plugininfo_base {
3318     public function get_uninstall_url() {
3319         return new moodle_url('/admin/tools.php', array('delete' => $this->name, 'sesskey' => sesskey()));
3320     }
3324 /**
3325  * Class for admin tool plugins
3326  */
3327 class plugininfo_report extends plugininfo_base {
3329     public function get_uninstall_url() {
3330         return new moodle_url('/admin/reports.php', array('delete' => $this->name, 'sesskey' => sesskey()));
3331     }
3335 /**
3336  * Class for local plugins
3337  */
3338 class plugininfo_local extends plugininfo_base {
3340     public function get_uninstall_url() {
3341         return new moodle_url('/admin/localplugins.php', array('delete' => $this->name, 'sesskey' => sesskey()));
3342     }
3345 /**
3346  * Class for HTML editors
3347  */
3348 class plugininfo_editor extends plugininfo_base {
3350     public function get_settings_section_name() {
3351         return 'editorsettings' . $this->name;
3352     }
3354     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3355         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3356         $ADMIN = $adminroot; // may be used in settings.php
3357         $editor = $this; // also can be used inside settings.php
3358         $section = $this->get_settings_section_name();
3360         $settings = null;
3361         if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3362             $settings = new admin_settingpage($section, $this->displayname,
3363                     'moodle/site:config', $this->is_enabled() === false);
3364             include($this->full_path('settings.php')); // this may also set $settings to null
3365         }
3366         if ($settings) {
3367             $ADMIN->add($parentnodename, $settings);
3368         }
3369     }
3371     /**
3372      * Returns the information about plugin availability
3373      *
3374      * True means that the plugin is enabled. False means that the plugin is
3375      * disabled. Null means that the information is not available, or the
3376      * plugin does not support configurable availability or the availability
3377      * can not be changed.
3378      *
3379      * @return null|bool
3380      */
3381     public function is_enabled() {
3382         global $CFG;
3383         if (empty($CFG->texteditors)) {
3384             $CFG->texteditors = 'tinymce,textarea';
3385         }
3386         if (in_array($this->name, explode(',', $CFG->texteditors))) {
3387             return true;
3388         }
3389         return false;
3390     }
3393 /**
3394  * Class for plagiarism plugins
3395  */
3396 class plugininfo_plagiarism extends plugininfo_base {
3398     public function get_settings_section_name() {
3399         return 'plagiarism'. $this->name;
3400     }
3402     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3403         // plagiarism plugin just redirect to settings.php in the plugins directory
3404         if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3405             $section = $this->get_settings_section_name();
3406             $settingsurl = new moodle_url($this->get_dir().'/settings.php');
3407             $settings = new admin_externalpage($section, $this->displayname,
3408                     $settingsurl, 'moodle/site:config', $this->is_enabled() === false);
3409             $adminroot->add($parentnodename, $settings);
3410         }
3411     }
3414 /**
3415  * Class for webservice protocols
3416  */
3417 class plugininfo_webservice extends plugininfo_base {
3419     public function get_settings_section_name() {
3420         return 'webservicesetting' . $this->name;
3421     }
3423     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
3424         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
3425         $ADMIN = $adminroot; // may be used in settings.php
3426         $webservice = $this; // also can be used inside settings.php
3427         $section = $this->get_settings_section_name();
3429         $settings = null;
3430         if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3431             $settings = new admin_settingpage($section, $this->displayname,
3432                     'moodle/site:config', $this->is_enabled() === false);
3433             include($this->full_path('settings.php')); // this may also set $settings to null
3434         }
3435         if ($settings) {
3436             $ADMIN->add($parentnodename, $settings);
3437         }
3438     }
3440     public function is_enabled() {
3441         global $CFG;
3442         if (empty($CFG->enablewebservices)) {
3443             return false;
3444         }
3445         $active_webservices = empty($CFG->webserviceprotocols) ? array() : explode(',', $CFG->webserviceprotocols);
3446         if (in_array($this->name, $active_webservices)) {
3447             return true;
3448         }
3449         return false;
3450     }
3452     public function get_uninstall_url() {
3453         return new moodle_url('/admin/webservice/protocols.php',
3454                 array('sesskey' => sesskey(), 'action' => 'uninstall', 'webservice' => $this->name));
3455     }
3458 /**
3459  * Class for course formats
3460  */
3461 class plugininfo_format extends plugininfo_base {
3463     /**
3464      * Gathers and returns the information about all plugins of the given type
3465      *
3466      * @param string $type the name of the plugintype, eg. mod, auth or workshopform
3467      * @param string $typerootdir full path to the location of the plugin dir
3468      * @param string $typeclass the name of the actually called class
3469      * @return array of plugintype classes, indexed by the plugin name
3470      */
3471     public static function get_plugins($type, $typerootdir, $typeclass) {
3472         global $CFG;
3473         $formats = parent::get_plugins($type, $typerootdir, $typeclass);
3474         require_once($CFG->dirroot.'/course/lib.php');
3475         $order = get_sorted_course_formats();
3476         $sortedformats = array();
3477         foreach ($order as $formatname) {
3478             $sortedformats[$formatname] = $formats[$formatname];
3479         }
3480         return $sortedformats;
3481     }
3483     public function get_settings_section_name() {
3484         return 'formatsetting' . $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; // also may be used in settings.php
3490         $section = $this->get_settings_section_name();
3492         $settings = null;
3493         if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
3494             $settings = new admin_settingpage($section, $this->displayname,
3495                     'moodle/site:config', $this->is_enabled() === false);
3496             include($this->full_path('settings.php')); // this may also set $settings to null
3497         }
3498         if ($settings) {
3499             $ADMIN->add($parentnodename, $settings);
3500         }
3501     }
3503     public function is_enabled() {
3504         return !get_config($this->component, 'disabled');
3505     }
3507     public function get_uninstall_url() {
3508         if ($this->name !== get_config('moodlecourse', 'format') && $this->name !== 'site') {
3509             return new moodle_url('/admin/courseformats.php',
3510                     array('sesskey' => sesskey(), 'action' => 'uninstall', 'format' => $this->name));
3511         }
3512         return parent::get_uninstall_url();
3513     }