7333e075511b31b588d66f5e7485e02670ec009c
[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      * Returns a tree of known plugins and information about them
91      *
92      * @param bool $disablecache force reload, cache can be used otherwise
93      * @return array 2D array. The first keys are plugin type names (e.g. qtype);
94      *      the second keys are the plugin local name (e.g. multichoice); and
95      *      the values are the corresponding objects extending {@link plugininfo_base}
96      */
97     public function get_plugins($disablecache=false) {
98         global $CFG;
100         if ($disablecache or is_null($this->pluginsinfo)) {
101             // Hack: include mod and editor subplugin management classes first,
102             //       the adminlib.php is supposed to contain extra admin settings too.
103             require_once($CFG->libdir.'/adminlib.php');
104             foreach(array('mod', 'editor') as $type) {
105                 foreach (get_plugin_list($type) as $dir) {
106                     if (file_exists("$dir/adminlib.php")) {
107                         include_once("$dir/adminlib.php");
108                     }
109                 }
110             }
111             $this->pluginsinfo = array();
112             $plugintypes = get_plugin_types();
113             $plugintypes = $this->reorder_plugin_types($plugintypes);
114             foreach ($plugintypes as $plugintype => $plugintyperootdir) {
115                 if (in_array($plugintype, array('base', 'general'))) {
116                     throw new coding_exception('Illegal usage of reserved word for plugin type');
117                 }
118                 if (class_exists('plugininfo_' . $plugintype)) {
119                     $plugintypeclass = 'plugininfo_' . $plugintype;
120                 } else {
121                     $plugintypeclass = 'plugininfo_general';
122                 }
123                 if (!in_array('plugininfo_base', class_parents($plugintypeclass))) {
124                     throw new coding_exception('Class ' . $plugintypeclass . ' must extend plugininfo_base');
125                 }
126                 $plugins = call_user_func(array($plugintypeclass, 'get_plugins'), $plugintype, $plugintyperootdir, $plugintypeclass);
127                 $this->pluginsinfo[$plugintype] = $plugins;
128             }
130             if (empty($CFG->disableupdatenotifications) and !during_initial_install()) {
131                 // append the information about available updates provided by {@link available_update_checker()}
132                 $provider = available_update_checker::instance();
133                 foreach ($this->pluginsinfo as $plugintype => $plugins) {
134                     foreach ($plugins as $plugininfoholder) {
135                         $plugininfoholder->check_available_updates($provider);
136                     }
137                 }
138             }
139         }
141         return $this->pluginsinfo;
142     }
144     /**
145      * Returns list of plugins that define their subplugins and the information
146      * about them from the db/subplugins.php file.
147      *
148      * At the moment, only activity modules and editors can define subplugins.
149      *
150      * @param bool $disablecache force reload, cache can be used otherwise
151      * @return array with keys like 'mod_quiz', and values the data from the
152      *      corresponding db/subplugins.php file.
153      */
154     public function get_subplugins($disablecache=false) {
156         if ($disablecache or is_null($this->subpluginsinfo)) {
157             $this->subpluginsinfo = array();
158             foreach (array('mod', 'editor') as $type) {
159                 $owners = get_plugin_list($type);
160                 foreach ($owners as $component => $ownerdir) {
161                     $componentsubplugins = array();
162                     if (file_exists($ownerdir . '/db/subplugins.php')) {
163                         $subplugins = array();
164                         include($ownerdir . '/db/subplugins.php');
165                         foreach ($subplugins as $subplugintype => $subplugintyperootdir) {
166                             $subplugin = new stdClass();
167                             $subplugin->type = $subplugintype;
168                             $subplugin->typerootdir = $subplugintyperootdir;
169                             $componentsubplugins[$subplugintype] = $subplugin;
170                         }
171                         $this->subpluginsinfo[$type . '_' . $component] = $componentsubplugins;
172                     }
173                 }
174             }
175         }
177         return $this->subpluginsinfo;
178     }
180     /**
181      * Returns the name of the plugin that defines the given subplugin type
182      *
183      * If the given subplugin type is not actually a subplugin, returns false.
184      *
185      * @param string $subplugintype the name of subplugin type, eg. workshopform or quiz
186      * @return false|string the name of the parent plugin, eg. mod_workshop
187      */
188     public function get_parent_of_subplugin($subplugintype) {
190         $parent = false;
191         foreach ($this->get_subplugins() as $pluginname => $subplugintypes) {
192             if (isset($subplugintypes[$subplugintype])) {
193                 $parent = $pluginname;
194                 break;
195             }
196         }
198         return $parent;
199     }
201     /**
202      * Returns a localized name of a given plugin
203      *
204      * @param string $plugin name of the plugin, eg mod_workshop or auth_ldap
205      * @return string
206      */
207     public function plugin_name($plugin) {
208         list($type, $name) = normalize_component($plugin);
209         return $this->pluginsinfo[$type][$name]->displayname;
210     }
212     /**
213      * Returns a localized name of a plugin type in plural form
214      *
215      * Most plugin types define their names in core_plugin lang file. In case of subplugins,
216      * we try to ask the parent plugin for the name. In the worst case, we will return
217      * the value of the passed $type parameter.
218      *
219      * @param string $type the type of the plugin, e.g. mod or workshopform
220      * @return string
221      */
222     public function plugintype_name_plural($type) {
224         if (get_string_manager()->string_exists('type_' . $type . '_plural', 'core_plugin')) {
225             // for most plugin types, their names are defined in core_plugin lang file
226             return get_string('type_' . $type . '_plural', 'core_plugin');
228         } else if ($parent = $this->get_parent_of_subplugin($type)) {
229             // if this is a subplugin, try to ask the parent plugin for the name
230             if (get_string_manager()->string_exists('subplugintype_' . $type . '_plural', $parent)) {
231                 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type . '_plural', $parent);
232             } else {
233                 return $this->plugin_name($parent) . ' / ' . $type;
234             }
236         } else {
237             return $type;
238         }
239     }
241     /**
242      * @param string $component frankenstyle component name.
243      * @return plugininfo_base|null the corresponding plugin information.
244      */
245     public function get_plugin_info($component) {
246         list($type, $name) = normalize_component($component);
247         $plugins = $this->get_plugins();
248         if (isset($plugins[$type][$name])) {
249             return $plugins[$type][$name];
250         } else {
251             return null;
252         }
253     }
255     /**
256      * Get a list of any other plugins that require this one.
257      * @param string $component frankenstyle component name.
258      * @return array of frankensyle component names that require this one.
259      */
260     public function other_plugins_that_require($component) {
261         $others = array();
262         foreach ($this->get_plugins() as $type => $plugins) {
263             foreach ($plugins as $plugin) {
264                 $required = $plugin->get_other_required_plugins();
265                 if (isset($required[$component])) {
266                     $others[] = $plugin->component;
267                 }
268             }
269         }
270         return $others;
271     }
273     /**
274      * Check a dependencies list against the list of installed plugins.
275      * @param array $dependencies compenent name to required version or ANY_VERSION.
276      * @return bool true if all the dependencies are satisfied.
277      */
278     public function are_dependencies_satisfied($dependencies) {
279         foreach ($dependencies as $component => $requiredversion) {
280             $otherplugin = $this->get_plugin_info($component);
281             if (is_null($otherplugin)) {
282                 return false;
283             }
285             if ($requiredversion != ANY_VERSION and $otherplugin->versiondisk < $requiredversion) {
286                 return false;
287             }
288         }
290         return true;
291     }
293     /**
294      * Checks all dependencies for all installed plugins
295      *
296      * This is used by install and upgrade. The array passed by reference as the second
297      * argument is populated with the list of plugins that have failed dependencies (note that
298      * a single plugin can appear multiple times in the $failedplugins).
299      *
300      * @param int $moodleversion the version from version.php.
301      * @param array $failedplugins to return the list of plugins with non-satisfied dependencies
302      * @return bool true if all the dependencies are satisfied for all plugins.
303      */
304     public function all_plugins_ok($moodleversion, &$failedplugins = array()) {
306         $return = true;
307         foreach ($this->get_plugins() as $type => $plugins) {
308             foreach ($plugins as $plugin) {
310                 if (!$plugin->is_core_dependency_satisfied($moodleversion)) {
311                     $return = false;
312                     $failedplugins[] = $plugin->component;
313                 }
315                 if (!$this->are_dependencies_satisfied($plugin->get_other_required_plugins())) {
316                     $return = false;
317                     $failedplugins[] = $plugin->component;
318                 }
319             }
320         }
322         return $return;
323     }
325     /**
326      * Checks if there are some plugins with a known available update
327      *
328      * @return bool true if there is at least one available update
329      */
330     public function some_plugins_updatable() {
331         foreach ($this->get_plugins() as $type => $plugins) {
332             foreach ($plugins as $plugin) {
333                 if ($plugin->available_updates()) {
334                     return true;
335                 }
336             }
337         }
339         return false;
340     }
342     /**
343      * Defines a list of all plugins that were originally shipped in the standard Moodle distribution,
344      * but are not anymore and are deleted during upgrades.
345      *
346      * The main purpose of this list is to hide missing plugins during upgrade.
347      *
348      * @param string $type plugin type
349      * @param string $name plugin name
350      * @return bool
351      */
352     public static function is_deleted_standard_plugin($type, $name) {
353         static $plugins = array(
354             // do not add 1.9-2.2 plugin removals here
355         );
357         if (!isset($plugins[$type])) {
358             return false;
359         }
360         return in_array($name, $plugins[$type]);
361     }
363     /**
364      * Defines a white list of all plugins shipped in the standard Moodle distribution
365      *
366      * @param string $type
367      * @return false|array array of standard plugins or false if the type is unknown
368      */
369     public static function standard_plugins_list($type) {
370         static $standard_plugins = array(
372             'assignment' => array(
373                 'offline', 'online', 'upload', 'uploadsingle'
374             ),
376             'assignsubmission' => array(
377                 'comments', 'file', 'onlinetext'
378             ),
380             'assignfeedback' => array(
381                 'comments', 'file', 'offline'
382             ),
384             'auth' => array(
385                 'cas', 'db', 'email', 'fc', 'imap', 'ldap', 'manual', 'mnet',
386                 'nntp', 'nologin', 'none', 'pam', 'pop3', 'radius',
387                 'shibboleth', 'webservice'
388             ),
390             'block' => array(
391                 'activity_modules', 'admin_bookmarks', 'blog_menu',
392                 'blog_recent', 'blog_tags', 'calendar_month',
393                 'calendar_upcoming', 'comments', 'community',
394                 'completionstatus', 'course_list', 'course_overview',
395                 'course_summary', 'feedback', 'glossary_random', 'html',
396                 'login', 'mentees', 'messages', 'mnet_hosts', 'myprofile',
397                 'navigation', 'news_items', 'online_users', 'participants',
398                 'private_files', 'quiz_results', 'recent_activity',
399                 'rss_client', 'search_forums', 'section_links',
400                 'selfcompletion', 'settings', 'site_main_menu',
401                 'social_activities', 'tag_flickr', 'tag_youtube', 'tags'
402             ),
404             'booktool' => array(
405                 'exportimscp', 'importhtml', 'print'
406             ),
408             'coursereport' => array(
409                 //deprecated!
410             ),
412             'datafield' => array(
413                 'checkbox', 'date', 'file', 'latlong', 'menu', 'multimenu',
414                 'number', 'picture', 'radiobutton', 'text', 'textarea', 'url'
415             ),
417             'datapreset' => array(
418                 'imagegallery'
419             ),
421             'editor' => array(
422                 'textarea', 'tinymce'
423             ),
425             'enrol' => array(
426                 'authorize', 'category', 'cohort', 'database', 'flatfile',
427                 'guest', 'imsenterprise', 'ldap', 'manual', 'meta', 'mnet',
428                 'paypal', 'self'
429             ),
431             'filter' => array(
432                 'activitynames', 'algebra', 'censor', 'emailprotect',
433                 'emoticon', 'mediaplugin', 'multilang', 'tex', 'tidy',
434                 'urltolink', 'data', 'glossary'
435             ),
437             'format' => array(
438                 'scorm', 'social', 'topics', 'weeks'
439             ),
441             'gradeexport' => array(
442                 'ods', 'txt', 'xls', 'xml'
443             ),
445             'gradeimport' => array(
446                 'csv', 'xml'
447             ),
449             'gradereport' => array(
450                 'grader', 'outcomes', 'overview', 'user'
451             ),
453             'gradingform' => array(
454                 'rubric', 'guide'
455             ),
457             'local' => array(
458             ),
460             'message' => array(
461                 'email', 'jabber', 'popup'
462             ),
464             'mnetservice' => array(
465                 'enrol'
466             ),
468             'mod' => array(
469                 'assign', 'assignment', 'book', 'chat', 'choice', 'data', 'feedback', 'folder',
470                 'forum', 'glossary', 'imscp', 'label', 'lesson', 'lti', 'page',
471                 'quiz', 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop'
472             ),
474             'plagiarism' => array(
475             ),
477             'portfolio' => array(
478                 'boxnet', 'download', 'flickr', 'googledocs', 'mahara', 'picasa'
479             ),
481             'profilefield' => array(
482                 'checkbox', 'datetime', 'menu', 'text', 'textarea'
483             ),
485             'qbehaviour' => array(
486                 'adaptive', 'adaptivenopenalty', 'deferredcbm',
487                 'deferredfeedback', 'immediatecbm', 'immediatefeedback',
488                 'informationitem', 'interactive', 'interactivecountback',
489                 'manualgraded', 'missing'
490             ),
492             'qformat' => array(
493                 'aiken', 'blackboard', 'blackboard_six', 'examview', 'gift',
494                 'learnwise', 'missingword', 'multianswer', 'webct',
495                 'xhtml', 'xml'
496             ),
498             'qtype' => array(
499                 'calculated', 'calculatedmulti', 'calculatedsimple',
500                 'description', 'essay', 'match', 'missingtype', 'multianswer',
501                 'multichoice', 'numerical', 'random', 'randomsamatch',
502                 'shortanswer', 'truefalse'
503             ),
505             'quiz' => array(
506                 'grading', 'overview', 'responses', 'statistics'
507             ),
509             'quizaccess' => array(
510                 'delaybetweenattempts', 'ipaddress', 'numattempts', 'openclosedate',
511                 'password', 'safebrowser', 'securewindow', 'timelimit'
512             ),
514             'report' => array(
515                 'backups', 'completion', 'configlog', 'courseoverview',
516                 'log', 'loglive', 'outline', 'participation', 'progress', 'questioninstances', 'security', 'stats'
517             ),
519             'repository' => array(
520                 'alfresco', 'boxnet', 'coursefiles', 'dropbox', 'equella', 'filesystem',
521                 'flickr', 'flickr_public', 'googledocs', 'local', 'merlot',
522                 'picasa', 'recent', 's3', 'upload', 'url', 'user', 'webdav',
523                 'wikimedia', 'youtube'
524             ),
526             'scormreport' => array(
527                 'basic',
528                 'interactions',
529                 'graphs'
530             ),
532             'tinymce' => array(
533                 'dragmath', 'moodleemoticon', 'moodleimage', 'moodlemedia', 'moodlenolink', 'spellchecker',
534             ),
536             'theme' => array(
537                 'afterburner', 'anomaly', 'arialist', 'base', 'binarius',
538                 'boxxie', 'brick', 'canvas', 'formal_white', 'formfactor',
539                 'fusion', 'leatherbound', 'magazine', 'mymobile', 'nimble',
540                 'nonzero', 'overlay', 'serenity', 'sky_high', 'splash',
541                 'standard', 'standardold'
542             ),
544             'tool' => array(
545                 'assignmentupgrade', 'capability', 'customlang', 'dbtransfer', 'generator',
546                 'health', 'innodb', 'langimport', 'multilangupgrade', 'phpunit', 'profiling',
547                 'qeupgradehelper', 'replace', 'spamcleaner', 'timezoneimport', 'unittest',
548                 'uploaduser', 'unsuproles', 'xmldb'
549             ),
551             'webservice' => array(
552                 'amf', 'rest', 'soap', 'xmlrpc'
553             ),
555             'workshopallocation' => array(
556                 'manual', 'random', 'scheduled'
557             ),
559             'workshopeval' => array(
560                 'best'
561             ),
563             'workshopform' => array(
564                 'accumulative', 'comments', 'numerrors', 'rubric'
565             )
566         );
568         if (isset($standard_plugins[$type])) {
569             return $standard_plugins[$type];
570         } else {
571             return false;
572         }
573     }
575     /**
576      * Reorders plugin types into a sequence to be displayed
577      *
578      * For technical reasons, plugin types returned by {@link get_plugin_types()} are
579      * in a certain order that does not need to fit the expected order for the display.
580      * Particularly, activity modules should be displayed first as they represent the
581      * real heart of Moodle. They should be followed by other plugin types that are
582      * used to build the courses (as that is what one expects from LMS). After that,
583      * other supportive plugin types follow.
584      *
585      * @param array $types associative array
586      * @return array same array with altered order of items
587      */
588     protected function reorder_plugin_types(array $types) {
589         $fix = array(
590             'mod'        => $types['mod'],
591             'block'      => $types['block'],
592             'qtype'      => $types['qtype'],
593             'qbehaviour' => $types['qbehaviour'],
594             'qformat'    => $types['qformat'],
595             'filter'     => $types['filter'],
596             'enrol'      => $types['enrol'],
597         );
598         foreach ($types as $type => $path) {
599             if (!isset($fix[$type])) {
600                 $fix[$type] = $path;
601             }
602         }
603         return $fix;
604     }
608 /**
609  * General exception thrown by the {@link available_update_checker} class
610  */
611 class available_update_checker_exception extends moodle_exception {
613     /**
614      * @param string $errorcode exception description identifier
615      * @param mixed $debuginfo debugging data to display
616      */
617     public function __construct($errorcode, $debuginfo=null) {
618         parent::__construct($errorcode, 'core_plugin', '', null, print_r($debuginfo, true));
619     }
623 /**
624  * Singleton class that handles checking for available updates
625  */
626 class available_update_checker {
628     /** @var available_update_checker holds the singleton instance */
629     protected static $singletoninstance;
630     /** @var null|int the timestamp of when the most recent response was fetched */
631     protected $recentfetch = null;
632     /** @var null|array the recent response from the update notification provider */
633     protected $recentresponse = null;
634     /** @var null|string the numerical version of the local Moodle code */
635     protected $currentversion = null;
636     /** @var null|string the release info of the local Moodle code */
637     protected $currentrelease = null;
638     /** @var null|string branch of the local Moodle code */
639     protected $currentbranch = null;
640     /** @var array of (string)frankestyle => (string)version list of additional plugins deployed at this site */
641     protected $currentplugins = array();
643     /**
644      * Direct initiation not allowed, use the factory method {@link self::instance()}
645      */
646     protected function __construct() {
647     }
649     /**
650      * Sorry, this is singleton
651      */
652     protected function __clone() {
653     }
655     /**
656      * Factory method for this class
657      *
658      * @return available_update_checker the singleton instance
659      */
660     public static function instance() {
661         if (is_null(self::$singletoninstance)) {
662             self::$singletoninstance = new self();
663         }
664         return self::$singletoninstance;
665     }
667     /**
668      * Returns the timestamp of the last execution of {@link fetch()}
669      *
670      * @return int|null null if it has never been executed or we don't known
671      */
672     public function get_last_timefetched() {
674         $this->restore_response();
676         if (!empty($this->recentfetch)) {
677             return $this->recentfetch;
679         } else {
680             return null;
681         }
682     }
684     /**
685      * Fetches the available update status from the remote site
686      *
687      * @throws available_update_checker_exception
688      */
689     public function fetch() {
690         $response = $this->get_response();
691         $this->validate_response($response);
692         $this->store_response($response);
693     }
695     /**
696      * Returns the available update information for the given component
697      *
698      * This method returns null if the most recent response does not contain any information
699      * about it. The returned structure is an array of available updates for the given
700      * component. Each update info is an object with at least one property called
701      * 'version'. Other possible properties are 'release', 'maturity', 'url' and 'downloadurl'.
702      *
703      * For the 'core' component, the method returns real updates only (those with higher version).
704      * For all other components, the list of all known remote updates is returned and the caller
705      * (usually the {@link plugin_manager}) is supposed to make the actual comparison of versions.
706      *
707      * @param string $component frankenstyle
708      * @param array $options with supported keys 'minmaturity' and/or 'notifybuilds'
709      * @return null|array null or array of available_update_info objects
710      */
711     public function get_update_info($component, array $options = array()) {
713         if (!isset($options['minmaturity'])) {
714             $options['minmaturity'] = 0;
715         }
717         if (!isset($options['notifybuilds'])) {
718             $options['notifybuilds'] = false;
719         }
721         if ($component == 'core') {
722             $this->load_current_environment();
723         }
725         $this->restore_response();
727         if (empty($this->recentresponse['updates'][$component])) {
728             return null;
729         }
731         $updates = array();
732         foreach ($this->recentresponse['updates'][$component] as $info) {
733             $update = new available_update_info($component, $info);
734             if (isset($update->maturity) and ($update->maturity < $options['minmaturity'])) {
735                 continue;
736             }
737             if ($component == 'core') {
738                 if ($update->version <= $this->currentversion) {
739                     continue;
740                 }
741                 if (empty($options['notifybuilds']) and $this->is_same_release($update->release)) {
742                     continue;
743                 }
744             }
745             $updates[] = $update;
746         }
748         if (empty($updates)) {
749             return null;
750         }
752         return $updates;
753     }
755     /**
756      * The method being run via cron.php
757      */
758     public function cron() {
759         global $CFG;
761         if (!$this->cron_autocheck_enabled()) {
762             $this->cron_mtrace('Automatic check for available updates not enabled, skipping.');
763             return;
764         }
766         $now = $this->cron_current_timestamp();
768         if ($this->cron_has_fresh_fetch($now)) {
769             $this->cron_mtrace('Recently fetched info about available updates is still fresh enough, skipping.');
770             return;
771         }
773         if ($this->cron_has_outdated_fetch($now)) {
774             $this->cron_mtrace('Outdated or missing info about available updates, forced fetching ... ', '');
775             $this->cron_execute();
776             return;
777         }
779         $offset = $this->cron_execution_offset();
780         $start = mktime(1, 0, 0, date('n', $now), date('j', $now), date('Y', $now)); // 01:00 AM today local time
781         if ($now > $start + $offset) {
782             $this->cron_mtrace('Regular daily check for available updates ... ', '');
783             $this->cron_execute();
784             return;
785         }
786     }
788     /// end of public API //////////////////////////////////////////////////////
790     /**
791      * Makes cURL request to get data from the remote site
792      *
793      * @return string raw request result
794      * @throws available_update_checker_exception
795      */
796     protected function get_response() {
797         global $CFG;
798         require_once($CFG->libdir.'/filelib.php');
800         $curl = new curl(array('proxy' => true));
801         $response = $curl->post($this->prepare_request_url(), $this->prepare_request_params());
802         $curlinfo = $curl->get_info();
803         if ($curlinfo['http_code'] != 200) {
804             throw new available_update_checker_exception('err_response_http_code', $curlinfo['http_code']);
805         }
806         return $response;
807     }
809     /**
810      * Makes sure the response is valid, has correct API format etc.
811      *
812      * @param string $response raw response as returned by the {@link self::get_response()}
813      * @throws available_update_checker_exception
814      */
815     protected function validate_response($response) {
817         $response = $this->decode_response($response);
819         if (empty($response)) {
820             throw new available_update_checker_exception('err_response_empty');
821         }
823         if (empty($response['status']) or $response['status'] !== 'OK') {
824             throw new available_update_checker_exception('err_response_status', $response['status']);
825         }
827         if (empty($response['apiver']) or $response['apiver'] !== '1.0') {
828             throw new available_update_checker_exception('err_response_format_version', $response['apiver']);
829         }
831         if (empty($response['forbranch']) or $response['forbranch'] !== moodle_major_version(true)) {
832             throw new available_update_checker_exception('err_response_target_version', $response['forbranch']);
833         }
834     }
836     /**
837      * Decodes the raw string response from the update notifications provider
838      *
839      * @param string $response as returned by {@link self::get_response()}
840      * @return array decoded response structure
841      */
842     protected function decode_response($response) {
843         return json_decode($response, true);
844     }
846     /**
847      * Stores the valid fetched response for later usage
848      *
849      * This implementation uses the config_plugins table as the permanent storage.
850      *
851      * @param string $response raw valid data returned by {@link self::get_response()}
852      */
853     protected function store_response($response) {
855         set_config('recentfetch', time(), 'core_plugin');
856         set_config('recentresponse', $response, 'core_plugin');
858         $this->restore_response(true);
859     }
861     /**
862      * Loads the most recent raw response record we have fetched
863      *
864      * After this method is called, $this->recentresponse is set to an array. If the
865      * array is empty, then either no data have been fetched yet or the fetched data
866      * do not have expected format (and thence they are ignored and a debugging
867      * message is displayed).
868      *
869      * This implementation uses the config_plugins table as the permanent storage.
870      *
871      * @param bool $forcereload reload even if it was already loaded
872      */
873     protected function restore_response($forcereload = false) {
875         if (!$forcereload and !is_null($this->recentresponse)) {
876             // we already have it, nothing to do
877             return;
878         }
880         $config = get_config('core_plugin');
882         if (!empty($config->recentresponse) and !empty($config->recentfetch)) {
883             try {
884                 $this->validate_response($config->recentresponse);
885                 $this->recentfetch = $config->recentfetch;
886                 $this->recentresponse = $this->decode_response($config->recentresponse);
887             } catch (available_update_checker_exception $e) {
888                 debugging('Invalid info about available updates detected and will be ignored: '.$e->getMessage(), DEBUG_ALL);
889                 $this->recentresponse = array();
890             }
892         } else {
893             $this->recentresponse = array();
894         }
895     }
897     /**
898      * Compares two raw {@link $recentresponse} records and returns the list of changed updates
899      *
900      * This method is used to populate potential update info to be sent to site admins.
901      *
902      * @param array $old
903      * @param array $new
904      * @throws available_update_checker_exception
905      * @return array parts of $new['updates'] that have changed
906      */
907     protected function compare_responses(array $old, array $new) {
909         if (empty($new)) {
910             return array();
911         }
913         if (!array_key_exists('updates', $new)) {
914             throw new available_update_checker_exception('err_response_format');
915         }
917         if (empty($old)) {
918             return $new['updates'];
919         }
921         if (!array_key_exists('updates', $old)) {
922             throw new available_update_checker_exception('err_response_format');
923         }
925         $changes = array();
927         foreach ($new['updates'] as $newcomponent => $newcomponentupdates) {
928             if (empty($old['updates'][$newcomponent])) {
929                 $changes[$newcomponent] = $newcomponentupdates;
930                 continue;
931             }
932             foreach ($newcomponentupdates as $newcomponentupdate) {
933                 $inold = false;
934                 foreach ($old['updates'][$newcomponent] as $oldcomponentupdate) {
935                     if ($newcomponentupdate['version'] == $oldcomponentupdate['version']) {
936                         $inold = true;
937                     }
938                 }
939                 if (!$inold) {
940                     if (!isset($changes[$newcomponent])) {
941                         $changes[$newcomponent] = array();
942                     }
943                     $changes[$newcomponent][] = $newcomponentupdate;
944                 }
945             }
946         }
948         return $changes;
949     }
951     /**
952      * Returns the URL to send update requests to
953      *
954      * During the development or testing, you can set $CFG->alternativeupdateproviderurl
955      * to a custom URL that will be used. Otherwise the standard URL will be returned.
956      *
957      * @return string URL
958      */
959     protected function prepare_request_url() {
960         global $CFG;
962         if (!empty($CFG->alternativeupdateproviderurl)) {
963             return $CFG->alternativeupdateproviderurl;
964         } else {
965             return 'http://download.moodle.org/api/1.0/updates.php';
966         }
967     }
969     /**
970      * Sets the properties currentversion, currentrelease, currentbranch and currentplugins
971      *
972      * @param bool $forcereload
973      */
974     protected function load_current_environment($forcereload=false) {
975         global $CFG;
977         if (!is_null($this->currentversion) and !$forcereload) {
978             // nothing to do
979             return;
980         }
982         $version = null;
983         $release = null;
985         require($CFG->dirroot.'/version.php');
986         $this->currentversion = $version;
987         $this->currentrelease = $release;
988         $this->currentbranch = moodle_major_version(true);
990         $pluginman = plugin_manager::instance();
991         foreach ($pluginman->get_plugins() as $type => $plugins) {
992             foreach ($plugins as $plugin) {
993                 if (!$plugin->is_standard()) {
994                     $this->currentplugins[$plugin->component] = $plugin->versiondisk;
995                 }
996             }
997         }
998     }
1000     /**
1001      * Returns the list of HTTP params to be sent to the updates provider URL
1002      *
1003      * @return array of (string)param => (string)value
1004      */
1005     protected function prepare_request_params() {
1006         global $CFG;
1008         $this->load_current_environment();
1009         $this->restore_response();
1011         $params = array();
1012         $params['format'] = 'json';
1014         if (isset($this->recentresponse['ticket'])) {
1015             $params['ticket'] = $this->recentresponse['ticket'];
1016         }
1018         if (isset($this->currentversion)) {
1019             $params['version'] = $this->currentversion;
1020         } else {
1021             throw new coding_exception('Main Moodle version must be already known here');
1022         }
1024         if (isset($this->currentbranch)) {
1025             $params['branch'] = $this->currentbranch;
1026         } else {
1027             throw new coding_exception('Moodle release must be already known here');
1028         }
1030         $plugins = array();
1031         foreach ($this->currentplugins as $plugin => $version) {
1032             $plugins[] = $plugin.'@'.$version;
1033         }
1034         if (!empty($plugins)) {
1035             $params['plugins'] = implode(',', $plugins);
1036         }
1038         return $params;
1039     }
1041     /**
1042      * Returns the current timestamp
1043      *
1044      * @return int the timestamp
1045      */
1046     protected function cron_current_timestamp() {
1047         return time();
1048     }
1050     /**
1051      * Output cron debugging info
1052      *
1053      * @see mtrace()
1054      * @param string $msg output message
1055      * @param string $eol end of line
1056      */
1057     protected function cron_mtrace($msg, $eol = PHP_EOL) {
1058         mtrace($msg, $eol);
1059     }
1061     /**
1062      * Decide if the autocheck feature is disabled in the server setting
1063      *
1064      * @return bool true if autocheck enabled, false if disabled
1065      */
1066     protected function cron_autocheck_enabled() {
1067         global $CFG;
1069         if (empty($CFG->updateautocheck)) {
1070             return false;
1071         } else {
1072             return true;
1073         }
1074     }
1076     /**
1077      * Decide if the recently fetched data are still fresh enough
1078      *
1079      * @param int $now current timestamp
1080      * @return bool true if no need to re-fetch, false otherwise
1081      */
1082     protected function cron_has_fresh_fetch($now) {
1083         $recent = $this->get_last_timefetched();
1085         if (empty($recent)) {
1086             return false;
1087         }
1089         if ($now < $recent) {
1090             $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1091             return true;
1092         }
1094         if ($now - $recent > 24 * HOURSECS) {
1095             return false;
1096         }
1098         return true;
1099     }
1101     /**
1102      * Decide if the fetch is outadated or even missing
1103      *
1104      * @param int $now current timestamp
1105      * @return bool false if no need to re-fetch, true otherwise
1106      */
1107     protected function cron_has_outdated_fetch($now) {
1108         $recent = $this->get_last_timefetched();
1110         if (empty($recent)) {
1111             return true;
1112         }
1114         if ($now < $recent) {
1115             $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1116             return false;
1117         }
1119         if ($now - $recent > 48 * HOURSECS) {
1120             return true;
1121         }
1123         return false;
1124     }
1126     /**
1127      * Returns the cron execution offset for this site
1128      *
1129      * The main {@link self::cron()} is supposed to run every night in some random time
1130      * between 01:00 and 06:00 AM (local time). The exact moment is defined by so called
1131      * execution offset, that is the amount of time after 01:00 AM. The offset value is
1132      * initially generated randomly and then used consistently at the site. This way, the
1133      * regular checks against the download.moodle.org server are spread in time.
1134      *
1135      * @return int the offset number of seconds from range 1 sec to 5 hours
1136      */
1137     protected function cron_execution_offset() {
1138         global $CFG;
1140         if (empty($CFG->updatecronoffset)) {
1141             set_config('updatecronoffset', rand(1, 5 * HOURSECS));
1142         }
1144         return $CFG->updatecronoffset;
1145     }
1147     /**
1148      * Fetch available updates info and eventually send notification to site admins
1149      */
1150     protected function cron_execute() {
1152         try {
1153             $this->restore_response();
1154             $previous = $this->recentresponse;
1155             $this->fetch();
1156             $this->restore_response(true);
1157             $current = $this->recentresponse;
1158             $changes = $this->compare_responses($previous, $current);
1159             $notifications = $this->cron_notifications($changes);
1160             $this->cron_notify($notifications);
1161             $this->cron_mtrace('done');
1162         } catch (available_update_checker_exception $e) {
1163             $this->cron_mtrace('FAILED!');
1164         }
1165     }
1167     /**
1168      * Given the list of changes in available updates, pick those to send to site admins
1169      *
1170      * @param array $changes as returned by {@link self::compare_responses()}
1171      * @return array of available_update_info objects to send to site admins
1172      */
1173     protected function cron_notifications(array $changes) {
1174         global $CFG;
1176         $notifications = array();
1177         $pluginman = plugin_manager::instance();
1178         $plugins = $pluginman->get_plugins(true);
1180         foreach ($changes as $component => $componentchanges) {
1181             if (empty($componentchanges)) {
1182                 continue;
1183             }
1184             $componentupdates = $this->get_update_info($component,
1185                 array('minmaturity' => $CFG->updateminmaturity, 'notifybuilds' => $CFG->updatenotifybuilds));
1186             if (empty($componentupdates)) {
1187                 continue;
1188             }
1189             // notify only about those $componentchanges that are present in $componentupdates
1190             // to respect the preferences
1191             foreach ($componentchanges as $componentchange) {
1192                 foreach ($componentupdates as $componentupdate) {
1193                     if ($componentupdate->version == $componentchange['version']) {
1194                         if ($component == 'core') {
1195                             // in case of 'core' this is enough, we already know that the
1196                             // $componentupdate is a real update with higher version
1197                             $notifications[] = $componentupdate;
1198                         } else {
1199                             // use the plugin_manager to check if the reported $componentchange
1200                             // is a real update with higher version. such a real update must be
1201                             // present in the 'availableupdates' property of one of the component's
1202                             // available_update_info object
1203                             list($plugintype, $pluginname) = normalize_component($component);
1204                             if (!empty($plugins[$plugintype][$pluginname]->availableupdates)) {
1205                                 foreach ($plugins[$plugintype][$pluginname]->availableupdates as $availableupdate) {
1206                                     if ($availableupdate->version == $componentchange['version']) {
1207                                         $notifications[] = $componentupdate;
1208                                     }
1209                                 }
1210                             }
1211                         }
1212                     }
1213                 }
1214             }
1215         }
1217         return $notifications;
1218     }
1220     /**
1221      * Sends the given notifications to site admins via messaging API
1222      *
1223      * @param array $notifications array of available_update_info objects to send
1224      */
1225     protected function cron_notify(array $notifications) {
1226         global $CFG;
1228         if (empty($notifications)) {
1229             return;
1230         }
1232         $admins = get_admins();
1234         if (empty($admins)) {
1235             return;
1236         }
1238         $this->cron_mtrace('sending notifications ... ', '');
1240         $text = get_string('updatenotifications', 'core_admin') . PHP_EOL;
1241         $html = html_writer::tag('h1', get_string('updatenotifications', 'core_admin')) . PHP_EOL;
1243         $coreupdates = array();
1244         $pluginupdates = array();
1246         foreach ($notifications as $notification) {
1247             if ($notification->component == 'core') {
1248                 $coreupdates[] = $notification;
1249             } else {
1250                 $pluginupdates[] = $notification;
1251             }
1252         }
1254         if (!empty($coreupdates)) {
1255             $text .= PHP_EOL . get_string('updateavailable', 'core_admin') . PHP_EOL;
1256             $html .= html_writer::tag('h2', get_string('updateavailable', 'core_admin')) . PHP_EOL;
1257             $html .= html_writer::start_tag('ul') . PHP_EOL;
1258             foreach ($coreupdates as $coreupdate) {
1259                 $html .= html_writer::start_tag('li');
1260                 if (isset($coreupdate->release)) {
1261                     $text .= get_string('updateavailable_release', 'core_admin', $coreupdate->release);
1262                     $html .= html_writer::tag('strong', get_string('updateavailable_release', 'core_admin', $coreupdate->release));
1263                 }
1264                 if (isset($coreupdate->version)) {
1265                     $text .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
1266                     $html .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
1267                 }
1268                 if (isset($coreupdate->maturity)) {
1269                     $text .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
1270                     $html .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
1271                 }
1272                 $text .= PHP_EOL;
1273                 $html .= html_writer::end_tag('li') . PHP_EOL;
1274             }
1275             $text .= PHP_EOL;
1276             $html .= html_writer::end_tag('ul') . PHP_EOL;
1278             $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/index.php');
1279             $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
1280             $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/index.php', $CFG->wwwroot.'/'.$CFG->admin.'/index.php'));
1281             $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
1282         }
1284         if (!empty($pluginupdates)) {
1285             $text .= PHP_EOL . get_string('updateavailableforplugin', 'core_admin') . PHP_EOL;
1286             $html .= html_writer::tag('h2', get_string('updateavailableforplugin', 'core_admin')) . PHP_EOL;
1288             $html .= html_writer::start_tag('ul') . PHP_EOL;
1289             foreach ($pluginupdates as $pluginupdate) {
1290                 $html .= html_writer::start_tag('li');
1291                 $text .= get_string('pluginname', $pluginupdate->component);
1292                 $html .= html_writer::tag('strong', get_string('pluginname', $pluginupdate->component));
1294                 $text .= ' ('.$pluginupdate->component.')';
1295                 $html .= ' ('.$pluginupdate->component.')';
1297                 $text .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
1298                 $html .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
1300                 $text .= PHP_EOL;
1301                 $html .= html_writer::end_tag('li') . PHP_EOL;
1302             }
1303             $text .= PHP_EOL;
1304             $html .= html_writer::end_tag('ul') . PHP_EOL;
1306             $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php');
1307             $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
1308             $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/plugins.php', $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php'));
1309             $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
1310         }
1312         $a = array('siteurl' => $CFG->wwwroot);
1313         $text .= get_string('updatenotificationfooter', 'core_admin', $a) . PHP_EOL;
1314         $a = array('siteurl' => html_writer::link($CFG->wwwroot, $CFG->wwwroot));
1315         $html .= html_writer::tag('footer', html_writer::tag('p', get_string('updatenotificationfooter', 'core_admin', $a),
1316             array('style' => 'font-size:smaller; color:#333;')));
1318         $mainadmin = reset($admins);
1320         foreach ($admins as $admin) {
1321             $message = new stdClass();
1322             $message->component         = 'moodle';
1323             $message->name              = 'availableupdate';
1324             $message->userfrom          = $mainadmin;
1325             $message->userto            = $admin;
1326             $message->subject           = get_string('updatenotifications', 'core_admin');
1327             $message->fullmessage       = $text;
1328             $message->fullmessageformat = FORMAT_PLAIN;
1329             $message->fullmessagehtml   = $html;
1330             $message->smallmessage      = get_string('updatenotifications', 'core_admin');
1331             $message->notification      = 1;
1332             message_send($message);
1333         }
1334     }
1336     /**
1337      * Compare two release labels and decide if they are the same
1338      *
1339      * @param string $remote release info of the available update
1340      * @param null|string $local release info of the local code, defaults to $release defined in version.php
1341      * @return boolean true if the releases declare the same minor+major version
1342      */
1343     protected function is_same_release($remote, $local=null) {
1345         if (is_null($local)) {
1346             $this->load_current_environment();
1347             $local = $this->currentrelease;
1348         }
1350         $pattern = '/^([0-9\.\+]+)([^(]*)/';
1352         preg_match($pattern, $remote, $remotematches);
1353         preg_match($pattern, $local, $localmatches);
1355         $remotematches[1] = str_replace('+', '', $remotematches[1]);
1356         $localmatches[1] = str_replace('+', '', $localmatches[1]);
1358         if ($remotematches[1] === $localmatches[1] and rtrim($remotematches[2]) === rtrim($localmatches[2])) {
1359             return true;
1360         } else {
1361             return false;
1362         }
1363     }
1367 /**
1368  * Defines the structure of objects returned by {@link available_update_checker::get_update_info()}
1369  */
1370 class available_update_info {
1372     /** @var string frankenstyle component name */
1373     public $component;
1374     /** @var int the available version of the component */
1375     public $version;
1376     /** @var string|null optional release name */
1377     public $release = null;
1378     /** @var int|null optional maturity info, eg {@link MATURITY_STABLE} */
1379     public $maturity = null;
1380     /** @var string|null optional URL of a page with more info about the update */
1381     public $url = null;
1382     /** @var string|null optional URL of a ZIP package that can be downloaded and installed */
1383     public $download = null;
1385     /**
1386      * Creates new instance of the class
1387      *
1388      * The $info array must provide at least the 'version' value and optionally all other
1389      * values to populate the object's properties.
1390      *
1391      * @param string $name the frankenstyle component name
1392      * @param array $info associative array with other properties
1393      */
1394     public function __construct($name, array $info) {
1395         $this->component = $name;
1396         foreach ($info as $k => $v) {
1397             if (property_exists('available_update_info', $k) and $k != 'component') {
1398                 $this->$k = $v;
1399             }
1400         }
1401     }
1405 /**
1406  * Factory class producing required subclasses of {@link plugininfo_base}
1407  */
1408 class plugininfo_default_factory {
1410     /**
1411      * Makes a new instance of the plugininfo class
1412      *
1413      * @param string $type the plugin type, eg. 'mod'
1414      * @param string $typerootdir full path to the location of all the plugins of this type
1415      * @param string $name the plugin name, eg. 'workshop'
1416      * @param string $namerootdir full path to the location of the plugin
1417      * @param string $typeclass the name of class that holds the info about the plugin
1418      * @return plugininfo_base the instance of $typeclass
1419      */
1420     public static function make($type, $typerootdir, $name, $namerootdir, $typeclass) {
1421         $plugin              = new $typeclass();
1422         $plugin->type        = $type;
1423         $plugin->typerootdir = $typerootdir;
1424         $plugin->name        = $name;
1425         $plugin->rootdir     = $namerootdir;
1427         $plugin->init_display_name();
1428         $plugin->load_disk_version();
1429         $plugin->load_db_version();
1430         $plugin->load_required_main_version();
1431         $plugin->init_is_standard();
1433         return $plugin;
1434     }
1438 /**
1439  * Base class providing access to the information about a plugin
1440  *
1441  * @property-read string component the component name, type_name
1442  */
1443 abstract class plugininfo_base {
1445     /** @var string the plugintype name, eg. mod, auth or workshopform */
1446     public $type;
1447     /** @var string full path to the location of all the plugins of this type */
1448     public $typerootdir;
1449     /** @var string the plugin name, eg. assignment, ldap */
1450     public $name;
1451     /** @var string the localized plugin name */
1452     public $displayname;
1453     /** @var string the plugin source, one of plugin_manager::PLUGIN_SOURCE_xxx constants */
1454     public $source;
1455     /** @var fullpath to the location of this plugin */
1456     public $rootdir;
1457     /** @var int|string the version of the plugin's source code */
1458     public $versiondisk;
1459     /** @var int|string the version of the installed plugin */
1460     public $versiondb;
1461     /** @var int|float|string required version of Moodle core  */
1462     public $versionrequires;
1463     /** @var array other plugins that this one depends on, lazy-loaded by {@link get_other_required_plugins()} */
1464     public $dependencies;
1465     /** @var int number of instances of the plugin - not supported yet */
1466     public $instances;
1467     /** @var int order of the plugin among other plugins of the same type - not supported yet */
1468     public $sortorder;
1469     /** @var array|null array of {@link available_update_info} for this plugin */
1470     public $availableupdates;
1472     /**
1473      * Gathers and returns the information about all plugins of the given type
1474      *
1475      * @param string $type the name of the plugintype, eg. mod, auth or workshopform
1476      * @param string $typerootdir full path to the location of the plugin dir
1477      * @param string $typeclass the name of the actually called class
1478      * @return array of plugintype classes, indexed by the plugin name
1479      */
1480     public static function get_plugins($type, $typerootdir, $typeclass) {
1482         // get the information about plugins at the disk
1483         $plugins = get_plugin_list($type);
1484         $ondisk = array();
1485         foreach ($plugins as $pluginname => $pluginrootdir) {
1486             $ondisk[$pluginname] = plugininfo_default_factory::make($type, $typerootdir,
1487                 $pluginname, $pluginrootdir, $typeclass);
1488         }
1489         return $ondisk;
1490     }
1492     /**
1493      * Sets {@link $displayname} property to a localized name of the plugin
1494      */
1495     public function init_display_name() {
1496         if (!get_string_manager()->string_exists('pluginname', $this->component)) {
1497             $this->displayname = '[pluginname,' . $this->component . ']';
1498         } else {
1499             $this->displayname = get_string('pluginname', $this->component);
1500         }
1501     }
1503     /**
1504      * Magic method getter, redirects to read only values.
1505      *
1506      * @param string $name
1507      * @return mixed
1508      */
1509     public function __get($name) {
1510         switch ($name) {
1511             case 'component': return $this->type . '_' . $this->name;
1513             default:
1514                 debugging('Invalid plugin property accessed! '.$name);
1515                 return null;
1516         }
1517     }
1519     /**
1520      * Return the full path name of a file within the plugin.
1521      *
1522      * No check is made to see if the file exists.
1523      *
1524      * @param string $relativepath e.g. 'version.php'.
1525      * @return string e.g. $CFG->dirroot . '/mod/quiz/version.php'.
1526      */
1527     public function full_path($relativepath) {
1528         if (empty($this->rootdir)) {
1529             return '';
1530         }
1531         return $this->rootdir . '/' . $relativepath;
1532     }
1534     /**
1535      * Load the data from version.php.
1536      *
1537      * @return stdClass the object called $plugin defined in version.php
1538      */
1539     protected function load_version_php() {
1540         $versionfile = $this->full_path('version.php');
1542         $plugin = new stdClass();
1543         if (is_readable($versionfile)) {
1544             include($versionfile);
1545         }
1546         return $plugin;
1547     }
1549     /**
1550      * Sets {@link $versiondisk} property to a numerical value representing the
1551      * version of the plugin's source code.
1552      *
1553      * If the value is null after calling this method, either the plugin
1554      * does not use versioning (typically does not have any database
1555      * data) or is missing from disk.
1556      */
1557     public function load_disk_version() {
1558         $plugin = $this->load_version_php();
1559         if (isset($plugin->version)) {
1560             $this->versiondisk = $plugin->version;
1561         }
1562     }
1564     /**
1565      * Sets {@link $versionrequires} property to a numerical value representing
1566      * the version of Moodle core that this plugin requires.
1567      */
1568     public function load_required_main_version() {
1569         $plugin = $this->load_version_php();
1570         if (isset($plugin->requires)) {
1571             $this->versionrequires = $plugin->requires;
1572         }
1573     }
1575     /**
1576      * Initialise {@link $dependencies} to the list of other plugins (in any)
1577      * that this one requires to be installed.
1578      */
1579     protected function load_other_required_plugins() {
1580         $plugin = $this->load_version_php();
1581         if (!empty($plugin->dependencies)) {
1582             $this->dependencies = $plugin->dependencies;
1583         } else {
1584             $this->dependencies = array(); // By default, no dependencies.
1585         }
1586     }
1588     /**
1589      * Get the list of other plugins that this plugin requires to be installed.
1590      *
1591      * @return array with keys the frankenstyle plugin name, and values either
1592      *      a version string (like '2011101700') or the constant ANY_VERSION.
1593      */
1594     public function get_other_required_plugins() {
1595         if (is_null($this->dependencies)) {
1596             $this->load_other_required_plugins();
1597         }
1598         return $this->dependencies;
1599     }
1601     /**
1602      * Sets {@link $versiondb} property to a numerical value representing the
1603      * currently installed version of the plugin.
1604      *
1605      * If the value is null after calling this method, either the plugin
1606      * does not use versioning (typically does not have any database
1607      * data) or has not been installed yet.
1608      */
1609     public function load_db_version() {
1610         if ($ver = self::get_version_from_config_plugins($this->component)) {
1611             $this->versiondb = $ver;
1612         }
1613     }
1615     /**
1616      * Sets {@link $source} property to one of plugin_manager::PLUGIN_SOURCE_xxx
1617      * constants.
1618      *
1619      * If the property's value is null after calling this method, then
1620      * the type of the plugin has not been recognized and you should throw
1621      * an exception.
1622      */
1623     public function init_is_standard() {
1625         $standard = plugin_manager::standard_plugins_list($this->type);
1627         if ($standard !== false) {
1628             $standard = array_flip($standard);
1629             if (isset($standard[$this->name])) {
1630                 $this->source = plugin_manager::PLUGIN_SOURCE_STANDARD;
1631             } else if (!is_null($this->versiondb) and is_null($this->versiondisk)
1632                     and plugin_manager::is_deleted_standard_plugin($this->type, $this->name)) {
1633                 $this->source = plugin_manager::PLUGIN_SOURCE_STANDARD; // to be deleted
1634             } else {
1635                 $this->source = plugin_manager::PLUGIN_SOURCE_EXTENSION;
1636             }
1637         }
1638     }
1640     /**
1641      * Returns true if the plugin is shipped with the official distribution
1642      * of the current Moodle version, false otherwise.
1643      *
1644      * @return bool
1645      */
1646     public function is_standard() {
1647         return $this->source === plugin_manager::PLUGIN_SOURCE_STANDARD;
1648     }
1650     /**
1651      * Returns true if the the given Moodle version is enough to run this plugin
1652      *
1653      * @param string|int|double $moodleversion
1654      * @return bool
1655      */
1656     public function is_core_dependency_satisfied($moodleversion) {
1658         if (empty($this->versionrequires)) {
1659             return true;
1661         } else {
1662             return (double)$this->versionrequires <= (double)$moodleversion;
1663         }
1664     }
1666     /**
1667      * Returns the status of the plugin
1668      *
1669      * @return string one of plugin_manager::PLUGIN_STATUS_xxx constants
1670      */
1671     public function get_status() {
1673         if (is_null($this->versiondb) and is_null($this->versiondisk)) {
1674             return plugin_manager::PLUGIN_STATUS_NODB;
1676         } else if (is_null($this->versiondb) and !is_null($this->versiondisk)) {
1677             return plugin_manager::PLUGIN_STATUS_NEW;
1679         } else if (!is_null($this->versiondb) and is_null($this->versiondisk)) {
1680             if (plugin_manager::is_deleted_standard_plugin($this->type, $this->name)) {
1681                 return plugin_manager::PLUGIN_STATUS_DELETE;
1682             } else {
1683                 return plugin_manager::PLUGIN_STATUS_MISSING;
1684             }
1686         } else if ((string)$this->versiondb === (string)$this->versiondisk) {
1687             return plugin_manager::PLUGIN_STATUS_UPTODATE;
1689         } else if ($this->versiondb < $this->versiondisk) {
1690             return plugin_manager::PLUGIN_STATUS_UPGRADE;
1692         } else if ($this->versiondb > $this->versiondisk) {
1693             return plugin_manager::PLUGIN_STATUS_DOWNGRADE;
1695         } else {
1696             // $version = pi(); and similar funny jokes - hopefully Donald E. Knuth will never contribute to Moodle ;-)
1697             throw new coding_exception('Unable to determine plugin state, check the plugin versions');
1698         }
1699     }
1701     /**
1702      * Returns the information about plugin availability
1703      *
1704      * True means that the plugin is enabled. False means that the plugin is
1705      * disabled. Null means that the information is not available, or the
1706      * plugin does not support configurable availability or the availability
1707      * can not be changed.
1708      *
1709      * @return null|bool
1710      */
1711     public function is_enabled() {
1712         return null;
1713     }
1715     /**
1716      * Populates the property {@link $availableupdates} with the information provided by
1717      * available update checker
1718      *
1719      * @param available_update_checker $provider the class providing the available update info
1720      */
1721     public function check_available_updates(available_update_checker $provider) {
1722         global $CFG;
1724         if (isset($CFG->updateminmaturity)) {
1725             $minmaturity = $CFG->updateminmaturity;
1726         } else {
1727             // this can happen during the very first upgrade to 2.3
1728             $minmaturity = MATURITY_STABLE;
1729         }
1731         $this->availableupdates = $provider->get_update_info($this->component,
1732             array('minmaturity' => $minmaturity));
1733     }
1735     /**
1736      * If there are updates for this plugin available, returns them.
1737      *
1738      * Returns array of {@link available_update_info} objects, if some update
1739      * is available. Returns null if there is no update available or if the update
1740      * availability is unknown.
1741      *
1742      * @return array|null
1743      */
1744     public function available_updates() {
1746         if (empty($this->availableupdates) or !is_array($this->availableupdates)) {
1747             return null;
1748         }
1750         $updates = array();
1752         foreach ($this->availableupdates as $availableupdate) {
1753             if ($availableupdate->version > $this->versiondisk) {
1754                 $updates[] = $availableupdate;
1755             }
1756         }
1758         if (empty($updates)) {
1759             return null;
1760         }
1762         return $updates;
1763     }
1765     /**
1766      * Returns the node name used in admin settings menu for this plugin settings (if applicable)
1767      *
1768      * @return null|string node name or null if plugin does not create settings node (default)
1769      */
1770     public function get_settings_section_name() {
1771         return null;
1772     }
1774     /**
1775      * Returns the URL of the plugin settings screen
1776      *
1777      * Null value means that the plugin either does not have the settings screen
1778      * or its location is not available via this library.
1779      *
1780      * @return null|moodle_url
1781      */
1782     public function get_settings_url() {
1783         $section = $this->get_settings_section_name();
1784         if ($section === null) {
1785             return null;
1786         }
1787         $settings = admin_get_root()->locate($section);
1788         if ($settings && $settings instanceof admin_settingpage) {
1789             return new moodle_url('/admin/settings.php', array('section' => $section));
1790         } else if ($settings && $settings instanceof admin_externalpage) {
1791             return new moodle_url($settings->url);
1792         } else {
1793             return null;
1794         }
1795     }
1797     /**
1798      * Loads plugin settings to the settings tree
1799      *
1800      * This function usually includes settings.php file in plugins folder.
1801      * Alternatively it can create a link to some settings page (instance of admin_externalpage)
1802      *
1803      * @param part_of_admin_tree $adminroot
1804      * @param string $parentnodename
1805      * @param bool $hassiteconfig whether the current user has moodle/site:config capability
1806      */
1807     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
1808     }
1810     /**
1811      * Returns the URL of the screen where this plugin can be uninstalled
1812      *
1813      * Visiting that URL must be safe, that is a manual confirmation is needed
1814      * for actual uninstallation of the plugin. Null value means that the
1815      * plugin either does not support uninstallation, or does not require any
1816      * database cleanup or the location of the screen is not available via this
1817      * library.
1818      *
1819      * @return null|moodle_url
1820      */
1821     public function get_uninstall_url() {
1822         return null;
1823     }
1825     /**
1826      * Returns relative directory of the plugin with heading '/'
1827      *
1828      * @return string
1829      */
1830     public function get_dir() {
1831         global $CFG;
1833         return substr($this->rootdir, strlen($CFG->dirroot));
1834     }
1836     /**
1837      * Provides access to plugin versions from {config_plugins}
1838      *
1839      * @param string $plugin plugin name
1840      * @param double $disablecache optional, defaults to false
1841      * @return int|false the stored value or false if not found
1842      */
1843     protected function get_version_from_config_plugins($plugin, $disablecache=false) {
1844         global $DB;
1845         static $pluginversions = null;
1847         if (is_null($pluginversions) or $disablecache) {
1848             try {
1849                 $pluginversions = $DB->get_records_menu('config_plugins', array('name' => 'version'), 'plugin', 'plugin,value');
1850             } catch (dml_exception $e) {
1851                 // before install
1852                 $pluginversions = array();
1853             }
1854         }
1856         if (!array_key_exists($plugin, $pluginversions)) {
1857             return false;
1858         }
1860         return $pluginversions[$plugin];
1861     }
1865 /**
1866  * General class for all plugin types that do not have their own class
1867  */
1868 class plugininfo_general extends plugininfo_base {
1872 /**
1873  * Class for page side blocks
1874  */
1875 class plugininfo_block extends plugininfo_base {
1877     public static function get_plugins($type, $typerootdir, $typeclass) {
1879         // get the information about blocks at the disk
1880         $blocks = parent::get_plugins($type, $typerootdir, $typeclass);
1882         // add blocks missing from disk
1883         $blocksinfo = self::get_blocks_info();
1884         foreach ($blocksinfo as $blockname => $blockinfo) {
1885             if (isset($blocks[$blockname])) {
1886                 continue;
1887             }
1888             $plugin                 = new $typeclass();
1889             $plugin->type           = $type;
1890             $plugin->typerootdir    = $typerootdir;
1891             $plugin->name           = $blockname;
1892             $plugin->rootdir        = null;
1893             $plugin->displayname    = $blockname;
1894             $plugin->versiondb      = $blockinfo->version;
1895             $plugin->init_is_standard();
1897             $blocks[$blockname]   = $plugin;
1898         }
1900         return $blocks;
1901     }
1903     public function init_display_name() {
1905         if (get_string_manager()->string_exists('pluginname', 'block_' . $this->name)) {
1906             $this->displayname = get_string('pluginname', 'block_' . $this->name);
1908         } else if (($block = block_instance($this->name)) !== false) {
1909             $this->displayname = $block->get_title();
1911         } else {
1912             parent::init_display_name();
1913         }
1914     }
1916     public function load_db_version() {
1917         global $DB;
1919         $blocksinfo = self::get_blocks_info();
1920         if (isset($blocksinfo[$this->name]->version)) {
1921             $this->versiondb = $blocksinfo[$this->name]->version;
1922         }
1923     }
1925     public function is_enabled() {
1927         $blocksinfo = self::get_blocks_info();
1928         if (isset($blocksinfo[$this->name]->visible)) {
1929             if ($blocksinfo[$this->name]->visible) {
1930                 return true;
1931             } else {
1932                 return false;
1933             }
1934         } else {
1935             return parent::is_enabled();
1936         }
1937     }
1939     public function get_settings_url() {
1941         if (($block = block_instance($this->name)) === false) {
1942             return parent::get_settings_url();
1944         } else if ($block->has_config()) {
1945             if (file_exists($this->full_path('settings.php'))) {
1946                 return new moodle_url('/admin/settings.php', array('section' => 'blocksetting' . $this->name));
1947             } else {
1948                 $blocksinfo = self::get_blocks_info();
1949                 return new moodle_url('/admin/block.php', array('block' => $blocksinfo[$this->name]->id));
1950             }
1952         } else {
1953             return parent::get_settings_url();
1954         }
1955     }
1957     public function get_uninstall_url() {
1959         $blocksinfo = self::get_blocks_info();
1960         return new moodle_url('/admin/blocks.php', array('delete' => $blocksinfo[$this->name]->id, 'sesskey' => sesskey()));
1961     }
1963     /**
1964      * Provides access to the records in {block} table
1965      *
1966      * @param bool $disablecache do not use internal static cache
1967      * @return array array of stdClasses
1968      */
1969     protected static function get_blocks_info($disablecache=false) {
1970         global $DB;
1971         static $blocksinfocache = null;
1973         if (is_null($blocksinfocache) or $disablecache) {
1974             try {
1975                 $blocksinfocache = $DB->get_records('block', null, 'name', 'name,id,version,visible');
1976             } catch (dml_exception $e) {
1977                 // before install
1978                 $blocksinfocache = array();
1979             }
1980         }
1982         return $blocksinfocache;
1983     }
1987 /**
1988  * Class for text filters
1989  */
1990 class plugininfo_filter extends plugininfo_base {
1992     public static function get_plugins($type, $typerootdir, $typeclass) {
1993         global $CFG, $DB;
1995         $filters = array();
1997         // get the list of filters from both /filter and /mod location
1998         $installed = filter_get_all_installed();
2000         foreach ($installed as $filterlegacyname => $displayname) {
2001             $plugin                 = new $typeclass();
2002             $plugin->type           = $type;
2003             $plugin->typerootdir    = $typerootdir;
2004             $plugin->name           = self::normalize_legacy_name($filterlegacyname);
2005             $plugin->rootdir        = $CFG->dirroot . '/' . $filterlegacyname;
2006             $plugin->displayname    = $displayname;
2008             $plugin->load_disk_version();
2009             $plugin->load_db_version();
2010             $plugin->load_required_main_version();
2011             $plugin->init_is_standard();
2013             $filters[$plugin->name] = $plugin;
2014         }
2016         $globalstates = self::get_global_states();
2018         if ($DB->get_manager()->table_exists('filter_active')) {
2019             // if we're upgrading from 1.9, the table does not exist yet
2020             // if it does, make sure that all installed filters are registered
2021             $needsreload  = false;
2022             foreach (array_keys($installed) as $filterlegacyname) {
2023                 if (!isset($globalstates[self::normalize_legacy_name($filterlegacyname)])) {
2024                     filter_set_global_state($filterlegacyname, TEXTFILTER_DISABLED);
2025                     $needsreload = true;
2026                 }
2027             }
2028             if ($needsreload) {
2029                 $globalstates = self::get_global_states(true);
2030             }
2031         }
2033         // make sure that all registered filters are installed, just in case
2034         foreach ($globalstates as $name => $info) {
2035             if (!isset($filters[$name])) {
2036                 // oops, there is a record in filter_active but the filter is not installed
2037                 $plugin                 = new $typeclass();
2038                 $plugin->type           = $type;
2039                 $plugin->typerootdir    = $typerootdir;
2040                 $plugin->name           = $name;
2041                 $plugin->rootdir        = $CFG->dirroot . '/' . $info->legacyname;
2042                 $plugin->displayname    = $info->legacyname;
2044                 $plugin->load_db_version();
2046                 if (is_null($plugin->versiondb)) {
2047                     // this is a hack to stimulate 'Missing from disk' error
2048                     // because $plugin->versiondisk will be null !== false
2049                     $plugin->versiondb = false;
2050                 }
2052                 $filters[$plugin->name] = $plugin;
2053             }
2054         }
2056         return $filters;
2057     }
2059     public function init_display_name() {
2060         // do nothing, the name is set in self::get_plugins()
2061     }
2063     /**
2064      * @see load_version_php()
2065      */
2066     protected function load_version_php() {
2067         if (strpos($this->name, 'mod_') === 0) {
2068             // filters bundled with modules do not have a version.php and so
2069             // do not provide their own versioning information.
2070             return new stdClass();
2071         }
2072         return parent::load_version_php();
2073     }
2075     public function is_enabled() {
2077         $globalstates = self::get_global_states();
2079         foreach ($globalstates as $filterlegacyname => $info) {
2080             $name = self::normalize_legacy_name($filterlegacyname);
2081             if ($name === $this->name) {
2082                 if ($info->active == TEXTFILTER_DISABLED) {
2083                     return false;
2084                 } else {
2085                     // it may be 'On' or 'Off, but available'
2086                     return null;
2087                 }
2088             }
2089         }
2091         return null;
2092     }
2094     public function get_settings_url() {
2096         $globalstates = self::get_global_states();
2097         $legacyname = $globalstates[$this->name]->legacyname;
2098         if (filter_has_global_settings($legacyname)) {
2099             return new moodle_url('/admin/settings.php', array('section' => 'filtersetting' . str_replace('/', '', $legacyname)));
2100         } else {
2101             return null;
2102         }
2103     }
2105     public function get_uninstall_url() {
2107         if (strpos($this->name, 'mod_') === 0) {
2108             return null;
2109         } else {
2110             $globalstates = self::get_global_states();
2111             $legacyname = $globalstates[$this->name]->legacyname;
2112             return new moodle_url('/admin/filters.php', array('sesskey' => sesskey(), 'filterpath' => $legacyname, 'action' => 'delete'));
2113         }
2114     }
2116     /**
2117      * Convert legacy filter names like 'filter/foo' or 'mod/bar' into frankenstyle
2118      *
2119      * @param string $legacyfiltername legacy filter name
2120      * @return string frankenstyle-like name
2121      */
2122     protected static function normalize_legacy_name($legacyfiltername) {
2124         $name = str_replace('/', '_', $legacyfiltername);
2125         if (strpos($name, 'filter_') === 0) {
2126             $name = substr($name, 7);
2127             if (empty($name)) {
2128                 throw new coding_exception('Unable to determine filter name: ' . $legacyfiltername);
2129             }
2130         }
2132         return $name;
2133     }
2135     /**
2136      * Provides access to the results of {@link filter_get_global_states()}
2137      * but indexed by the normalized filter name
2138      *
2139      * The legacy filter name is available as ->legacyname property.
2140      *
2141      * @param bool $disablecache
2142      * @return array
2143      */
2144     protected static function get_global_states($disablecache=false) {
2145         global $DB;
2146         static $globalstatescache = null;
2148         if ($disablecache or is_null($globalstatescache)) {
2150             if (!$DB->get_manager()->table_exists('filter_active')) {
2151                 // we're upgrading from 1.9 and the table used by {@link filter_get_global_states()}
2152                 // does not exist yet
2153                 $globalstatescache = array();
2155             } else {
2156                 foreach (filter_get_global_states() as $legacyname => $info) {
2157                     $name                       = self::normalize_legacy_name($legacyname);
2158                     $filterinfo                 = new stdClass();
2159                     $filterinfo->legacyname     = $legacyname;
2160                     $filterinfo->active         = $info->active;
2161                     $filterinfo->sortorder      = $info->sortorder;
2162                     $globalstatescache[$name]   = $filterinfo;
2163                 }
2164             }
2165         }
2167         return $globalstatescache;
2168     }
2172 /**
2173  * Class for activity modules
2174  */
2175 class plugininfo_mod extends plugininfo_base {
2177     public static function get_plugins($type, $typerootdir, $typeclass) {
2179         // get the information about plugins at the disk
2180         $modules = parent::get_plugins($type, $typerootdir, $typeclass);
2182         // add modules missing from disk
2183         $modulesinfo = self::get_modules_info();
2184         foreach ($modulesinfo as $modulename => $moduleinfo) {
2185             if (isset($modules[$modulename])) {
2186                 continue;
2187             }
2188             $plugin                 = new $typeclass();
2189             $plugin->type           = $type;
2190             $plugin->typerootdir    = $typerootdir;
2191             $plugin->name           = $modulename;
2192             $plugin->rootdir        = null;
2193             $plugin->displayname    = $modulename;
2194             $plugin->versiondb      = $moduleinfo->version;
2195             $plugin->init_is_standard();
2197             $modules[$modulename]   = $plugin;
2198         }
2200         return $modules;
2201     }
2203     /**
2204      * Magic method getter, redirects to read only values.
2205      *
2206      * For module plugins we pretend the object has 'visible' property for compatibility
2207      * with plugins developed for Moodle version below 2.4
2208      *
2209      * @param string $name
2210      * @return mixed
2211      */
2212     public function __get($name) {
2213         if ($name === 'visible') {
2214             debugging('This is now an instance of plugininfo_mod, please use $module->is_enabled() instead of $module->visible', DEBUG_DEVELOPER);
2215             return ($this->is_enabled() !== false);
2216         }
2217         return parent::__get($name);
2218     }
2220     public function init_display_name() {
2221         if (get_string_manager()->string_exists('pluginname', $this->component)) {
2222             $this->displayname = get_string('pluginname', $this->component);
2223         } else {
2224             $this->displayname = get_string('modulename', $this->component);
2225         }
2226     }
2228     /**
2229      * Load the data from version.php.
2230      * @return object the data object defined in version.php.
2231      */
2232     protected function load_version_php() {
2233         $versionfile = $this->full_path('version.php');
2235         $module = new stdClass();
2236         if (is_readable($versionfile)) {
2237             include($versionfile);
2238         }
2239         return $module;
2240     }
2242     public function load_db_version() {
2243         global $DB;
2245         $modulesinfo = self::get_modules_info();
2246         if (isset($modulesinfo[$this->name]->version)) {
2247             $this->versiondb = $modulesinfo[$this->name]->version;
2248         }
2249     }
2251     public function is_enabled() {
2253         $modulesinfo = self::get_modules_info();
2254         if (isset($modulesinfo[$this->name]->visible)) {
2255             if ($modulesinfo[$this->name]->visible) {
2256                 return true;
2257             } else {
2258                 return false;
2259             }
2260         } else {
2261             return parent::is_enabled();
2262         }
2263     }
2265     public function get_settings_section_name() {
2266         return 'modsetting' . $this->name;
2267     }
2269     public function load_settings(part_of_admin_tree $adminroot, $parentnodename, $hassiteconfig) {
2270         global $CFG, $USER, $DB, $OUTPUT, $PAGE; // in case settings.php wants to refer to them
2271         $ADMIN = $adminroot; // may be used in settings.php
2272         $module = $this; // also can be used inside settings.php
2273         $section = $this->get_settings_section_name();
2275         $settings = null;
2276         if ($hassiteconfig && file_exists($this->full_path('settings.php'))) {
2277             $settings = new admin_settingpage($section, $this->displayname,
2278                     'moodle/site:config', $this->is_enabled() === false);
2279             include($this->full_path('settings.php')); // this may also set $settings to null
2280         }
2281         if ($settings) {
2282             $ADMIN->add($parentnodename, $settings);
2283         }
2284     }
2286     public function get_uninstall_url() {
2288         if ($this->name !== 'forum') {
2289             return new moodle_url('/admin/modules.php', array('delete' => $this->name, 'sesskey' => sesskey()));
2290         } else {
2291             return null;
2292         }
2293     }
2295     /**
2296      * Provides access to the records in {modules} table
2297      *
2298      * @param bool $disablecache do not use internal static cache
2299      * @return array array of stdClasses
2300      */
2301     protected static function get_modules_info($disablecache=false) {
2302         global $DB;
2303         static $modulesinfocache = null;
2305         if (is_null($modulesinfocache) or $disablecache) {
2306             try {
2307                 $modulesinfocache = $DB->get_records('modules', null, 'name', 'name,id,version,visible');
2308             } catch (dml_exception $e) {
2309                 // before install
2310                 $modulesinfocache = array();
2311             }
2312         }
2314         return $modulesinfocache;
2315     }
2319 /**
2320  * Class for question behaviours.
2321  */
2322 class plugininfo_qbehaviour extends plugininfo_base {
2324     public function get_uninstall_url() {
2325         return new moodle_url('/admin/qbehaviours.php',
2326                 array('delete' => $this->name, 'sesskey' => sesskey()));
2327     }
2331 /**
2332  * Class for question types
2333  */
2334 class plugininfo_qtype extends plugininfo_base {
2336     public function get_uninstall_url() {
2337         return new moodle_url('/admin/qtypes.php',
2338                 array('delete' => $this->name, 'sesskey' => sesskey()));
2339     }
2343 /**
2344  * Class for authentication plugins
2345  */
2346 class plugininfo_auth extends plugininfo_base {
2348     public function is_enabled() {
2349         global $CFG;
2350         /** @var null|array list of enabled authentication plugins */
2351         static $enabled = null;
2353         if (in_array($this->name, array('nologin', 'manual'))) {
2354             // these two are always enabled and can't be disabled
2355             return null;
2356         }
2358         if (is_null($enabled)) {
2359             $enabled = array_flip(explode(',', $CFG->auth));
2360         }
2362         return isset($enabled[$this->name]);
2363     }
2365     public function get_settings_url() {
2366         if (file_exists($this->full_path('settings.php'))) {
2367             return new moodle_url('/admin/settings.php', array('section' => 'authsetting' . $this->name));
2368         } else {
2369             return new moodle_url('/admin/auth_config.php', array('auth' => $this->name));
2370         }
2371     }
2375 /**
2376  * Class for enrolment plugins
2377  */
2378 class plugininfo_enrol extends plugininfo_base {
2380     public function is_enabled() {
2381         global $CFG;
2382         /** @var null|array list of enabled enrolment plugins */
2383         static $enabled = null;
2385         // We do not actually need whole enrolment classes here so we do not call
2386         // {@link enrol_get_plugins()}. Note that this may produce slightly different
2387         // results, for example if the enrolment plugin does not contain lib.php
2388         // but it is listed in $CFG->enrol_plugins_enabled
2390         if (is_null($enabled)) {
2391             $enabled = array_flip(explode(',', $CFG->enrol_plugins_enabled));
2392         }
2394         return isset($enabled[$this->name]);
2395     }
2397     public function get_settings_url() {
2399         if ($this->is_enabled() or file_exists($this->full_path('settings.php'))) {
2400             return new moodle_url('/admin/settings.php', array('section' => 'enrolsettings' . $this->name));
2401         } else {
2402             return parent::get_settings_url();
2403         }
2404     }
2406     public function get_uninstall_url() {
2407         return new moodle_url('/admin/enrol.php', array('action' => 'uninstall', 'enrol' => $this->name, 'sesskey' => sesskey()));
2408     }
2412 /**
2413  * Class for messaging processors
2414  */
2415 class plugininfo_message extends plugininfo_base {
2417     public function get_settings_url() {
2418         $processors = get_message_processors();
2419         if (isset($processors[$this->name])) {
2420             $processor = $processors[$this->name];
2421             if ($processor->available && $processor->hassettings) {
2422                 return new moodle_url('settings.php', array('section' => 'messagesetting'.$processor->name));
2423             }
2424         }
2425         return parent::get_settings_url();
2426     }
2428     /**
2429      * @see plugintype_interface::is_enabled()
2430      */
2431     public function is_enabled() {
2432         $processors = get_message_processors();
2433         if (isset($processors[$this->name])) {
2434             return $processors[$this->name]->configured && $processors[$this->name]->enabled;
2435         } else {
2436             return parent::is_enabled();
2437         }
2438     }
2440     /**
2441      * @see plugintype_interface::get_uninstall_url()
2442      */
2443     public function get_uninstall_url() {
2444         $processors = get_message_processors();
2445         if (isset($processors[$this->name])) {
2446             return new moodle_url('message.php', array('uninstall' => $processors[$this->name]->id, 'sesskey' => sesskey()));
2447         } else {
2448             return parent::get_uninstall_url();
2449         }
2450     }
2454 /**
2455  * Class for repositories
2456  */
2457 class plugininfo_repository extends plugininfo_base {
2459     public function is_enabled() {
2461         $enabled = self::get_enabled_repositories();
2463         return isset($enabled[$this->name]);
2464     }
2466     public function get_settings_url() {
2468         if ($this->is_enabled()) {
2469             return new moodle_url('/admin/repository.php', array('sesskey' => sesskey(), 'action' => 'edit', 'repos' => $this->name));
2470         } else {
2471             return parent::get_settings_url();
2472         }
2473     }
2475     /**
2476      * Provides access to the records in {repository} table
2477      *
2478      * @param bool $disablecache do not use internal static cache
2479      * @return array array of stdClasses
2480      */
2481     protected static function get_enabled_repositories($disablecache=false) {
2482         global $DB;
2483         static $repositories = null;
2485         if (is_null($repositories) or $disablecache) {
2486             $repositories = $DB->get_records('repository', null, 'type', 'type,visible,sortorder');
2487         }
2489         return $repositories;
2490     }
2494 /**
2495  * Class for portfolios
2496  */
2497 class plugininfo_portfolio extends plugininfo_base {
2499     public function is_enabled() {
2501         $enabled = self::get_enabled_portfolios();
2503         return isset($enabled[$this->name]);
2504     }
2506     /**
2507      * Provides access to the records in {portfolio_instance} table
2508      *
2509      * @param bool $disablecache do not use internal static cache
2510      * @return array array of stdClasses
2511      */
2512     protected static function get_enabled_portfolios($disablecache=false) {
2513         global $DB;
2514         static $portfolios = null;
2516         if (is_null($portfolios) or $disablecache) {
2517             $portfolios = array();
2518             $instances  = $DB->get_recordset('portfolio_instance', null, 'plugin');
2519             foreach ($instances as $instance) {
2520                 if (isset($portfolios[$instance->plugin])) {
2521                     if ($instance->visible) {
2522                         $portfolios[$instance->plugin]->visible = $instance->visible;
2523                     }
2524                 } else {
2525                     $portfolios[$instance->plugin] = $instance;
2526                 }
2527             }
2528         }
2530         return $portfolios;
2531     }
2535 /**
2536  * Class for themes
2537  */
2538 class plugininfo_theme extends plugininfo_base {
2540     public function is_enabled() {
2541         global $CFG;
2543         if ((!empty($CFG->theme) and $CFG->theme === $this->name) or
2544             (!empty($CFG->themelegacy) and $CFG->themelegacy === $this->name)) {
2545             return true;
2546         } else {
2547             return parent::is_enabled();
2548         }
2549     }
2553 /**
2554  * Class representing an MNet service
2555  */
2556 class plugininfo_mnetservice extends plugininfo_base {
2558     public function is_enabled() {
2559         global $CFG;
2561         if (empty($CFG->mnet_dispatcher_mode) || $CFG->mnet_dispatcher_mode !== 'strict') {
2562             return false;
2563         } else {
2564             return parent::is_enabled();
2565         }
2566     }
2570 /**
2571  * Class for admin tool plugins
2572  */
2573 class plugininfo_tool extends plugininfo_base {
2575     public function get_uninstall_url() {
2576         return new moodle_url('/admin/tools.php', array('delete' => $this->name, 'sesskey' => sesskey()));
2577     }
2581 /**
2582  * Class for admin tool plugins
2583  */
2584 class plugininfo_report extends plugininfo_base {
2586     public function get_uninstall_url() {
2587         return new moodle_url('/admin/reports.php', array('delete' => $this->name, 'sesskey' => sesskey()));
2588     }
2592 /**
2593  * Class for local plugins
2594  */
2595 class plugininfo_local extends plugininfo_base {
2597     public function get_uninstall_url() {
2598         return new moodle_url('/admin/localplugins.php', array('delete' => $this->name, 'sesskey' => sesskey()));
2599     }
2601     public function get_settings_url() {
2602         if (file_exists($this->full_path('settings.php'))) {
2603             return new moodle_url('/admin/settings.php', array('section' => 'local_' . $this->name));
2604         } else {
2605             return parent::get_settings_url();
2606         }
2607     }