MDL-36266 Improve update notification message subject
[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 require_once($CFG->libdir.'/filelib.php');  // curl class needed here
35 /**
36  * Singleton class providing general plugins management functionality
37  */
38 class plugin_manager {
40     /** the plugin is shipped with standard Moodle distribution */
41     const PLUGIN_SOURCE_STANDARD    = 'std';
42     /** the plugin is added extension */
43     const PLUGIN_SOURCE_EXTENSION   = 'ext';
45     /** the plugin uses neither database nor capabilities, no versions */
46     const PLUGIN_STATUS_NODB        = 'nodb';
47     /** the plugin is up-to-date */
48     const PLUGIN_STATUS_UPTODATE    = 'uptodate';
49     /** the plugin is about to be installed */
50     const PLUGIN_STATUS_NEW         = 'new';
51     /** the plugin is about to be upgraded */
52     const PLUGIN_STATUS_UPGRADE     = 'upgrade';
53     /** the standard plugin is about to be deleted */
54     const PLUGIN_STATUS_DELETE     = 'delete';
55     /** the version at the disk is lower than the one already installed */
56     const PLUGIN_STATUS_DOWNGRADE   = 'downgrade';
57     /** the plugin is installed but missing from disk */
58     const PLUGIN_STATUS_MISSING     = 'missing';
60     /** @var plugin_manager holds the singleton instance */
61     protected static $singletoninstance;
62     /** @var array of raw plugins information */
63     protected $pluginsinfo = null;
64     /** @var array of raw subplugins information */
65     protected $subpluginsinfo = null;
67     /**
68      * Direct initiation not allowed, use the factory method {@link self::instance()}
69      */
70     protected function __construct() {
71     }
73     /**
74      * Sorry, this is singleton
75      */
76     protected function __clone() {
77     }
79     /**
80      * Factory method for this class
81      *
82      * @return plugin_manager the singleton instance
83      */
84     public static function instance() {
85         if (is_null(self::$singletoninstance)) {
86             self::$singletoninstance = new self();
87         }
88         return self::$singletoninstance;
89     }
91     /**
92      * Returns a tree of known plugins and information about them
93      *
94      * @param bool $disablecache force reload, cache can be used otherwise
95      * @return array 2D array. The first keys are plugin type names (e.g. qtype);
96      *      the second keys are the plugin local name (e.g. multichoice); and
97      *      the values are the corresponding objects extending {@link plugininfo_base}
98      */
99     public function get_plugins($disablecache=false) {
100         global $CFG;
102         if ($disablecache or is_null($this->pluginsinfo)) {
103             $this->pluginsinfo = array();
104             $plugintypes = get_plugin_types();
105             $plugintypes = $this->reorder_plugin_types($plugintypes);
106             foreach ($plugintypes as $plugintype => $plugintyperootdir) {
107                 if (in_array($plugintype, array('base', 'general'))) {
108                     throw new coding_exception('Illegal usage of reserved word for plugin type');
109                 }
110                 if (class_exists('plugininfo_' . $plugintype)) {
111                     $plugintypeclass = 'plugininfo_' . $plugintype;
112                 } else {
113                     $plugintypeclass = 'plugininfo_general';
114                 }
115                 if (!in_array('plugininfo_base', class_parents($plugintypeclass))) {
116                     throw new coding_exception('Class ' . $plugintypeclass . ' must extend plugininfo_base');
117                 }
118                 $plugins = call_user_func(array($plugintypeclass, 'get_plugins'), $plugintype, $plugintyperootdir, $plugintypeclass);
119                 $this->pluginsinfo[$plugintype] = $plugins;
120             }
122             if (empty($CFG->disableupdatenotifications) and !during_initial_install()) {
123                 // append the information about available updates provided by {@link available_update_checker()}
124                 $provider = available_update_checker::instance();
125                 foreach ($this->pluginsinfo as $plugintype => $plugins) {
126                     foreach ($plugins as $plugininfoholder) {
127                         $plugininfoholder->check_available_updates($provider);
128                     }
129                 }
130             }
131         }
133         return $this->pluginsinfo;
134     }
136     /**
137      * Returns list of plugins that define their subplugins and the information
138      * about them from the db/subplugins.php file.
139      *
140      * At the moment, only activity modules can define subplugins.
141      *
142      * @param bool $disablecache force reload, cache can be used otherwise
143      * @return array with keys like 'mod_quiz', and values the data from the
144      *      corresponding db/subplugins.php file.
145      */
146     public function get_subplugins($disablecache=false) {
148         if ($disablecache or is_null($this->subpluginsinfo)) {
149             $this->subpluginsinfo = array();
150             $mods = get_plugin_list('mod');
151             foreach ($mods as $mod => $moddir) {
152                 $modsubplugins = array();
153                 if (file_exists($moddir . '/db/subplugins.php')) {
154                     include($moddir . '/db/subplugins.php');
155                     foreach ($subplugins as $subplugintype => $subplugintyperootdir) {
156                         $subplugin = new stdClass();
157                         $subplugin->type = $subplugintype;
158                         $subplugin->typerootdir = $subplugintyperootdir;
159                         $modsubplugins[$subplugintype] = $subplugin;
160                     }
161                 $this->subpluginsinfo['mod_' . $mod] = $modsubplugins;
162                 }
163             }
164         }
166         return $this->subpluginsinfo;
167     }
169     /**
170      * Returns the name of the plugin that defines the given subplugin type
171      *
172      * If the given subplugin type is not actually a subplugin, returns false.
173      *
174      * @param string $subplugintype the name of subplugin type, eg. workshopform or quiz
175      * @return false|string the name of the parent plugin, eg. mod_workshop
176      */
177     public function get_parent_of_subplugin($subplugintype) {
179         $parent = false;
180         foreach ($this->get_subplugins() as $pluginname => $subplugintypes) {
181             if (isset($subplugintypes[$subplugintype])) {
182                 $parent = $pluginname;
183                 break;
184             }
185         }
187         return $parent;
188     }
190     /**
191      * Returns a localized name of a given plugin
192      *
193      * @param string $plugin name of the plugin, eg mod_workshop or auth_ldap
194      * @return string
195      */
196     public function plugin_name($plugin) {
197         list($type, $name) = normalize_component($plugin);
198         return $this->pluginsinfo[$type][$name]->displayname;
199     }
201     /**
202      * Returns a localized name of a plugin type in plural form
203      *
204      * Most plugin types define their names in core_plugin lang file. In case of subplugins,
205      * we try to ask the parent plugin for the name. In the worst case, we will return
206      * the value of the passed $type parameter.
207      *
208      * @param string $type the type of the plugin, e.g. mod or workshopform
209      * @return string
210      */
211     public function plugintype_name_plural($type) {
213         if (get_string_manager()->string_exists('type_' . $type . '_plural', 'core_plugin')) {
214             // for most plugin types, their names are defined in core_plugin lang file
215             return get_string('type_' . $type . '_plural', 'core_plugin');
217         } else if ($parent = $this->get_parent_of_subplugin($type)) {
218             // if this is a subplugin, try to ask the parent plugin for the name
219             if (get_string_manager()->string_exists('subplugintype_' . $type . '_plural', $parent)) {
220                 return $this->plugin_name($parent) . ' / ' . get_string('subplugintype_' . $type . '_plural', $parent);
221             } else {
222                 return $this->plugin_name($parent) . ' / ' . $type;
223             }
225         } else {
226             return $type;
227         }
228     }
230     /**
231      * @param string $component frankenstyle component name.
232      * @return plugininfo_base|null the corresponding plugin information.
233      */
234     public function get_plugin_info($component) {
235         list($type, $name) = normalize_component($component);
236         $plugins = $this->get_plugins();
237         if (isset($plugins[$type][$name])) {
238             return $plugins[$type][$name];
239         } else {
240             return null;
241         }
242     }
244     /**
245      * Get a list of any other plugins that require this one.
246      * @param string $component frankenstyle component name.
247      * @return array of frankensyle component names that require this one.
248      */
249     public function other_plugins_that_require($component) {
250         $others = array();
251         foreach ($this->get_plugins() as $type => $plugins) {
252             foreach ($plugins as $plugin) {
253                 $required = $plugin->get_other_required_plugins();
254                 if (isset($required[$component])) {
255                     $others[] = $plugin->component;
256                 }
257             }
258         }
259         return $others;
260     }
262     /**
263      * Check a dependencies list against the list of installed plugins.
264      * @param array $dependencies compenent name to required version or ANY_VERSION.
265      * @return bool true if all the dependencies are satisfied.
266      */
267     public function are_dependencies_satisfied($dependencies) {
268         foreach ($dependencies as $component => $requiredversion) {
269             $otherplugin = $this->get_plugin_info($component);
270             if (is_null($otherplugin)) {
271                 return false;
272             }
274             if ($requiredversion != ANY_VERSION and $otherplugin->versiondisk < $requiredversion) {
275                 return false;
276             }
277         }
279         return true;
280     }
282     /**
283      * Checks all dependencies for all installed plugins
284      *
285      * This is used by install and upgrade. The array passed by reference as the second
286      * argument is populated with the list of plugins that have failed dependencies (note that
287      * a single plugin can appear multiple times in the $failedplugins).
288      *
289      * @param int $moodleversion the version from version.php.
290      * @param array $failedplugins to return the list of plugins with non-satisfied dependencies
291      * @return bool true if all the dependencies are satisfied for all plugins.
292      */
293     public function all_plugins_ok($moodleversion, &$failedplugins = array()) {
295         $return = true;
296         foreach ($this->get_plugins() as $type => $plugins) {
297             foreach ($plugins as $plugin) {
299                 if (!$plugin->is_core_dependency_satisfied($moodleversion)) {
300                     $return = false;
301                     $failedplugins[] = $plugin->component;
302                 }
304                 if (!$this->are_dependencies_satisfied($plugin->get_other_required_plugins())) {
305                     $return = false;
306                     $failedplugins[] = $plugin->component;
307                 }
308             }
309         }
311         return $return;
312     }
314     /**
315      * Checks if there are some plugins with a known available update
316      *
317      * @return bool true if there is at least one available update
318      */
319     public function some_plugins_updatable() {
320         foreach ($this->get_plugins() as $type => $plugins) {
321             foreach ($plugins as $plugin) {
322                 if ($plugin->available_updates()) {
323                     return true;
324                 }
325             }
326         }
328         return false;
329     }
331     /**
332      * Defines a list of all plugins that were originally shipped in the standard Moodle distribution,
333      * but are not anymore and are deleted during upgrades.
334      *
335      * The main purpose of this list is to hide missing plugins during upgrade.
336      *
337      * @param string $type plugin type
338      * @param string $name plugin name
339      * @return bool
340      */
341     public static function is_deleted_standard_plugin($type, $name) {
342         static $plugins = array(
343             // do not add 1.9-2.2 plugin removals here
344         );
346         if (!isset($plugins[$type])) {
347             return false;
348         }
349         return in_array($name, $plugins[$type]);
350     }
352     /**
353      * Defines a white list of all plugins shipped in the standard Moodle distribution
354      *
355      * @param string $type
356      * @return false|array array of standard plugins or false if the type is unknown
357      */
358     public static function standard_plugins_list($type) {
359         static $standard_plugins = array(
361             'assignment' => array(
362                 'offline', 'online', 'upload', 'uploadsingle'
363             ),
365             'assignsubmission' => array(
366                 'comments', 'file', 'onlinetext'
367             ),
369             'assignfeedback' => array(
370                 'comments', 'file'
371             ),
373             'auth' => array(
374                 'cas', 'db', 'email', 'fc', 'imap', 'ldap', 'manual', 'mnet',
375                 'nntp', 'nologin', 'none', 'pam', 'pop3', 'radius',
376                 'shibboleth', 'webservice'
377             ),
379             'block' => array(
380                 'activity_modules', 'admin_bookmarks', 'blog_menu',
381                 'blog_recent', 'blog_tags', 'calendar_month',
382                 'calendar_upcoming', 'comments', 'community',
383                 'completionstatus', 'course_list', 'course_overview',
384                 'course_summary', 'feedback', 'glossary_random', 'html',
385                 'login', 'mentees', 'messages', 'mnet_hosts', 'myprofile',
386                 'navigation', 'news_items', 'online_users', 'participants',
387                 'private_files', 'quiz_results', 'recent_activity',
388                 'rss_client', 'search_forums', 'section_links',
389                 'selfcompletion', 'settings', 'site_main_menu',
390                 'social_activities', 'tag_flickr', 'tag_youtube', 'tags'
391             ),
393             'booktool' => array(
394                 'exportimscp', 'importhtml', 'print'
395             ),
397             'coursereport' => array(
398                 //deprecated!
399             ),
401             'datafield' => array(
402                 'checkbox', 'date', 'file', 'latlong', 'menu', 'multimenu',
403                 'number', 'picture', 'radiobutton', 'text', 'textarea', 'url'
404             ),
406             'datapreset' => array(
407                 'imagegallery'
408             ),
410             'editor' => array(
411                 'textarea', 'tinymce'
412             ),
414             'enrol' => array(
415                 'authorize', 'category', 'cohort', 'database', 'flatfile',
416                 'guest', 'imsenterprise', 'ldap', 'manual', 'meta', 'mnet',
417                 'paypal', 'self'
418             ),
420             'filter' => array(
421                 'activitynames', 'algebra', 'censor', 'emailprotect',
422                 'emoticon', 'mediaplugin', 'multilang', 'tex', 'tidy',
423                 'urltolink', 'data', 'glossary'
424             ),
426             'format' => array(
427                 'scorm', 'social', 'topics', 'weeks'
428             ),
430             'gradeexport' => array(
431                 'ods', 'txt', 'xls', 'xml'
432             ),
434             'gradeimport' => array(
435                 'csv', 'xml'
436             ),
438             'gradereport' => array(
439                 'grader', 'outcomes', 'overview', 'user'
440             ),
442             'gradingform' => array(
443                 'rubric', 'guide'
444             ),
446             'local' => array(
447             ),
449             'message' => array(
450                 'email', 'jabber', 'popup'
451             ),
453             'mnetservice' => array(
454                 'enrol'
455             ),
457             'mod' => array(
458                 'assign', 'assignment', 'book', 'chat', 'choice', 'data', 'feedback', 'folder',
459                 'forum', 'glossary', 'imscp', 'label', 'lesson', 'lti', 'page',
460                 'quiz', 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop'
461             ),
463             'plagiarism' => array(
464             ),
466             'portfolio' => array(
467                 'boxnet', 'download', 'flickr', 'googledocs', 'mahara', 'picasa'
468             ),
470             'profilefield' => array(
471                 'checkbox', 'datetime', 'menu', 'text', 'textarea'
472             ),
474             'qbehaviour' => array(
475                 'adaptive', 'adaptivenopenalty', 'deferredcbm',
476                 'deferredfeedback', 'immediatecbm', 'immediatefeedback',
477                 'informationitem', 'interactive', 'interactivecountback',
478                 'manualgraded', 'missing'
479             ),
481             'qformat' => array(
482                 'aiken', 'blackboard', 'blackboard_six', 'examview', 'gift',
483                 'learnwise', 'missingword', 'multianswer', 'webct',
484                 'xhtml', 'xml'
485             ),
487             'qtype' => array(
488                 'calculated', 'calculatedmulti', 'calculatedsimple',
489                 'description', 'essay', 'match', 'missingtype', 'multianswer',
490                 'multichoice', 'numerical', 'random', 'randomsamatch',
491                 'shortanswer', 'truefalse'
492             ),
494             'quiz' => array(
495                 'grading', 'overview', 'responses', 'statistics'
496             ),
498             'quizaccess' => array(
499                 'delaybetweenattempts', 'ipaddress', 'numattempts', 'openclosedate',
500                 'password', 'safebrowser', 'securewindow', 'timelimit'
501             ),
503             'report' => array(
504                 'backups', 'completion', 'configlog', 'courseoverview',
505                 'log', 'loglive', 'outline', 'participation', 'progress', 'questioninstances', 'security', 'stats'
506             ),
508             'repository' => array(
509                 'alfresco', 'boxnet', 'coursefiles', 'dropbox', 'equella', 'filesystem',
510                 'flickr', 'flickr_public', 'googledocs', 'local', 'merlot',
511                 'picasa', 'recent', 's3', 'upload', 'url', 'user', 'webdav',
512                 'wikimedia', 'youtube'
513             ),
515             'scormreport' => array(
516                 'basic',
517                 'interactions',
518                 'graphs'
519             ),
521             'theme' => array(
522                 'afterburner', 'anomaly', 'arialist', 'base', 'binarius',
523                 'boxxie', 'brick', 'canvas', 'formal_white', 'formfactor',
524                 'fusion', 'leatherbound', 'magazine', 'mymobile', 'nimble',
525                 'nonzero', 'overlay', 'serenity', 'sky_high', 'splash',
526                 'standard', 'standardold'
527             ),
529             'tool' => array(
530                 'assignmentupgrade', 'bloglevelupgrade', 'capability', 'customlang', 'dbtransfer', 'generator',
531                 'health', 'innodb', 'langimport', 'multilangupgrade', 'phpunit', 'profiling',
532                 'qeupgradehelper', 'replace', 'spamcleaner', 'timezoneimport', 'unittest',
533                 'uploaduser', 'unsuproles', 'xmldb'
534             ),
536             'webservice' => array(
537                 'amf', 'rest', 'soap', 'xmlrpc'
538             ),
540             'workshopallocation' => array(
541                 'manual', 'random', 'scheduled'
542             ),
544             'workshopeval' => array(
545                 'best'
546             ),
548             'workshopform' => array(
549                 'accumulative', 'comments', 'numerrors', 'rubric'
550             )
551         );
553         if (isset($standard_plugins[$type])) {
554             return $standard_plugins[$type];
555         } else {
556             return false;
557         }
558     }
560     /**
561      * Reorders plugin types into a sequence to be displayed
562      *
563      * For technical reasons, plugin types returned by {@link get_plugin_types()} are
564      * in a certain order that does not need to fit the expected order for the display.
565      * Particularly, activity modules should be displayed first as they represent the
566      * real heart of Moodle. They should be followed by other plugin types that are
567      * used to build the courses (as that is what one expects from LMS). After that,
568      * other supportive plugin types follow.
569      *
570      * @param array $types associative array
571      * @return array same array with altered order of items
572      */
573     protected function reorder_plugin_types(array $types) {
574         $fix = array(
575             'mod'        => $types['mod'],
576             'block'      => $types['block'],
577             'qtype'      => $types['qtype'],
578             'qbehaviour' => $types['qbehaviour'],
579             'qformat'    => $types['qformat'],
580             'filter'     => $types['filter'],
581             'enrol'      => $types['enrol'],
582         );
583         foreach ($types as $type => $path) {
584             if (!isset($fix[$type])) {
585                 $fix[$type] = $path;
586             }
587         }
588         return $fix;
589     }
593 /**
594  * General exception thrown by the {@link available_update_checker} class
595  */
596 class available_update_checker_exception extends moodle_exception {
598     /**
599      * @param string $errorcode exception description identifier
600      * @param mixed $debuginfo debugging data to display
601      */
602     public function __construct($errorcode, $debuginfo=null) {
603         parent::__construct($errorcode, 'core_plugin', '', null, print_r($debuginfo, true));
604     }
608 /**
609  * Singleton class that handles checking for available updates
610  */
611 class available_update_checker {
613     /** @var available_update_checker holds the singleton instance */
614     protected static $singletoninstance;
615     /** @var null|int the timestamp of when the most recent response was fetched */
616     protected $recentfetch = null;
617     /** @var null|array the recent response from the update notification provider */
618     protected $recentresponse = null;
619     /** @var null|string the numerical version of the local Moodle code */
620     protected $currentversion = null;
621     /** @var null|string the release info of the local Moodle code */
622     protected $currentrelease = null;
623     /** @var null|string branch of the local Moodle code */
624     protected $currentbranch = null;
625     /** @var array of (string)frankestyle => (string)version list of additional plugins deployed at this site */
626     protected $currentplugins = array();
628     /**
629      * Direct initiation not allowed, use the factory method {@link self::instance()}
630      */
631     protected function __construct() {
632     }
634     /**
635      * Sorry, this is singleton
636      */
637     protected function __clone() {
638     }
640     /**
641      * Factory method for this class
642      *
643      * @return available_update_checker the singleton instance
644      */
645     public static function instance() {
646         if (is_null(self::$singletoninstance)) {
647             self::$singletoninstance = new self();
648         }
649         return self::$singletoninstance;
650     }
652     /**
653      * Returns the timestamp of the last execution of {@link fetch()}
654      *
655      * @return int|null null if it has never been executed or we don't known
656      */
657     public function get_last_timefetched() {
659         $this->restore_response();
661         if (!empty($this->recentfetch)) {
662             return $this->recentfetch;
664         } else {
665             return null;
666         }
667     }
669     /**
670      * Fetches the available update status from the remote site
671      *
672      * @throws available_update_checker_exception
673      */
674     public function fetch() {
675         $response = $this->get_response();
676         $this->validate_response($response);
677         $this->store_response($response);
678     }
680     /**
681      * Returns the available update information for the given component
682      *
683      * This method returns null if the most recent response does not contain any information
684      * about it. The returned structure is an array of available updates for the given
685      * component. Each update info is an object with at least one property called
686      * 'version'. Other possible properties are 'release', 'maturity', 'url' and 'downloadurl'.
687      *
688      * For the 'core' component, the method returns real updates only (those with higher version).
689      * For all other components, the list of all known remote updates is returned and the caller
690      * (usually the {@link plugin_manager}) is supposed to make the actual comparison of versions.
691      *
692      * @param string $component frankenstyle
693      * @param array $options with supported keys 'minmaturity' and/or 'notifybuilds'
694      * @return null|array null or array of available_update_info objects
695      */
696     public function get_update_info($component, array $options = array()) {
698         if (!isset($options['minmaturity'])) {
699             $options['minmaturity'] = 0;
700         }
702         if (!isset($options['notifybuilds'])) {
703             $options['notifybuilds'] = false;
704         }
706         if ($component == 'core') {
707             $this->load_current_environment();
708         }
710         $this->restore_response();
712         if (empty($this->recentresponse['updates'][$component])) {
713             return null;
714         }
716         $updates = array();
717         foreach ($this->recentresponse['updates'][$component] as $info) {
718             $update = new available_update_info($component, $info);
719             if (isset($update->maturity) and ($update->maturity < $options['minmaturity'])) {
720                 continue;
721             }
722             if ($component == 'core') {
723                 if ($update->version <= $this->currentversion) {
724                     continue;
725                 }
726                 if (empty($options['notifybuilds']) and $this->is_same_release($update->release)) {
727                     continue;
728                 }
729             }
730             $updates[] = $update;
731         }
733         if (empty($updates)) {
734             return null;
735         }
737         return $updates;
738     }
740     /**
741      * The method being run via cron.php
742      */
743     public function cron() {
744         global $CFG;
746         if (!$this->cron_autocheck_enabled()) {
747             $this->cron_mtrace('Automatic check for available updates not enabled, skipping.');
748             return;
749         }
751         $now = $this->cron_current_timestamp();
753         if ($this->cron_has_fresh_fetch($now)) {
754             $this->cron_mtrace('Recently fetched info about available updates is still fresh enough, skipping.');
755             return;
756         }
758         if ($this->cron_has_outdated_fetch($now)) {
759             $this->cron_mtrace('Outdated or missing info about available updates, forced fetching ... ', '');
760             $this->cron_execute();
761             return;
762         }
764         $offset = $this->cron_execution_offset();
765         $start = mktime(1, 0, 0, date('n', $now), date('j', $now), date('Y', $now)); // 01:00 AM today local time
766         if ($now > $start + $offset) {
767             $this->cron_mtrace('Regular daily check for available updates ... ', '');
768             $this->cron_execute();
769             return;
770         }
771     }
773     /// end of public API //////////////////////////////////////////////////////
775     /**
776      * Makes cURL request to get data from the remote site
777      *
778      * @return string raw request result
779      * @throws available_update_checker_exception
780      */
781     protected function get_response() {
782         $curl = new curl(array('proxy' => true));
783         $response = $curl->post($this->prepare_request_url(), $this->prepare_request_params());
784         $curlinfo = $curl->get_info();
785         if ($curlinfo['http_code'] != 200) {
786             throw new available_update_checker_exception('err_response_http_code', $curlinfo['http_code']);
787         }
788         return $response;
789     }
791     /**
792      * Makes sure the response is valid, has correct API format etc.
793      *
794      * @param string $response raw response as returned by the {@link self::get_response()}
795      * @throws available_update_checker_exception
796      */
797     protected function validate_response($response) {
799         $response = $this->decode_response($response);
801         if (empty($response)) {
802             throw new available_update_checker_exception('err_response_empty');
803         }
805         if (empty($response['status']) or $response['status'] !== 'OK') {
806             throw new available_update_checker_exception('err_response_status', $response['status']);
807         }
809         if (empty($response['apiver']) or $response['apiver'] !== '1.0') {
810             throw new available_update_checker_exception('err_response_format_version', $response['apiver']);
811         }
813         if (empty($response['forbranch']) or $response['forbranch'] !== moodle_major_version(true)) {
814             throw new available_update_checker_exception('err_response_target_version', $response['forbranch']);
815         }
816     }
818     /**
819      * Decodes the raw string response from the update notifications provider
820      *
821      * @param string $response as returned by {@link self::get_response()}
822      * @return array decoded response structure
823      */
824     protected function decode_response($response) {
825         return json_decode($response, true);
826     }
828     /**
829      * Stores the valid fetched response for later usage
830      *
831      * This implementation uses the config_plugins table as the permanent storage.
832      *
833      * @param string $response raw valid data returned by {@link self::get_response()}
834      */
835     protected function store_response($response) {
837         set_config('recentfetch', time(), 'core_plugin');
838         set_config('recentresponse', $response, 'core_plugin');
840         $this->restore_response(true);
841     }
843     /**
844      * Loads the most recent raw response record we have fetched
845      *
846      * After this method is called, $this->recentresponse is set to an array. If the
847      * array is empty, then either no data have been fetched yet or the fetched data
848      * do not have expected format (and thence they are ignored and a debugging
849      * message is displayed).
850      *
851      * This implementation uses the config_plugins table as the permanent storage.
852      *
853      * @param bool $forcereload reload even if it was already loaded
854      */
855     protected function restore_response($forcereload = false) {
857         if (!$forcereload and !is_null($this->recentresponse)) {
858             // we already have it, nothing to do
859             return;
860         }
862         $config = get_config('core_plugin');
864         if (!empty($config->recentresponse) and !empty($config->recentfetch)) {
865             try {
866                 $this->validate_response($config->recentresponse);
867                 $this->recentfetch = $config->recentfetch;
868                 $this->recentresponse = $this->decode_response($config->recentresponse);
869             } catch (available_update_checker_exception $e) {
870                 debugging('Invalid info about available updates detected and will be ignored: '.$e->getMessage(), DEBUG_ALL);
871                 $this->recentresponse = array();
872             }
874         } else {
875             $this->recentresponse = array();
876         }
877     }
879     /**
880      * Compares two raw {@link $recentresponse} records and returns the list of changed updates
881      *
882      * This method is used to populate potential update info to be sent to site admins.
883      *
884      * @param array $old
885      * @param array $new
886      * @throws available_update_checker_exception
887      * @return array parts of $new['updates'] that have changed
888      */
889     protected function compare_responses(array $old, array $new) {
891         if (empty($new)) {
892             return array();
893         }
895         if (!array_key_exists('updates', $new)) {
896             throw new available_update_checker_exception('err_response_format');
897         }
899         if (empty($old)) {
900             return $new['updates'];
901         }
903         if (!array_key_exists('updates', $old)) {
904             throw new available_update_checker_exception('err_response_format');
905         }
907         $changes = array();
909         foreach ($new['updates'] as $newcomponent => $newcomponentupdates) {
910             if (empty($old['updates'][$newcomponent])) {
911                 $changes[$newcomponent] = $newcomponentupdates;
912                 continue;
913             }
914             foreach ($newcomponentupdates as $newcomponentupdate) {
915                 $inold = false;
916                 foreach ($old['updates'][$newcomponent] as $oldcomponentupdate) {
917                     if ($newcomponentupdate['version'] == $oldcomponentupdate['version']) {
918                         $inold = true;
919                     }
920                 }
921                 if (!$inold) {
922                     if (!isset($changes[$newcomponent])) {
923                         $changes[$newcomponent] = array();
924                     }
925                     $changes[$newcomponent][] = $newcomponentupdate;
926                 }
927             }
928         }
930         return $changes;
931     }
933     /**
934      * Returns the URL to send update requests to
935      *
936      * During the development or testing, you can set $CFG->alternativeupdateproviderurl
937      * to a custom URL that will be used. Otherwise the standard URL will be returned.
938      *
939      * @return string URL
940      */
941     protected function prepare_request_url() {
942         global $CFG;
944         if (!empty($CFG->alternativeupdateproviderurl)) {
945             return $CFG->alternativeupdateproviderurl;
946         } else {
947             return 'http://download.moodle.org/api/1.0/updates.php';
948         }
949     }
951     /**
952      * Sets the properties currentversion, currentrelease, currentbranch and currentplugins
953      *
954      * @param bool $forcereload
955      */
956     protected function load_current_environment($forcereload=false) {
957         global $CFG;
959         if (!is_null($this->currentversion) and !$forcereload) {
960             // nothing to do
961             return;
962         }
964         require($CFG->dirroot.'/version.php');
965         $this->currentversion = $version;
966         $this->currentrelease = $release;
967         $this->currentbranch = moodle_major_version(true);
969         $pluginman = plugin_manager::instance();
970         foreach ($pluginman->get_plugins() as $type => $plugins) {
971             foreach ($plugins as $plugin) {
972                 if (!$plugin->is_standard()) {
973                     $this->currentplugins[$plugin->component] = $plugin->versiondisk;
974                 }
975             }
976         }
977     }
979     /**
980      * Returns the list of HTTP params to be sent to the updates provider URL
981      *
982      * @return array of (string)param => (string)value
983      */
984     protected function prepare_request_params() {
985         global $CFG;
987         $this->load_current_environment();
988         $this->restore_response();
990         $params = array();
991         $params['format'] = 'json';
993         if (isset($this->recentresponse['ticket'])) {
994             $params['ticket'] = $this->recentresponse['ticket'];
995         }
997         if (isset($this->currentversion)) {
998             $params['version'] = $this->currentversion;
999         } else {
1000             throw new coding_exception('Main Moodle version must be already known here');
1001         }
1003         if (isset($this->currentbranch)) {
1004             $params['branch'] = $this->currentbranch;
1005         } else {
1006             throw new coding_exception('Moodle release must be already known here');
1007         }
1009         $plugins = array();
1010         foreach ($this->currentplugins as $plugin => $version) {
1011             $plugins[] = $plugin.'@'.$version;
1012         }
1013         if (!empty($plugins)) {
1014             $params['plugins'] = implode(',', $plugins);
1015         }
1017         return $params;
1018     }
1020     /**
1021      * Returns the current timestamp
1022      *
1023      * @return int the timestamp
1024      */
1025     protected function cron_current_timestamp() {
1026         return time();
1027     }
1029     /**
1030      * Output cron debugging info
1031      *
1032      * @see mtrace()
1033      * @param string $msg output message
1034      * @param string $eol end of line
1035      */
1036     protected function cron_mtrace($msg, $eol = PHP_EOL) {
1037         mtrace($msg, $eol);
1038     }
1040     /**
1041      * Decide if the autocheck feature is disabled in the server setting
1042      *
1043      * @return bool true if autocheck enabled, false if disabled
1044      */
1045     protected function cron_autocheck_enabled() {
1046         global $CFG;
1048         if (empty($CFG->updateautocheck)) {
1049             return false;
1050         } else {
1051             return true;
1052         }
1053     }
1055     /**
1056      * Decide if the recently fetched data are still fresh enough
1057      *
1058      * @param int $now current timestamp
1059      * @return bool true if no need to re-fetch, false otherwise
1060      */
1061     protected function cron_has_fresh_fetch($now) {
1062         $recent = $this->get_last_timefetched();
1064         if (empty($recent)) {
1065             return false;
1066         }
1068         if ($now < $recent) {
1069             $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1070             return true;
1071         }
1073         if ($now - $recent > 24 * HOURSECS) {
1074             return false;
1075         }
1077         return true;
1078     }
1080     /**
1081      * Decide if the fetch is outadated or even missing
1082      *
1083      * @param int $now current timestamp
1084      * @return bool false if no need to re-fetch, true otherwise
1085      */
1086     protected function cron_has_outdated_fetch($now) {
1087         $recent = $this->get_last_timefetched();
1089         if (empty($recent)) {
1090             return true;
1091         }
1093         if ($now < $recent) {
1094             $this->cron_mtrace('The most recent fetch is reported to be in the future, this is weird!');
1095             return false;
1096         }
1098         if ($now - $recent > 48 * HOURSECS) {
1099             return true;
1100         }
1102         return false;
1103     }
1105     /**
1106      * Returns the cron execution offset for this site
1107      *
1108      * The main {@link self::cron()} is supposed to run every night in some random time
1109      * between 01:00 and 06:00 AM (local time). The exact moment is defined by so called
1110      * execution offset, that is the amount of time after 01:00 AM. The offset value is
1111      * initially generated randomly and then used consistently at the site. This way, the
1112      * regular checks against the download.moodle.org server are spread in time.
1113      *
1114      * @return int the offset number of seconds from range 1 sec to 5 hours
1115      */
1116     protected function cron_execution_offset() {
1117         global $CFG;
1119         if (empty($CFG->updatecronoffset)) {
1120             set_config('updatecronoffset', rand(1, 5 * HOURSECS));
1121         }
1123         return $CFG->updatecronoffset;
1124     }
1126     /**
1127      * Fetch available updates info and eventually send notification to site admins
1128      */
1129     protected function cron_execute() {
1131         try {
1132             $this->restore_response();
1133             $previous = $this->recentresponse;
1134             $this->fetch();
1135             $this->restore_response(true);
1136             $current = $this->recentresponse;
1137             $changes = $this->compare_responses($previous, $current);
1138             $notifications = $this->cron_notifications($changes);
1139             $this->cron_notify($notifications);
1140             $this->cron_mtrace('done');
1141         } catch (available_update_checker_exception $e) {
1142             $this->cron_mtrace('FAILED!');
1143         }
1144     }
1146     /**
1147      * Given the list of changes in available updates, pick those to send to site admins
1148      *
1149      * @param array $changes as returned by {@link self::compare_responses()}
1150      * @return array of available_update_info objects to send to site admins
1151      */
1152     protected function cron_notifications(array $changes) {
1153         global $CFG;
1155         $notifications = array();
1156         $pluginman = plugin_manager::instance();
1157         $plugins = $pluginman->get_plugins(true);
1159         foreach ($changes as $component => $componentchanges) {
1160             if (empty($componentchanges)) {
1161                 continue;
1162             }
1163             $componentupdates = $this->get_update_info($component,
1164                 array('minmaturity' => $CFG->updateminmaturity, 'notifybuilds' => $CFG->updatenotifybuilds));
1165             if (empty($componentupdates)) {
1166                 continue;
1167             }
1168             // notify only about those $componentchanges that are present in $componentupdates
1169             // to respect the preferences
1170             foreach ($componentchanges as $componentchange) {
1171                 foreach ($componentupdates as $componentupdate) {
1172                     if ($componentupdate->version == $componentchange['version']) {
1173                         if ($component == 'core') {
1174                             // in case of 'core' this is enough, we already know that the
1175                             // $componentupdate is a real update with higher version
1176                             $notifications[] = $componentupdate;
1177                         } else {
1178                             // use the plugin_manager to check if the reported $componentchange
1179                             // is a real update with higher version. such a real update must be
1180                             // present in the 'availableupdates' property of one of the component's
1181                             // available_update_info object
1182                             list($plugintype, $pluginname) = normalize_component($component);
1183                             if (!empty($plugins[$plugintype][$pluginname]->availableupdates)) {
1184                                 foreach ($plugins[$plugintype][$pluginname]->availableupdates as $availableupdate) {
1185                                     if ($availableupdate->version == $componentchange['version']) {
1186                                         $notifications[] = $componentupdate;
1187                                     }
1188                                 }
1189                             }
1190                         }
1191                     }
1192                 }
1193             }
1194         }
1196         return $notifications;
1197     }
1199     /**
1200      * Sends the given notifications to site admins via messaging API
1201      *
1202      * @param array $notifications array of available_update_info objects to send
1203      */
1204     protected function cron_notify(array $notifications) {
1205         global $CFG;
1207         if (empty($notifications)) {
1208             return;
1209         }
1211         $admins = get_admins();
1213         if (empty($admins)) {
1214             return;
1215         }
1217         $this->cron_mtrace('sending notifications ... ', '');
1219         $text = get_string('updatenotifications', 'core_admin') . PHP_EOL;
1220         $html = html_writer::tag('h1', get_string('updatenotifications', 'core_admin')) . PHP_EOL;
1222         $coreupdates = array();
1223         $pluginupdates = array();
1225         foreach ($notifications as $notification) {
1226             if ($notification->component == 'core') {
1227                 $coreupdates[] = $notification;
1228             } else {
1229                 $pluginupdates[] = $notification;
1230             }
1231         }
1233         if (!empty($coreupdates)) {
1234             $text .= PHP_EOL . get_string('updateavailable', 'core_admin') . PHP_EOL;
1235             $html .= html_writer::tag('h2', get_string('updateavailable', 'core_admin')) . PHP_EOL;
1236             $html .= html_writer::start_tag('ul') . PHP_EOL;
1237             foreach ($coreupdates as $coreupdate) {
1238                 $html .= html_writer::start_tag('li');
1239                 if (isset($coreupdate->release)) {
1240                     $text .= get_string('updateavailable_release', 'core_admin', $coreupdate->release);
1241                     $html .= html_writer::tag('strong', get_string('updateavailable_release', 'core_admin', $coreupdate->release));
1242                 }
1243                 if (isset($coreupdate->version)) {
1244                     $text .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
1245                     $html .= ' '.get_string('updateavailable_version', 'core_admin', $coreupdate->version);
1246                 }
1247                 if (isset($coreupdate->maturity)) {
1248                     $text .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
1249                     $html .= ' ('.get_string('maturity'.$coreupdate->maturity, 'core_admin').')';
1250                 }
1251                 $text .= PHP_EOL;
1252                 $html .= html_writer::end_tag('li') . PHP_EOL;
1253             }
1254             $text .= PHP_EOL;
1255             $html .= html_writer::end_tag('ul') . PHP_EOL;
1257             $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/index.php');
1258             $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
1259             $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/index.php', $CFG->wwwroot.'/'.$CFG->admin.'/index.php'));
1260             $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
1261         }
1263         if (!empty($pluginupdates)) {
1264             $text .= PHP_EOL . get_string('updateavailableforplugin', 'core_admin') . PHP_EOL;
1265             $html .= html_writer::tag('h2', get_string('updateavailableforplugin', 'core_admin')) . PHP_EOL;
1267             $html .= html_writer::start_tag('ul') . PHP_EOL;
1268             foreach ($pluginupdates as $pluginupdate) {
1269                 $html .= html_writer::start_tag('li');
1270                 $text .= get_string('pluginname', $pluginupdate->component);
1271                 $html .= html_writer::tag('strong', get_string('pluginname', $pluginupdate->component));
1273                 $text .= ' ('.$pluginupdate->component.')';
1274                 $html .= ' ('.$pluginupdate->component.')';
1276                 $text .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
1277                 $html .= ' '.get_string('updateavailable', 'core_plugin', $pluginupdate->version);
1279                 $text .= PHP_EOL;
1280                 $html .= html_writer::end_tag('li') . PHP_EOL;
1281             }
1282             $text .= PHP_EOL;
1283             $html .= html_writer::end_tag('ul') . PHP_EOL;
1285             $a = array('url' => $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php');
1286             $text .= get_string('updateavailabledetailslink', 'core_admin', $a) . PHP_EOL;
1287             $a = array('url' => html_writer::link($CFG->wwwroot.'/'.$CFG->admin.'/plugins.php', $CFG->wwwroot.'/'.$CFG->admin.'/plugins.php'));
1288             $html .= html_writer::tag('p', get_string('updateavailabledetailslink', 'core_admin', $a)) . PHP_EOL;
1289         }
1291         $a = array('siteurl' => $CFG->wwwroot);
1292         $text .= get_string('updatenotificationfooter', 'core_admin', $a) . PHP_EOL;
1293         $a = array('siteurl' => html_writer::link($CFG->wwwroot, $CFG->wwwroot));
1294         $html .= html_writer::tag('footer', html_writer::tag('p', get_string('updatenotificationfooter', 'core_admin', $a),
1295             array('style' => 'font-size:smaller; color:#333;')));
1297         $mainadmin = reset($admins);
1299         foreach ($admins as $admin) {
1300             $message = new stdClass();
1301             $message->component         = 'moodle';
1302             $message->name              = 'availableupdate';
1303             $message->userfrom          = $mainadmin;
1304             $message->userto            = $admin;
1305             $message->subject           = get_string('updatenotificationsubject', 'core_admin', array('siteurl' => $CFG->wwwroot));
1306             $message->fullmessage       = $text;
1307             $message->fullmessageformat = FORMAT_PLAIN;
1308             $message->fullmessagehtml   = $html;
1309             $message->smallmessage      = get_string('updatenotifications', 'core_admin');
1310             $message->notification      = 1;
1311             message_send($message);
1312         }
1313     }
1315     /**
1316      * Compare two release labels and decide if they are the same
1317      *
1318      * @param string $remote release info of the available update
1319      * @param null|string $local release info of the local code, defaults to $release defined in version.php
1320      * @return boolean true if the releases declare the same minor+major version
1321      */
1322     protected function is_same_release($remote, $local=null) {
1324         if (is_null($local)) {
1325             $this->load_current_environment();
1326             $local = $this->currentrelease;
1327         }
1329         $pattern = '/^([0-9\.\+]+)([^(]*)/';
1331         preg_match($pattern, $remote, $remotematches);
1332         preg_match($pattern, $local, $localmatches);
1334         $remotematches[1] = str_replace('+', '', $remotematches[1]);
1335         $localmatches[1] = str_replace('+', '', $localmatches[1]);
1337         if ($remotematches[1] === $localmatches[1] and rtrim($remotematches[2]) === rtrim($localmatches[2])) {
1338             return true;
1339         } else {
1340             return false;
1341         }
1342     }
1346 /**
1347  * Defines the structure of objects returned by {@link available_update_checker::get_update_info()}
1348  */
1349 class available_update_info {
1351     /** @var string frankenstyle component name */
1352     public $component;
1353     /** @var int the available version of the component */
1354     public $version;
1355     /** @var string|null optional release name */
1356     public $release = null;
1357     /** @var int|null optional maturity info, eg {@link MATURITY_STABLE} */
1358     public $maturity = null;
1359     /** @var string|null optional URL of a page with more info about the update */
1360     public $url = null;
1361     /** @var string|null optional URL of a ZIP package that can be downloaded and installed */
1362     public $download = null;
1364     /**
1365      * Creates new instance of the class
1366      *
1367      * The $info array must provide at least the 'version' value and optionally all other
1368      * values to populate the object's properties.
1369      *
1370      * @param string $name the frankenstyle component name
1371      * @param array $info associative array with other properties
1372      */
1373     public function __construct($name, array $info) {
1374         $this->component = $name;
1375         foreach ($info as $k => $v) {
1376             if (property_exists('available_update_info', $k) and $k != 'component') {
1377                 $this->$k = $v;
1378             }
1379         }
1380     }
1384 /**
1385  * Factory class producing required subclasses of {@link plugininfo_base}
1386  */
1387 class plugininfo_default_factory {
1389     /**
1390      * Makes a new instance of the plugininfo class
1391      *
1392      * @param string $type the plugin type, eg. 'mod'
1393      * @param string $typerootdir full path to the location of all the plugins of this type
1394      * @param string $name the plugin name, eg. 'workshop'
1395      * @param string $namerootdir full path to the location of the plugin
1396      * @param string $typeclass the name of class that holds the info about the plugin
1397      * @return plugininfo_base the instance of $typeclass
1398      */
1399     public static function make($type, $typerootdir, $name, $namerootdir, $typeclass) {
1400         $plugin              = new $typeclass();
1401         $plugin->type        = $type;
1402         $plugin->typerootdir = $typerootdir;
1403         $plugin->name        = $name;
1404         $plugin->rootdir     = $namerootdir;
1406         $plugin->init_display_name();
1407         $plugin->load_disk_version();
1408         $plugin->load_db_version();
1409         $plugin->load_required_main_version();
1410         $plugin->init_is_standard();
1412         return $plugin;
1413     }
1417 /**
1418  * Base class providing access to the information about a plugin
1419  *
1420  * @property-read string component the component name, type_name
1421  */
1422 abstract class plugininfo_base {
1424     /** @var string the plugintype name, eg. mod, auth or workshopform */
1425     public $type;
1426     /** @var string full path to the location of all the plugins of this type */
1427     public $typerootdir;
1428     /** @var string the plugin name, eg. assignment, ldap */
1429     public $name;
1430     /** @var string the localized plugin name */
1431     public $displayname;
1432     /** @var string the plugin source, one of plugin_manager::PLUGIN_SOURCE_xxx constants */
1433     public $source;
1434     /** @var fullpath to the location of this plugin */
1435     public $rootdir;
1436     /** @var int|string the version of the plugin's source code */
1437     public $versiondisk;
1438     /** @var int|string the version of the installed plugin */
1439     public $versiondb;
1440     /** @var int|float|string required version of Moodle core  */
1441     public $versionrequires;
1442     /** @var array other plugins that this one depends on, lazy-loaded by {@link get_other_required_plugins()} */
1443     public $dependencies;
1444     /** @var int number of instances of the plugin - not supported yet */
1445     public $instances;
1446     /** @var int order of the plugin among other plugins of the same type - not supported yet */
1447     public $sortorder;
1448     /** @var array|null array of {@link available_update_info} for this plugin */
1449     public $availableupdates;
1451     /**
1452      * Gathers and returns the information about all plugins of the given type
1453      *
1454      * @param string $type the name of the plugintype, eg. mod, auth or workshopform
1455      * @param string $typerootdir full path to the location of the plugin dir
1456      * @param string $typeclass the name of the actually called class
1457      * @return array of plugintype classes, indexed by the plugin name
1458      */
1459     public static function get_plugins($type, $typerootdir, $typeclass) {
1461         // get the information about plugins at the disk
1462         $plugins = get_plugin_list($type);
1463         $ondisk = array();
1464         foreach ($plugins as $pluginname => $pluginrootdir) {
1465             $ondisk[$pluginname] = plugininfo_default_factory::make($type, $typerootdir,
1466                 $pluginname, $pluginrootdir, $typeclass);
1467         }
1468         return $ondisk;
1469     }
1471     /**
1472      * Sets {@link $displayname} property to a localized name of the plugin
1473      */
1474     public function init_display_name() {
1475         if (!get_string_manager()->string_exists('pluginname', $this->component)) {
1476             $this->displayname = '[pluginname,' . $this->component . ']';
1477         } else {
1478             $this->displayname = get_string('pluginname', $this->component);
1479         }
1480     }
1482     /**
1483      * Magic method getter, redirects to read only values.
1484      *
1485      * @param string $name
1486      * @return mixed
1487      */
1488     public function __get($name) {
1489         switch ($name) {
1490             case 'component': return $this->type . '_' . $this->name;
1492             default:
1493                 debugging('Invalid plugin property accessed! '.$name);
1494                 return null;
1495         }
1496     }
1498     /**
1499      * Return the full path name of a file within the plugin.
1500      *
1501      * No check is made to see if the file exists.
1502      *
1503      * @param string $relativepath e.g. 'version.php'.
1504      * @return string e.g. $CFG->dirroot . '/mod/quiz/version.php'.
1505      */
1506     public function full_path($relativepath) {
1507         if (empty($this->rootdir)) {
1508             return '';
1509         }
1510         return $this->rootdir . '/' . $relativepath;
1511     }
1513     /**
1514      * Load the data from version.php.
1515      *
1516      * @return stdClass the object called $plugin defined in version.php
1517      */
1518     protected function load_version_php() {
1519         $versionfile = $this->full_path('version.php');
1521         $plugin = new stdClass();
1522         if (is_readable($versionfile)) {
1523             include($versionfile);
1524         }
1525         return $plugin;
1526     }
1528     /**
1529      * Sets {@link $versiondisk} property to a numerical value representing the
1530      * version of the plugin's source code.
1531      *
1532      * If the value is null after calling this method, either the plugin
1533      * does not use versioning (typically does not have any database
1534      * data) or is missing from disk.
1535      */
1536     public function load_disk_version() {
1537         $plugin = $this->load_version_php();
1538         if (isset($plugin->version)) {
1539             $this->versiondisk = $plugin->version;
1540         }
1541     }
1543     /**
1544      * Sets {@link $versionrequires} property to a numerical value representing
1545      * the version of Moodle core that this plugin requires.
1546      */
1547     public function load_required_main_version() {
1548         $plugin = $this->load_version_php();
1549         if (isset($plugin->requires)) {
1550             $this->versionrequires = $plugin->requires;
1551         }
1552     }
1554     /**
1555      * Initialise {@link $dependencies} to the list of other plugins (in any)
1556      * that this one requires to be installed.
1557      */
1558     protected function load_other_required_plugins() {
1559         $plugin = $this->load_version_php();
1560         if (!empty($plugin->dependencies)) {
1561             $this->dependencies = $plugin->dependencies;
1562         } else {
1563             $this->dependencies = array(); // By default, no dependencies.
1564         }
1565     }
1567     /**
1568      * Get the list of other plugins that this plugin requires to be installed.
1569      *
1570      * @return array with keys the frankenstyle plugin name, and values either
1571      *      a version string (like '2011101700') or the constant ANY_VERSION.
1572      */
1573     public function get_other_required_plugins() {
1574         if (is_null($this->dependencies)) {
1575             $this->load_other_required_plugins();
1576         }
1577         return $this->dependencies;
1578     }
1580     /**
1581      * Sets {@link $versiondb} property to a numerical value representing the
1582      * currently installed version of the plugin.
1583      *
1584      * If the value is null after calling this method, either the plugin
1585      * does not use versioning (typically does not have any database
1586      * data) or has not been installed yet.
1587      */
1588     public function load_db_version() {
1589         if ($ver = self::get_version_from_config_plugins($this->component)) {
1590             $this->versiondb = $ver;
1591         }
1592     }
1594     /**
1595      * Sets {@link $source} property to one of plugin_manager::PLUGIN_SOURCE_xxx
1596      * constants.
1597      *
1598      * If the property's value is null after calling this method, then
1599      * the type of the plugin has not been recognized and you should throw
1600      * an exception.
1601      */
1602     public function init_is_standard() {
1604         $standard = plugin_manager::standard_plugins_list($this->type);
1606         if ($standard !== false) {
1607             $standard = array_flip($standard);
1608             if (isset($standard[$this->name])) {
1609                 $this->source = plugin_manager::PLUGIN_SOURCE_STANDARD;
1610             } else if (!is_null($this->versiondb) and is_null($this->versiondisk)
1611                     and plugin_manager::is_deleted_standard_plugin($this->type, $this->name)) {
1612                 $this->source = plugin_manager::PLUGIN_SOURCE_STANDARD; // to be deleted
1613             } else {
1614                 $this->source = plugin_manager::PLUGIN_SOURCE_EXTENSION;
1615             }
1616         }
1617     }
1619     /**
1620      * Returns true if the plugin is shipped with the official distribution
1621      * of the current Moodle version, false otherwise.
1622      *
1623      * @return bool
1624      */
1625     public function is_standard() {
1626         return $this->source === plugin_manager::PLUGIN_SOURCE_STANDARD;
1627     }
1629     /**
1630      * Returns true if the the given Moodle version is enough to run this plugin
1631      *
1632      * @param string|int|double $moodleversion
1633      * @return bool
1634      */
1635     public function is_core_dependency_satisfied($moodleversion) {
1637         if (empty($this->versionrequires)) {
1638             return true;
1640         } else {
1641             return (double)$this->versionrequires <= (double)$moodleversion;
1642         }
1643     }
1645     /**
1646      * Returns the status of the plugin
1647      *
1648      * @return string one of plugin_manager::PLUGIN_STATUS_xxx constants
1649      */
1650     public function get_status() {
1652         if (is_null($this->versiondb) and is_null($this->versiondisk)) {
1653             return plugin_manager::PLUGIN_STATUS_NODB;
1655         } else if (is_null($this->versiondb) and !is_null($this->versiondisk)) {
1656             return plugin_manager::PLUGIN_STATUS_NEW;
1658         } else if (!is_null($this->versiondb) and is_null($this->versiondisk)) {
1659             if (plugin_manager::is_deleted_standard_plugin($this->type, $this->name)) {
1660                 return plugin_manager::PLUGIN_STATUS_DELETE;
1661             } else {
1662                 return plugin_manager::PLUGIN_STATUS_MISSING;
1663             }
1665         } else if ((string)$this->versiondb === (string)$this->versiondisk) {
1666             return plugin_manager::PLUGIN_STATUS_UPTODATE;
1668         } else if ($this->versiondb < $this->versiondisk) {
1669             return plugin_manager::PLUGIN_STATUS_UPGRADE;
1671         } else if ($this->versiondb > $this->versiondisk) {
1672             return plugin_manager::PLUGIN_STATUS_DOWNGRADE;
1674         } else {
1675             // $version = pi(); and similar funny jokes - hopefully Donald E. Knuth will never contribute to Moodle ;-)
1676             throw new coding_exception('Unable to determine plugin state, check the plugin versions');
1677         }
1678     }
1680     /**
1681      * Returns the information about plugin availability
1682      *
1683      * True means that the plugin is enabled. False means that the plugin is
1684      * disabled. Null means that the information is not available, or the
1685      * plugin does not support configurable availability or the availability
1686      * can not be changed.
1687      *
1688      * @return null|bool
1689      */
1690     public function is_enabled() {
1691         return null;
1692     }
1694     /**
1695      * Populates the property {@link $availableupdates} with the information provided by
1696      * available update checker
1697      *
1698      * @param available_update_checker $provider the class providing the available update info
1699      */
1700     public function check_available_updates(available_update_checker $provider) {
1701         global $CFG;
1703         if (isset($CFG->updateminmaturity)) {
1704             $minmaturity = $CFG->updateminmaturity;
1705         } else {
1706             // this can happen during the very first upgrade to 2.3
1707             $minmaturity = MATURITY_STABLE;
1708         }
1710         $this->availableupdates = $provider->get_update_info($this->component,
1711             array('minmaturity' => $minmaturity));
1712     }
1714     /**
1715      * If there are updates for this plugin available, returns them.
1716      *
1717      * Returns array of {@link available_update_info} objects, if some update
1718      * is available. Returns null if there is no update available or if the update
1719      * availability is unknown.
1720      *
1721      * @return array|null
1722      */
1723     public function available_updates() {
1725         if (empty($this->availableupdates) or !is_array($this->availableupdates)) {
1726             return null;
1727         }
1729         $updates = array();
1731         foreach ($this->availableupdates as $availableupdate) {
1732             if ($availableupdate->version > $this->versiondisk) {
1733                 $updates[] = $availableupdate;
1734             }
1735         }
1737         if (empty($updates)) {
1738             return null;
1739         }
1741         return $updates;
1742     }
1744     /**
1745      * Returns the URL of the plugin settings screen
1746      *
1747      * Null value means that the plugin either does not have the settings screen
1748      * or its location is not available via this library.
1749      *
1750      * @return null|moodle_url
1751      */
1752     public function get_settings_url() {
1753         return null;
1754     }
1756     /**
1757      * Returns the URL of the screen where this plugin can be uninstalled
1758      *
1759      * Visiting that URL must be safe, that is a manual confirmation is needed
1760      * for actual uninstallation of the plugin. Null value means that the
1761      * plugin either does not support uninstallation, or does not require any
1762      * database cleanup or the location of the screen is not available via this
1763      * library.
1764      *
1765      * @return null|moodle_url
1766      */
1767     public function get_uninstall_url() {
1768         return null;
1769     }
1771     /**
1772      * Returns relative directory of the plugin with heading '/'
1773      *
1774      * @return string
1775      */
1776     public function get_dir() {
1777         global $CFG;
1779         return substr($this->rootdir, strlen($CFG->dirroot));
1780     }
1782     /**
1783      * Provides access to plugin versions from {config_plugins}
1784      *
1785      * @param string $plugin plugin name
1786      * @param double $disablecache optional, defaults to false
1787      * @return int|false the stored value or false if not found
1788      */
1789     protected function get_version_from_config_plugins($plugin, $disablecache=false) {
1790         global $DB;
1791         static $pluginversions = null;
1793         if (is_null($pluginversions) or $disablecache) {
1794             try {
1795                 $pluginversions = $DB->get_records_menu('config_plugins', array('name' => 'version'), 'plugin', 'plugin,value');
1796             } catch (dml_exception $e) {
1797                 // before install
1798                 $pluginversions = array();
1799             }
1800         }
1802         if (!array_key_exists($plugin, $pluginversions)) {
1803             return false;
1804         }
1806         return $pluginversions[$plugin];
1807     }
1811 /**
1812  * General class for all plugin types that do not have their own class
1813  */
1814 class plugininfo_general extends plugininfo_base {
1818 /**
1819  * Class for page side blocks
1820  */
1821 class plugininfo_block extends plugininfo_base {
1823     public static function get_plugins($type, $typerootdir, $typeclass) {
1825         // get the information about blocks at the disk
1826         $blocks = parent::get_plugins($type, $typerootdir, $typeclass);
1828         // add blocks missing from disk
1829         $blocksinfo = self::get_blocks_info();
1830         foreach ($blocksinfo as $blockname => $blockinfo) {
1831             if (isset($blocks[$blockname])) {
1832                 continue;
1833             }
1834             $plugin                 = new $typeclass();
1835             $plugin->type           = $type;
1836             $plugin->typerootdir    = $typerootdir;
1837             $plugin->name           = $blockname;
1838             $plugin->rootdir        = null;
1839             $plugin->displayname    = $blockname;
1840             $plugin->versiondb      = $blockinfo->version;
1841             $plugin->init_is_standard();
1843             $blocks[$blockname]   = $plugin;
1844         }
1846         return $blocks;
1847     }
1849     public function init_display_name() {
1851         if (get_string_manager()->string_exists('pluginname', 'block_' . $this->name)) {
1852             $this->displayname = get_string('pluginname', 'block_' . $this->name);
1854         } else if (($block = block_instance($this->name)) !== false) {
1855             $this->displayname = $block->get_title();
1857         } else {
1858             parent::init_display_name();
1859         }
1860     }
1862     public function load_db_version() {
1863         global $DB;
1865         $blocksinfo = self::get_blocks_info();
1866         if (isset($blocksinfo[$this->name]->version)) {
1867             $this->versiondb = $blocksinfo[$this->name]->version;
1868         }
1869     }
1871     public function is_enabled() {
1873         $blocksinfo = self::get_blocks_info();
1874         if (isset($blocksinfo[$this->name]->visible)) {
1875             if ($blocksinfo[$this->name]->visible) {
1876                 return true;
1877             } else {
1878                 return false;
1879             }
1880         } else {
1881             return parent::is_enabled();
1882         }
1883     }
1885     public function get_settings_url() {
1887         if (($block = block_instance($this->name)) === false) {
1888             return parent::get_settings_url();
1890         } else if ($block->has_config()) {
1891             if (file_exists($this->full_path('settings.php'))) {
1892                 return new moodle_url('/admin/settings.php', array('section' => 'blocksetting' . $this->name));
1893             } else {
1894                 $blocksinfo = self::get_blocks_info();
1895                 return new moodle_url('/admin/block.php', array('block' => $blocksinfo[$this->name]->id));
1896             }
1898         } else {
1899             return parent::get_settings_url();
1900         }
1901     }
1903     public function get_uninstall_url() {
1905         $blocksinfo = self::get_blocks_info();
1906         return new moodle_url('/admin/blocks.php', array('delete' => $blocksinfo[$this->name]->id, 'sesskey' => sesskey()));
1907     }
1909     /**
1910      * Provides access to the records in {block} table
1911      *
1912      * @param bool $disablecache do not use internal static cache
1913      * @return array array of stdClasses
1914      */
1915     protected static function get_blocks_info($disablecache=false) {
1916         global $DB;
1917         static $blocksinfocache = null;
1919         if (is_null($blocksinfocache) or $disablecache) {
1920             try {
1921                 $blocksinfocache = $DB->get_records('block', null, 'name', 'name,id,version,visible');
1922             } catch (dml_exception $e) {
1923                 // before install
1924                 $blocksinfocache = array();
1925             }
1926         }
1928         return $blocksinfocache;
1929     }
1933 /**
1934  * Class for text filters
1935  */
1936 class plugininfo_filter extends plugininfo_base {
1938     public static function get_plugins($type, $typerootdir, $typeclass) {
1939         global $CFG, $DB;
1941         $filters = array();
1943         // get the list of filters from both /filter and /mod location
1944         $installed = filter_get_all_installed();
1946         foreach ($installed as $filterlegacyname => $displayname) {
1947             $plugin                 = new $typeclass();
1948             $plugin->type           = $type;
1949             $plugin->typerootdir    = $typerootdir;
1950             $plugin->name           = self::normalize_legacy_name($filterlegacyname);
1951             $plugin->rootdir        = $CFG->dirroot . '/' . $filterlegacyname;
1952             $plugin->displayname    = $displayname;
1954             $plugin->load_disk_version();
1955             $plugin->load_db_version();
1956             $plugin->load_required_main_version();
1957             $plugin->init_is_standard();
1959             $filters[$plugin->name] = $plugin;
1960         }
1962         $globalstates = self::get_global_states();
1964         if ($DB->get_manager()->table_exists('filter_active')) {
1965             // if we're upgrading from 1.9, the table does not exist yet
1966             // if it does, make sure that all installed filters are registered
1967             $needsreload  = false;
1968             foreach (array_keys($installed) as $filterlegacyname) {
1969                 if (!isset($globalstates[self::normalize_legacy_name($filterlegacyname)])) {
1970                     filter_set_global_state($filterlegacyname, TEXTFILTER_DISABLED);
1971                     $needsreload = true;
1972                 }
1973             }
1974             if ($needsreload) {
1975                 $globalstates = self::get_global_states(true);
1976             }
1977         }
1979         // make sure that all registered filters are installed, just in case
1980         foreach ($globalstates as $name => $info) {
1981             if (!isset($filters[$name])) {
1982                 // oops, there is a record in filter_active but the filter is not installed
1983                 $plugin                 = new $typeclass();
1984                 $plugin->type           = $type;
1985                 $plugin->typerootdir    = $typerootdir;
1986                 $plugin->name           = $name;
1987                 $plugin->rootdir        = $CFG->dirroot . '/' . $info->legacyname;
1988                 $plugin->displayname    = $info->legacyname;
1990                 $plugin->load_db_version();
1992                 if (is_null($plugin->versiondb)) {
1993                     // this is a hack to stimulate 'Missing from disk' error
1994                     // because $plugin->versiondisk will be null !== false
1995                     $plugin->versiondb = false;
1996                 }
1998                 $filters[$plugin->name] = $plugin;
1999             }
2000         }
2002         return $filters;
2003     }
2005     public function init_display_name() {
2006         // do nothing, the name is set in self::get_plugins()
2007     }
2009     /**
2010      * @see load_version_php()
2011      */
2012     protected function load_version_php() {
2013         if (strpos($this->name, 'mod_') === 0) {
2014             // filters bundled with modules do not have a version.php and so
2015             // do not provide their own versioning information.
2016             return new stdClass();
2017         }
2018         return parent::load_version_php();
2019     }
2021     public function is_enabled() {
2023         $globalstates = self::get_global_states();
2025         foreach ($globalstates as $filterlegacyname => $info) {
2026             $name = self::normalize_legacy_name($filterlegacyname);
2027             if ($name === $this->name) {
2028                 if ($info->active == TEXTFILTER_DISABLED) {
2029                     return false;
2030                 } else {
2031                     // it may be 'On' or 'Off, but available'
2032                     return null;
2033                 }
2034             }
2035         }
2037         return null;
2038     }
2040     public function get_settings_url() {
2042         $globalstates = self::get_global_states();
2043         $legacyname = $globalstates[$this->name]->legacyname;
2044         if (filter_has_global_settings($legacyname)) {
2045             return new moodle_url('/admin/settings.php', array('section' => 'filtersetting' . str_replace('/', '', $legacyname)));
2046         } else {
2047             return null;
2048         }
2049     }
2051     public function get_uninstall_url() {
2053         if (strpos($this->name, 'mod_') === 0) {
2054             return null;
2055         } else {
2056             $globalstates = self::get_global_states();
2057             $legacyname = $globalstates[$this->name]->legacyname;
2058             return new moodle_url('/admin/filters.php', array('sesskey' => sesskey(), 'filterpath' => $legacyname, 'action' => 'delete'));
2059         }
2060     }
2062     /**
2063      * Convert legacy filter names like 'filter/foo' or 'mod/bar' into frankenstyle
2064      *
2065      * @param string $legacyfiltername legacy filter name
2066      * @return string frankenstyle-like name
2067      */
2068     protected static function normalize_legacy_name($legacyfiltername) {
2070         $name = str_replace('/', '_', $legacyfiltername);
2071         if (strpos($name, 'filter_') === 0) {
2072             $name = substr($name, 7);
2073             if (empty($name)) {
2074                 throw new coding_exception('Unable to determine filter name: ' . $legacyfiltername);
2075             }
2076         }
2078         return $name;
2079     }
2081     /**
2082      * Provides access to the results of {@link filter_get_global_states()}
2083      * but indexed by the normalized filter name
2084      *
2085      * The legacy filter name is available as ->legacyname property.
2086      *
2087      * @param bool $disablecache
2088      * @return array
2089      */
2090     protected static function get_global_states($disablecache=false) {
2091         global $DB;
2092         static $globalstatescache = null;
2094         if ($disablecache or is_null($globalstatescache)) {
2096             if (!$DB->get_manager()->table_exists('filter_active')) {
2097                 // we're upgrading from 1.9 and the table used by {@link filter_get_global_states()}
2098                 // does not exist yet
2099                 $globalstatescache = array();
2101             } else {
2102                 foreach (filter_get_global_states() as $legacyname => $info) {
2103                     $name                       = self::normalize_legacy_name($legacyname);
2104                     $filterinfo                 = new stdClass();
2105                     $filterinfo->legacyname     = $legacyname;
2106                     $filterinfo->active         = $info->active;
2107                     $filterinfo->sortorder      = $info->sortorder;
2108                     $globalstatescache[$name]   = $filterinfo;
2109                 }
2110             }
2111         }
2113         return $globalstatescache;
2114     }
2118 /**
2119  * Class for activity modules
2120  */
2121 class plugininfo_mod extends plugininfo_base {
2123     public static function get_plugins($type, $typerootdir, $typeclass) {
2125         // get the information about plugins at the disk
2126         $modules = parent::get_plugins($type, $typerootdir, $typeclass);
2128         // add modules missing from disk
2129         $modulesinfo = self::get_modules_info();
2130         foreach ($modulesinfo as $modulename => $moduleinfo) {
2131             if (isset($modules[$modulename])) {
2132                 continue;
2133             }
2134             $plugin                 = new $typeclass();
2135             $plugin->type           = $type;
2136             $plugin->typerootdir    = $typerootdir;
2137             $plugin->name           = $modulename;
2138             $plugin->rootdir        = null;
2139             $plugin->displayname    = $modulename;
2140             $plugin->versiondb      = $moduleinfo->version;
2141             $plugin->init_is_standard();
2143             $modules[$modulename]   = $plugin;
2144         }
2146         return $modules;
2147     }
2149     public function init_display_name() {
2150         if (get_string_manager()->string_exists('pluginname', $this->component)) {
2151             $this->displayname = get_string('pluginname', $this->component);
2152         } else {
2153             $this->displayname = get_string('modulename', $this->component);
2154         }
2155     }
2157     /**
2158      * Load the data from version.php.
2159      * @return object the data object defined in version.php.
2160      */
2161     protected function load_version_php() {
2162         $versionfile = $this->full_path('version.php');
2164         $module = new stdClass();
2165         if (is_readable($versionfile)) {
2166             include($versionfile);
2167         }
2168         return $module;
2169     }
2171     public function load_db_version() {
2172         global $DB;
2174         $modulesinfo = self::get_modules_info();
2175         if (isset($modulesinfo[$this->name]->version)) {
2176             $this->versiondb = $modulesinfo[$this->name]->version;
2177         }
2178     }
2180     public function is_enabled() {
2182         $modulesinfo = self::get_modules_info();
2183         if (isset($modulesinfo[$this->name]->visible)) {
2184             if ($modulesinfo[$this->name]->visible) {
2185                 return true;
2186             } else {
2187                 return false;
2188             }
2189         } else {
2190             return parent::is_enabled();
2191         }
2192     }
2194     public function get_settings_url() {
2196         if (file_exists($this->full_path('settings.php')) or file_exists($this->full_path('settingstree.php'))) {
2197             return new moodle_url('/admin/settings.php', array('section' => 'modsetting' . $this->name));
2198         } else {
2199             return parent::get_settings_url();
2200         }
2201     }
2203     public function get_uninstall_url() {
2205         if ($this->name !== 'forum') {
2206             return new moodle_url('/admin/modules.php', array('delete' => $this->name, 'sesskey' => sesskey()));
2207         } else {
2208             return null;
2209         }
2210     }
2212     /**
2213      * Provides access to the records in {modules} table
2214      *
2215      * @param bool $disablecache do not use internal static cache
2216      * @return array array of stdClasses
2217      */
2218     protected static function get_modules_info($disablecache=false) {
2219         global $DB;
2220         static $modulesinfocache = null;
2222         if (is_null($modulesinfocache) or $disablecache) {
2223             try {
2224                 $modulesinfocache = $DB->get_records('modules', null, 'name', 'name,id,version,visible');
2225             } catch (dml_exception $e) {
2226                 // before install
2227                 $modulesinfocache = array();
2228             }
2229         }
2231         return $modulesinfocache;
2232     }
2236 /**
2237  * Class for question behaviours.
2238  */
2239 class plugininfo_qbehaviour extends plugininfo_base {
2241     public function get_uninstall_url() {
2242         return new moodle_url('/admin/qbehaviours.php',
2243                 array('delete' => $this->name, 'sesskey' => sesskey()));
2244     }
2248 /**
2249  * Class for question types
2250  */
2251 class plugininfo_qtype extends plugininfo_base {
2253     public function get_uninstall_url() {
2254         return new moodle_url('/admin/qtypes.php',
2255                 array('delete' => $this->name, 'sesskey' => sesskey()));
2256     }
2260 /**
2261  * Class for authentication plugins
2262  */
2263 class plugininfo_auth extends plugininfo_base {
2265     public function is_enabled() {
2266         global $CFG;
2267         /** @var null|array list of enabled authentication plugins */
2268         static $enabled = null;
2270         if (in_array($this->name, array('nologin', 'manual'))) {
2271             // these two are always enabled and can't be disabled
2272             return null;
2273         }
2275         if (is_null($enabled)) {
2276             $enabled = array_flip(explode(',', $CFG->auth));
2277         }
2279         return isset($enabled[$this->name]);
2280     }
2282     public function get_settings_url() {
2283         if (file_exists($this->full_path('settings.php'))) {
2284             return new moodle_url('/admin/settings.php', array('section' => 'authsetting' . $this->name));
2285         } else {
2286             return new moodle_url('/admin/auth_config.php', array('auth' => $this->name));
2287         }
2288     }
2292 /**
2293  * Class for enrolment plugins
2294  */
2295 class plugininfo_enrol extends plugininfo_base {
2297     public function is_enabled() {
2298         global $CFG;
2299         /** @var null|array list of enabled enrolment plugins */
2300         static $enabled = null;
2302         // We do not actually need whole enrolment classes here so we do not call
2303         // {@link enrol_get_plugins()}. Note that this may produce slightly different
2304         // results, for example if the enrolment plugin does not contain lib.php
2305         // but it is listed in $CFG->enrol_plugins_enabled
2307         if (is_null($enabled)) {
2308             $enabled = array_flip(explode(',', $CFG->enrol_plugins_enabled));
2309         }
2311         return isset($enabled[$this->name]);
2312     }
2314     public function get_settings_url() {
2316         if ($this->is_enabled() or file_exists($this->full_path('settings.php'))) {
2317             return new moodle_url('/admin/settings.php', array('section' => 'enrolsettings' . $this->name));
2318         } else {
2319             return parent::get_settings_url();
2320         }
2321     }
2323     public function get_uninstall_url() {
2324         return new moodle_url('/admin/enrol.php', array('action' => 'uninstall', 'enrol' => $this->name, 'sesskey' => sesskey()));
2325     }
2329 /**
2330  * Class for messaging processors
2331  */
2332 class plugininfo_message extends plugininfo_base {
2334     public function get_settings_url() {
2335         $processors = get_message_processors();
2336         if (isset($processors[$this->name])) {
2337             $processor = $processors[$this->name];
2338             if ($processor->available && $processor->hassettings) {
2339                 return new moodle_url('settings.php', array('section' => 'messagesetting'.$processor->name));
2340             }
2341         }
2342         return parent::get_settings_url();
2343     }
2345     /**
2346      * @see plugintype_interface::is_enabled()
2347      */
2348     public function is_enabled() {
2349         $processors = get_message_processors();
2350         if (isset($processors[$this->name])) {
2351             return $processors[$this->name]->configured && $processors[$this->name]->enabled;
2352         } else {
2353             return parent::is_enabled();
2354         }
2355     }
2357     /**
2358      * @see plugintype_interface::get_uninstall_url()
2359      */
2360     public function get_uninstall_url() {
2361         $processors = get_message_processors();
2362         if (isset($processors[$this->name])) {
2363             return new moodle_url('message.php', array('uninstall' => $processors[$this->name]->id, 'sesskey' => sesskey()));
2364         } else {
2365             return parent::get_uninstall_url();
2366         }
2367     }
2371 /**
2372  * Class for repositories
2373  */
2374 class plugininfo_repository extends plugininfo_base {
2376     public function is_enabled() {
2378         $enabled = self::get_enabled_repositories();
2380         return isset($enabled[$this->name]);
2381     }
2383     public function get_settings_url() {
2385         if ($this->is_enabled()) {
2386             return new moodle_url('/admin/repository.php', array('sesskey' => sesskey(), 'action' => 'edit', 'repos' => $this->name));
2387         } else {
2388             return parent::get_settings_url();
2389         }
2390     }
2392     /**
2393      * Provides access to the records in {repository} table
2394      *
2395      * @param bool $disablecache do not use internal static cache
2396      * @return array array of stdClasses
2397      */
2398     protected static function get_enabled_repositories($disablecache=false) {
2399         global $DB;
2400         static $repositories = null;
2402         if (is_null($repositories) or $disablecache) {
2403             $repositories = $DB->get_records('repository', null, 'type', 'type,visible,sortorder');
2404         }
2406         return $repositories;
2407     }
2411 /**
2412  * Class for portfolios
2413  */
2414 class plugininfo_portfolio extends plugininfo_base {
2416     public function is_enabled() {
2418         $enabled = self::get_enabled_portfolios();
2420         return isset($enabled[$this->name]);
2421     }
2423     /**
2424      * Provides access to the records in {portfolio_instance} table
2425      *
2426      * @param bool $disablecache do not use internal static cache
2427      * @return array array of stdClasses
2428      */
2429     protected static function get_enabled_portfolios($disablecache=false) {
2430         global $DB;
2431         static $portfolios = null;
2433         if (is_null($portfolios) or $disablecache) {
2434             $portfolios = array();
2435             $instances  = $DB->get_recordset('portfolio_instance', null, 'plugin');
2436             foreach ($instances as $instance) {
2437                 if (isset($portfolios[$instance->plugin])) {
2438                     if ($instance->visible) {
2439                         $portfolios[$instance->plugin]->visible = $instance->visible;
2440                     }
2441                 } else {
2442                     $portfolios[$instance->plugin] = $instance;
2443                 }
2444             }
2445         }
2447         return $portfolios;
2448     }
2452 /**
2453  * Class for themes
2454  */
2455 class plugininfo_theme extends plugininfo_base {
2457     public function is_enabled() {
2458         global $CFG;
2460         if ((!empty($CFG->theme) and $CFG->theme === $this->name) or
2461             (!empty($CFG->themelegacy) and $CFG->themelegacy === $this->name)) {
2462             return true;
2463         } else {
2464             return parent::is_enabled();
2465         }
2466     }
2470 /**
2471  * Class representing an MNet service
2472  */
2473 class plugininfo_mnetservice extends plugininfo_base {
2475     public function is_enabled() {
2476         global $CFG;
2478         if (empty($CFG->mnet_dispatcher_mode) || $CFG->mnet_dispatcher_mode !== 'strict') {
2479             return false;
2480         } else {
2481             return parent::is_enabled();
2482         }
2483     }
2487 /**
2488  * Class for admin tool plugins
2489  */
2490 class plugininfo_tool extends plugininfo_base {
2492     public function get_uninstall_url() {
2493         return new moodle_url('/admin/tools.php', array('delete' => $this->name, 'sesskey' => sesskey()));
2494     }
2498 /**
2499  * Class for admin tool plugins
2500  */
2501 class plugininfo_report extends plugininfo_base {
2503     public function get_uninstall_url() {
2504         return new moodle_url('/admin/reports.php', array('delete' => $this->name, 'sesskey' => sesskey()));
2505     }
2509 /**
2510  * Class for local plugins
2511  */
2512 class plugininfo_local extends plugininfo_base {
2514     public function get_uninstall_url() {
2515         return new moodle_url('/admin/localplugins.php', array('delete' => $this->name, 'sesskey' => sesskey()));
2516     }