MDL-48766 lib: Import MaxMind GeoIP2 PHP API
[moodle.git] / lib / classes / component.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Components (core subsystems + plugins) related code.
19  *
20  * @package    core
21  * @copyright  2013 Petr Skoda {@link http://skodak.org}
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 defined('MOODLE_INTERNAL') || die();
27 // Constants used in version.php files, these must exist when core_component executes.
29 /** Software maturity level - internals can be tested using white box techniques. */
30 define('MATURITY_ALPHA',    50);
31 /** Software maturity level - feature complete, ready for preview and testing. */
32 define('MATURITY_BETA',     100);
33 /** Software maturity level - tested, will be released unless there are fatal bugs. */
34 define('MATURITY_RC',       150);
35 /** Software maturity level - ready for production deployment. */
36 define('MATURITY_STABLE',   200);
37 /** Any version - special value that can be used in $plugin->dependencies in version.php files. */
38 define('ANY_VERSION', 'any');
41 /**
42  * Collection of components related methods.
43  */
44 class core_component {
45     /** @var array list of ignored directories - watch out for auth/db exception */
46     protected static $ignoreddirs = array('CVS'=>true, '_vti_cnf'=>true, 'simpletest'=>true, 'db'=>true, 'yui'=>true, 'tests'=>true, 'classes'=>true, 'fonts'=>true);
47     /** @var array list plugin types that support subplugins, do not add more here unless absolutely necessary */
48     protected static $supportsubplugins = array('mod', 'editor', 'tool', 'local');
50     /** @var array cache of plugin types */
51     protected static $plugintypes = null;
52     /** @var array cache of plugin locations */
53     protected static $plugins = null;
54     /** @var array cache of core subsystems */
55     protected static $subsystems = null;
56     /** @var array subplugin type parents */
57     protected static $parents = null;
58     /** @var array subplugins */
59     protected static $subplugins = null;
60     /** @var array list of all known classes that can be autoloaded */
61     protected static $classmap = null;
62     /** @var array list of all classes that have been renamed to be autoloaded */
63     protected static $classmaprenames = null;
64     /** @var array list of some known files that can be included. */
65     protected static $filemap = null;
66     /** @var int|float core version. */
67     protected static $version = null;
68     /** @var array list of the files to map. */
69     protected static $filestomap = array('lib.php', 'settings.php');
70     /** @var array associative array of PSR-0 namespaces and corresponding paths. */
71     protected static $psr0namespaces = array(
72         'Horde' => 'lib/horde/framework/Horde'
73     );
74     /** @var array associative array of PRS-4 namespaces and corresponding paths. */
75     protected static $psr4namespaces = array(
76         'MaxMind' => 'lib/maxmind/MaxMind',
77         'GeoIp2' => 'lib/maxmind/GeoIP2',
78     );
80     /**
81      * Class loader for Frankenstyle named classes in standard locations.
82      * Frankenstyle namespaces are supported.
83      *
84      * The expected location for core classes is:
85      *    1/ core_xx_yy_zz ---> lib/classes/xx_yy_zz.php
86      *    2/ \core\xx_yy_zz ---> lib/classes/xx_yy_zz.php
87      *    3/ \core\xx\yy_zz ---> lib/classes/xx/yy_zz.php
88      *
89      * The expected location for plugin classes is:
90      *    1/ mod_name_xx_yy_zz ---> mod/name/classes/xx_yy_zz.php
91      *    2/ \mod_name\xx_yy_zz ---> mod/name/classes/xx_yy_zz.php
92      *    3/ \mod_name\xx\yy_zz ---> mod/name/classes/xx/yy_zz.php
93      *
94      * @param string $classname
95      */
96     public static function classloader($classname) {
97         self::init();
99         if (isset(self::$classmap[$classname])) {
100             // Global $CFG is expected in included scripts.
101             global $CFG;
102             // Function include would be faster, but for BC it is better to include only once.
103             include_once(self::$classmap[$classname]);
104             return;
105         }
106         if (isset(self::$classmaprenames[$classname]) && isset(self::$classmap[self::$classmaprenames[$classname]])) {
107             $newclassname = self::$classmaprenames[$classname];
108             $debugging = "Class '%s' has been renamed for the autoloader and is now deprecated. Please use '%s' instead.";
109             debugging(sprintf($debugging, $classname, $newclassname), DEBUG_DEVELOPER);
110             if (PHP_VERSION_ID >= 70000 && preg_match('#\\\null(\\\|$)#', $classname)) {
111                 throw new \coding_exception("Cannot alias $classname to $newclassname");
112             }
113             class_alias($newclassname, $classname);
114             return;
115         }
117         $file = self::psr_classloader($classname);
118         // If the file is found, require it.
119         if (!empty($file)) {
120             require($file);
121             return;
122         }
123     }
125     /**
126      * Return the path to a class from our defined PSR-0 or PSR-4 standard namespaces on
127      * demand. Only returns paths to files that exist.
128      *
129      * Adapated from http://www.php-fig.org/psr/psr-4/examples/ and made PSR-0
130      * compatible.
131      *
132      * @param string $class the name of the class.
133      * @return string|bool The full path to the file defining the class. Or false if it could not be resolved or does not exist.
134      */
135     protected static function psr_classloader($class) {
136         // Iterate through each PSR-4 namespace prefix.
137         foreach (self::$psr4namespaces as $prefix => $path) {
138             $file = self::get_class_file($class, $prefix, $path, array('\\'));
139             if (!empty($file) && file_exists($file)) {
140                 return $file;
141             }
142         }
144         // Iterate through each PSR-0 namespace prefix.
145         foreach (self::$psr0namespaces as $prefix => $path) {
146             $file = self::get_class_file($class, $prefix, $path, array('\\', '_'));
147             if (!empty($file) && file_exists($file)) {
148                 return $file;
149             }
150         }
152         return false;
153     }
155     /**
156      * Return the path to the class based on the given namespace prefix and path it corresponds to.
157      *
158      * Will return the path even if the file does not exist. Check the file esists before requiring.
159      *
160      * @param string $class the name of the class.
161      * @param string $prefix The namespace prefix used to identify the base directory of the source files.
162      * @param string $path The relative path to the base directory of the source files.
163      * @param string[] $separators The characters that should be used for separating.
164      * @return string|bool The full path to the file defining the class. Or false if it could not be resolved.
165      */
166     protected static function get_class_file($class, $prefix, $path, $separators) {
167         global $CFG;
169         // Does the class use the namespace prefix?
170         $len = strlen($prefix);
171         if (strncmp($prefix, $class, $len) !== 0) {
172             // No, move to the next prefix.
173             return false;
174         }
175         $path = $CFG->dirroot . '/' . $path;
177         // Get the relative class name.
178         $relativeclass = substr($class, $len);
180         // Replace the namespace prefix with the base directory, replace namespace
181         // separators with directory separators in the relative class name, append
182         // with .php.
183         $file = $path . str_replace($separators, '/', $relativeclass) . '.php';
185         return $file;
186     }
189     /**
190      * Initialise caches, always call before accessing self:: caches.
191      */
192     protected static function init() {
193         global $CFG;
195         // Init only once per request/CLI execution, we ignore changes done afterwards.
196         if (isset(self::$plugintypes)) {
197             return;
198         }
200         if (defined('IGNORE_COMPONENT_CACHE') and IGNORE_COMPONENT_CACHE) {
201             self::fill_all_caches();
202             return;
203         }
205         if (!empty($CFG->alternative_component_cache)) {
206             // Hack for heavily clustered sites that want to manage component cache invalidation manually.
207             $cachefile = $CFG->alternative_component_cache;
209             if (file_exists($cachefile)) {
210                 if (CACHE_DISABLE_ALL) {
211                     // Verify the cache state only on upgrade pages.
212                     $content = self::get_cache_content();
213                     if (sha1_file($cachefile) !== sha1($content)) {
214                         die('Outdated component cache file defined in $CFG->alternative_component_cache, can not continue');
215                     }
216                     return;
217                 }
218                 $cache = array();
219                 include($cachefile);
220                 self::$plugintypes      = $cache['plugintypes'];
221                 self::$plugins          = $cache['plugins'];
222                 self::$subsystems       = $cache['subsystems'];
223                 self::$parents          = $cache['parents'];
224                 self::$subplugins       = $cache['subplugins'];
225                 self::$classmap         = $cache['classmap'];
226                 self::$classmaprenames  = $cache['classmaprenames'];
227                 self::$filemap          = $cache['filemap'];
228                 return;
229             }
231             if (!is_writable(dirname($cachefile))) {
232                 die('Can not create alternative component cache file defined in $CFG->alternative_component_cache, can not continue');
233             }
235             // Lets try to create the file, it might be in some writable directory or a local cache dir.
237         } else {
238             // Note: $CFG->cachedir MUST be shared by all servers in a cluster,
239             //       use $CFG->alternative_component_cache if you do not like it.
240             $cachefile = "$CFG->cachedir/core_component.php";
241         }
243         if (!CACHE_DISABLE_ALL and !self::is_developer()) {
244             // 1/ Use the cache only outside of install and upgrade.
245             // 2/ Let developers add/remove classes in developer mode.
246             if (is_readable($cachefile)) {
247                 $cache = false;
248                 include($cachefile);
249                 if (!is_array($cache)) {
250                     // Something is very wrong.
251                 } else if (!isset($cache['version'])) {
252                     // Something is very wrong.
253                 } else if ((float) $cache['version'] !== (float) self::fetch_core_version()) {
254                     // Outdated cache. We trigger an error log to track an eventual repetitive failure of float comparison.
255                     error_log('Resetting core_component cache after core upgrade to version ' . self::fetch_core_version());
256                 } else if ($cache['plugintypes']['mod'] !== "$CFG->dirroot/mod") {
257                     // $CFG->dirroot was changed.
258                 } else {
259                     // The cache looks ok, let's use it.
260                     self::$plugintypes      = $cache['plugintypes'];
261                     self::$plugins          = $cache['plugins'];
262                     self::$subsystems       = $cache['subsystems'];
263                     self::$parents          = $cache['parents'];
264                     self::$subplugins       = $cache['subplugins'];
265                     self::$classmap         = $cache['classmap'];
266                     self::$classmaprenames  = $cache['classmaprenames'];
267                     self::$filemap          = $cache['filemap'];
268                     return;
269                 }
270                 // Note: we do not verify $CFG->admin here intentionally,
271                 //       they must visit admin/index.php after any change.
272             }
273         }
275         if (!isset(self::$plugintypes)) {
276             // This needs to be atomic and self-fixing as much as possible.
278             $content = self::get_cache_content();
279             if (file_exists($cachefile)) {
280                 if (sha1_file($cachefile) === sha1($content)) {
281                     return;
282                 }
283                 // Stale cache detected!
284                 unlink($cachefile);
285             }
287             // Permissions might not be setup properly in installers.
288             $dirpermissions = !isset($CFG->directorypermissions) ? 02777 : $CFG->directorypermissions;
289             $filepermissions = !isset($CFG->filepermissions) ? ($dirpermissions & 0666) : $CFG->filepermissions;
291             clearstatcache();
292             $cachedir = dirname($cachefile);
293             if (!is_dir($cachedir)) {
294                 mkdir($cachedir, $dirpermissions, true);
295             }
297             if ($fp = @fopen($cachefile.'.tmp', 'xb')) {
298                 fwrite($fp, $content);
299                 fclose($fp);
300                 @rename($cachefile.'.tmp', $cachefile);
301                 @chmod($cachefile, $filepermissions);
302             }
303             @unlink($cachefile.'.tmp'); // Just in case anything fails (race condition).
304             self::invalidate_opcode_php_cache($cachefile);
305         }
306     }
308     /**
309      * Are we in developer debug mode?
310      *
311      * Note: You need to set "$CFG->debug = (E_ALL | E_STRICT);" in config.php,
312      *       the reason is we need to use this before we setup DB connection or caches for CFG.
313      *
314      * @return bool
315      */
316     protected static function is_developer() {
317         global $CFG;
319         // Note we can not rely on $CFG->debug here because DB is not initialised yet.
320         if (isset($CFG->config_php_settings['debug'])) {
321             $debug = (int)$CFG->config_php_settings['debug'];
322         } else {
323             return false;
324         }
326         if ($debug & E_ALL and $debug & E_STRICT) {
327             return true;
328         }
330         return false;
331     }
333     /**
334      * Create cache file content.
335      *
336      * @private this is intended for $CFG->alternative_component_cache only.
337      *
338      * @return string
339      */
340     public static function get_cache_content() {
341         if (!isset(self::$plugintypes)) {
342             self::fill_all_caches();
343         }
345         $cache = array(
346             'subsystems'        => self::$subsystems,
347             'plugintypes'       => self::$plugintypes,
348             'plugins'           => self::$plugins,
349             'parents'           => self::$parents,
350             'subplugins'        => self::$subplugins,
351             'classmap'          => self::$classmap,
352             'classmaprenames'   => self::$classmaprenames,
353             'filemap'           => self::$filemap,
354             'version'           => self::$version,
355         );
357         return '<?php
358 $cache = '.var_export($cache, true).';
359 ';
360     }
362     /**
363      * Fill all caches.
364      */
365     protected static function fill_all_caches() {
366         self::$subsystems = self::fetch_subsystems();
368         list(self::$plugintypes, self::$parents, self::$subplugins) = self::fetch_plugintypes();
370         self::$plugins = array();
371         foreach (self::$plugintypes as $type => $fulldir) {
372             self::$plugins[$type] = self::fetch_plugins($type, $fulldir);
373         }
375         self::fill_classmap_cache();
376         self::fill_classmap_renames_cache();
377         self::fill_filemap_cache();
378         self::fetch_core_version();
379     }
381     /**
382      * Get the core version.
383      *
384      * In order for this to work properly, opcache should be reset beforehand.
385      *
386      * @return float core version.
387      */
388     protected static function fetch_core_version() {
389         global $CFG;
390         if (self::$version === null) {
391             $version = null; // Prevent IDE complaints.
392             require($CFG->dirroot . '/version.php');
393             self::$version = $version;
394         }
395         return self::$version;
396     }
398     /**
399      * Returns list of core subsystems.
400      * @return array
401      */
402     protected static function fetch_subsystems() {
403         global $CFG;
405         // NOTE: Any additions here must be verified to not collide with existing add-on modules and subplugins!!!
407         $info = array(
408             'access'      => null,
409             'admin'       => $CFG->dirroot.'/'.$CFG->admin,
410             'antivirus'   => $CFG->dirroot . '/lib/antivirus',
411             'auth'        => $CFG->dirroot.'/auth',
412             'availability' => $CFG->dirroot . '/availability',
413             'backup'      => $CFG->dirroot.'/backup/util/ui',
414             'badges'      => $CFG->dirroot.'/badges',
415             'block'       => $CFG->dirroot.'/blocks',
416             'blog'        => $CFG->dirroot.'/blog',
417             'bulkusers'   => null,
418             'cache'       => $CFG->dirroot.'/cache',
419             'calendar'    => $CFG->dirroot.'/calendar',
420             'cohort'      => $CFG->dirroot.'/cohort',
421             'comment'     => $CFG->dirroot.'/comment',
422             'competency'  => $CFG->dirroot.'/competency',
423             'completion'  => $CFG->dirroot.'/completion',
424             'countries'   => null,
425             'course'      => $CFG->dirroot.'/course',
426             'currencies'  => null,
427             'dbtransfer'  => null,
428             'debug'       => null,
429             'editor'      => $CFG->dirroot.'/lib/editor',
430             'edufields'   => null,
431             'enrol'       => $CFG->dirroot.'/enrol',
432             'error'       => null,
433             'filepicker'  => null,
434             'files'       => $CFG->dirroot.'/files',
435             'filters'     => null,
436             //'fonts'       => null, // Bogus.
437             'form'        => $CFG->dirroot.'/lib/form',
438             'grades'      => $CFG->dirroot.'/grade',
439             'grading'     => $CFG->dirroot.'/grade/grading',
440             'group'       => $CFG->dirroot.'/group',
441             'help'        => null,
442             'hub'         => null,
443             'imscc'       => null,
444             'install'     => null,
445             'iso6392'     => null,
446             'langconfig'  => null,
447             'license'     => null,
448             'mathslib'    => null,
449             'media'       => null,
450             'message'     => $CFG->dirroot.'/message',
451             'mimetypes'   => null,
452             'mnet'        => $CFG->dirroot.'/mnet',
453             //'moodle.org'  => null, // Not used any more.
454             'my'          => $CFG->dirroot.'/my',
455             'notes'       => $CFG->dirroot.'/notes',
456             'pagetype'    => null,
457             'pix'         => null,
458             'plagiarism'  => $CFG->dirroot.'/plagiarism',
459             'plugin'      => null,
460             'portfolio'   => $CFG->dirroot.'/portfolio',
461             'publish'     => $CFG->dirroot.'/course/publish',
462             'question'    => $CFG->dirroot.'/question',
463             'rating'      => $CFG->dirroot.'/rating',
464             'register'    => $CFG->dirroot.'/'.$CFG->admin.'/registration', // Broken badly if $CFG->admin changed.
465             'repository'  => $CFG->dirroot.'/repository',
466             'rss'         => $CFG->dirroot.'/rss',
467             'role'        => $CFG->dirroot.'/'.$CFG->admin.'/roles',
468             'search'      => $CFG->dirroot.'/search',
469             'table'       => null,
470             'tag'         => $CFG->dirroot.'/tag',
471             'timezones'   => null,
472             'user'        => $CFG->dirroot.'/user',
473             'userkey'     => null,
474             'webservice'  => $CFG->dirroot.'/webservice',
475         );
477         return $info;
478     }
480     /**
481      * Returns list of known plugin types.
482      * @return array
483      */
484     protected static function fetch_plugintypes() {
485         global $CFG;
487         $types = array(
488             'antivirus'     => $CFG->dirroot . '/lib/antivirus',
489             'availability'  => $CFG->dirroot . '/availability/condition',
490             'qtype'         => $CFG->dirroot.'/question/type',
491             'mod'           => $CFG->dirroot.'/mod',
492             'auth'          => $CFG->dirroot.'/auth',
493             'calendartype'  => $CFG->dirroot.'/calendar/type',
494             'enrol'         => $CFG->dirroot.'/enrol',
495             'message'       => $CFG->dirroot.'/message/output',
496             'block'         => $CFG->dirroot.'/blocks',
497             'filter'        => $CFG->dirroot.'/filter',
498             'editor'        => $CFG->dirroot.'/lib/editor',
499             'format'        => $CFG->dirroot.'/course/format',
500             'dataformat'    => $CFG->dirroot.'/dataformat',
501             'profilefield'  => $CFG->dirroot.'/user/profile/field',
502             'report'        => $CFG->dirroot.'/report',
503             'coursereport'  => $CFG->dirroot.'/course/report', // Must be after system reports.
504             'gradeexport'   => $CFG->dirroot.'/grade/export',
505             'gradeimport'   => $CFG->dirroot.'/grade/import',
506             'gradereport'   => $CFG->dirroot.'/grade/report',
507             'gradingform'   => $CFG->dirroot.'/grade/grading/form',
508             'mnetservice'   => $CFG->dirroot.'/mnet/service',
509             'webservice'    => $CFG->dirroot.'/webservice',
510             'repository'    => $CFG->dirroot.'/repository',
511             'portfolio'     => $CFG->dirroot.'/portfolio',
512             'search'        => $CFG->dirroot.'/search/engine',
513             'qbehaviour'    => $CFG->dirroot.'/question/behaviour',
514             'qformat'       => $CFG->dirroot.'/question/format',
515             'plagiarism'    => $CFG->dirroot.'/plagiarism',
516             'tool'          => $CFG->dirroot.'/'.$CFG->admin.'/tool',
517             'cachestore'    => $CFG->dirroot.'/cache/stores',
518             'cachelock'     => $CFG->dirroot.'/cache/locks',
519         );
520         $parents = array();
521         $subplugins = array();
523         if (!empty($CFG->themedir) and is_dir($CFG->themedir) ) {
524             $types['theme'] = $CFG->themedir;
525         } else {
526             $types['theme'] = $CFG->dirroot.'/theme';
527         }
529         foreach (self::$supportsubplugins as $type) {
530             if ($type === 'local') {
531                 // Local subplugins must be after local plugins.
532                 continue;
533             }
534             $plugins = self::fetch_plugins($type, $types[$type]);
535             foreach ($plugins as $plugin => $fulldir) {
536                 $subtypes = self::fetch_subtypes($fulldir);
537                 if (!$subtypes) {
538                     continue;
539                 }
540                 $subplugins[$type.'_'.$plugin] = array();
541                 foreach($subtypes as $subtype => $subdir) {
542                     if (isset($types[$subtype])) {
543                         error_log("Invalid subtype '$subtype', duplicate detected.");
544                         continue;
545                     }
546                     $types[$subtype] = $subdir;
547                     $parents[$subtype] = $type.'_'.$plugin;
548                     $subplugins[$type.'_'.$plugin][$subtype] = array_keys(self::fetch_plugins($subtype, $subdir));
549                 }
550             }
551         }
552         // Local is always last!
553         $types['local'] = $CFG->dirroot.'/local';
555         if (in_array('local', self::$supportsubplugins)) {
556             $type = 'local';
557             $plugins = self::fetch_plugins($type, $types[$type]);
558             foreach ($plugins as $plugin => $fulldir) {
559                 $subtypes = self::fetch_subtypes($fulldir);
560                 if (!$subtypes) {
561                     continue;
562                 }
563                 $subplugins[$type.'_'.$plugin] = array();
564                 foreach($subtypes as $subtype => $subdir) {
565                     if (isset($types[$subtype])) {
566                         error_log("Invalid subtype '$subtype', duplicate detected.");
567                         continue;
568                     }
569                     $types[$subtype] = $subdir;
570                     $parents[$subtype] = $type.'_'.$plugin;
571                     $subplugins[$type.'_'.$plugin][$subtype] = array_keys(self::fetch_plugins($subtype, $subdir));
572                 }
573             }
574         }
576         return array($types, $parents, $subplugins);
577     }
579     /**
580      * Returns list of subtypes.
581      * @param string $ownerdir
582      * @return array
583      */
584     protected static function fetch_subtypes($ownerdir) {
585         global $CFG;
587         $types = array();
588         if (file_exists("$ownerdir/db/subplugins.php")) {
589             $subplugins = array();
590             include("$ownerdir/db/subplugins.php");
591             foreach ($subplugins as $subtype => $dir) {
592                 if (!preg_match('/^[a-z][a-z0-9]*$/', $subtype)) {
593                     error_log("Invalid subtype '$subtype'' detected in '$ownerdir', invalid characters present.");
594                     continue;
595                 }
596                 if (isset(self::$subsystems[$subtype])) {
597                     error_log("Invalid subtype '$subtype'' detected in '$ownerdir', duplicates core subsystem.");
598                     continue;
599                 }
600                 if ($CFG->admin !== 'admin' and strpos($dir, 'admin/') === 0) {
601                     $dir = preg_replace('|^admin/|', "$CFG->admin/", $dir);
602                 }
603                 if (!is_dir("$CFG->dirroot/$dir")) {
604                     error_log("Invalid subtype directory '$dir' detected in '$ownerdir'.");
605                     continue;
606                 }
607                 $types[$subtype] = "$CFG->dirroot/$dir";
608             }
609         }
610         return $types;
611     }
613     /**
614      * Returns list of plugins of given type in given directory.
615      * @param string $plugintype
616      * @param string $fulldir
617      * @return array
618      */
619     protected static function fetch_plugins($plugintype, $fulldir) {
620         global $CFG;
622         $fulldirs = (array)$fulldir;
623         if ($plugintype === 'theme') {
624             if (realpath($fulldir) !== realpath($CFG->dirroot.'/theme')) {
625                 // Include themes in standard location too.
626                 array_unshift($fulldirs, $CFG->dirroot.'/theme');
627             }
628         }
630         $result = array();
632         foreach ($fulldirs as $fulldir) {
633             if (!is_dir($fulldir)) {
634                 continue;
635             }
636             $items = new \DirectoryIterator($fulldir);
637             foreach ($items as $item) {
638                 if ($item->isDot() or !$item->isDir()) {
639                     continue;
640                 }
641                 $pluginname = $item->getFilename();
642                 if ($plugintype === 'auth' and $pluginname === 'db') {
643                     // Special exception for this wrong plugin name.
644                 } else if (isset(self::$ignoreddirs[$pluginname])) {
645                     continue;
646                 }
647                 if (!self::is_valid_plugin_name($plugintype, $pluginname)) {
648                     // Always ignore plugins with problematic names here.
649                     continue;
650                 }
651                 $result[$pluginname] = $fulldir.'/'.$pluginname;
652                 unset($item);
653             }
654             unset($items);
655         }
657         ksort($result);
658         return $result;
659     }
661     /**
662      * Find all classes that can be autoloaded including frankenstyle namespaces.
663      */
664     protected static function fill_classmap_cache() {
665         global $CFG;
667         self::$classmap = array();
669         self::load_classes('core', "$CFG->dirroot/lib/classes");
671         foreach (self::$subsystems as $subsystem => $fulldir) {
672             if (!$fulldir) {
673                 continue;
674             }
675             self::load_classes('core_'.$subsystem, "$fulldir/classes");
676         }
678         foreach (self::$plugins as $plugintype => $plugins) {
679             foreach ($plugins as $pluginname => $fulldir) {
680                 self::load_classes($plugintype.'_'.$pluginname, "$fulldir/classes");
681             }
682         }
683         ksort(self::$classmap);
684     }
686     /**
687      * Fills up the cache defining what plugins have certain files.
688      *
689      * @see self::get_plugin_list_with_file
690      * @return void
691      */
692     protected static function fill_filemap_cache() {
693         global $CFG;
695         self::$filemap = array();
697         foreach (self::$filestomap as $file) {
698             if (!isset(self::$filemap[$file])) {
699                 self::$filemap[$file] = array();
700             }
701             foreach (self::$plugins as $plugintype => $plugins) {
702                 if (!isset(self::$filemap[$file][$plugintype])) {
703                     self::$filemap[$file][$plugintype] = array();
704                 }
705                 foreach ($plugins as $pluginname => $fulldir) {
706                     if (file_exists("$fulldir/$file")) {
707                         self::$filemap[$file][$plugintype][$pluginname] = "$fulldir/$file";
708                     }
709                 }
710             }
711         }
712     }
714     /**
715      * Find classes in directory and recurse to subdirs.
716      * @param string $component
717      * @param string $fulldir
718      * @param string $namespace
719      */
720     protected static function load_classes($component, $fulldir, $namespace = '') {
721         if (!is_dir($fulldir)) {
722             return;
723         }
725         if (!is_readable($fulldir)) {
726             // TODO: MDL-51711 We should generate some diagnostic debugging information in this case
727             // because its pretty likely to lead to a missing class error further down the line.
728             // But our early setup code can't handle errors this early at the moment.
729             return;
730         }
732         $items = new \DirectoryIterator($fulldir);
733         foreach ($items as $item) {
734             if ($item->isDot()) {
735                 continue;
736             }
737             if ($item->isDir()) {
738                 $dirname = $item->getFilename();
739                 self::load_classes($component, "$fulldir/$dirname", $namespace.'\\'.$dirname);
740                 continue;
741             }
743             $filename = $item->getFilename();
744             $classname = preg_replace('/\.php$/', '', $filename);
746             if ($filename === $classname) {
747                 // Not a php file.
748                 continue;
749             }
750             if ($namespace === '') {
751                 // Legacy long frankenstyle class name.
752                 self::$classmap[$component.'_'.$classname] = "$fulldir/$filename";
753             }
754             // New namespaced classes.
755             self::$classmap[$component.$namespace.'\\'.$classname] = "$fulldir/$filename";
756         }
757         unset($item);
758         unset($items);
759     }
762     /**
763      * List all core subsystems and their location
764      *
765      * This is a whitelist of components that are part of the core and their
766      * language strings are defined in /lang/en/<<subsystem>>.php. If a given
767      * plugin is not listed here and it does not have proper plugintype prefix,
768      * then it is considered as course activity module.
769      *
770      * The location is absolute file path to dir. NULL means there is no special
771      * directory for this subsystem. If the location is set, the subsystem's
772      * renderer.php is expected to be there.
773      *
774      * @return array of (string)name => (string|null)full dir location
775      */
776     public static function get_core_subsystems() {
777         self::init();
778         return self::$subsystems;
779     }
781     /**
782      * Get list of available plugin types together with their location.
783      *
784      * @return array as (string)plugintype => (string)fulldir
785      */
786     public static function get_plugin_types() {
787         self::init();
788         return self::$plugintypes;
789     }
791     /**
792      * Get list of plugins of given type.
793      *
794      * @param string $plugintype
795      * @return array as (string)pluginname => (string)fulldir
796      */
797     public static function get_plugin_list($plugintype) {
798         self::init();
800         if (!isset(self::$plugins[$plugintype])) {
801             return array();
802         }
803         return self::$plugins[$plugintype];
804     }
806     /**
807      * Get a list of all the plugins of a given type that define a certain class
808      * in a certain file. The plugin component names and class names are returned.
809      *
810      * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
811      * @param string $class the part of the name of the class after the
812      *      frankenstyle prefix. e.g 'thing' if you are looking for classes with
813      *      names like report_courselist_thing. If you are looking for classes with
814      *      the same name as the plugin name (e.g. qtype_multichoice) then pass ''.
815      *      Frankenstyle namespaces are also supported.
816      * @param string $file the name of file within the plugin that defines the class.
817      * @return array with frankenstyle plugin names as keys (e.g. 'report_courselist', 'mod_forum')
818      *      and the class names as values (e.g. 'report_courselist_thing', 'qtype_multichoice').
819      */
820     public static function get_plugin_list_with_class($plugintype, $class, $file = null) {
821         global $CFG; // Necessary in case it is referenced by included PHP scripts.
823         if ($class) {
824             $suffix = '_' . $class;
825         } else {
826             $suffix = '';
827         }
829         $pluginclasses = array();
830         $plugins = self::get_plugin_list($plugintype);
831         foreach ($plugins as $plugin => $fulldir) {
832             // Try class in frankenstyle namespace.
833             if ($class) {
834                 $classname = '\\' . $plugintype . '_' . $plugin . '\\' . $class;
835                 if (class_exists($classname, true)) {
836                     $pluginclasses[$plugintype . '_' . $plugin] = $classname;
837                     continue;
838                 }
839             }
841             // Try autoloading of class with frankenstyle prefix.
842             $classname = $plugintype . '_' . $plugin . $suffix;
843             if (class_exists($classname, true)) {
844                 $pluginclasses[$plugintype . '_' . $plugin] = $classname;
845                 continue;
846             }
848             // Fall back to old file location and class name.
849             if ($file and file_exists("$fulldir/$file")) {
850                 include_once("$fulldir/$file");
851                 if (class_exists($classname, false)) {
852                     $pluginclasses[$plugintype . '_' . $plugin] = $classname;
853                     continue;
854                 }
855             }
856         }
858         return $pluginclasses;
859     }
861     /**
862      * Get a list of all the plugins of a given type that contain a particular file.
863      *
864      * @param string $plugintype the type of plugin, e.g. 'mod' or 'report'.
865      * @param string $file the name of file that must be present in the plugin.
866      *                     (e.g. 'view.php', 'db/install.xml').
867      * @param bool $include if true (default false), the file will be include_once-ed if found.
868      * @return array with plugin name as keys (e.g. 'forum', 'courselist') and the path
869      *               to the file relative to dirroot as value (e.g. "$CFG->dirroot/mod/forum/view.php").
870      */
871     public static function get_plugin_list_with_file($plugintype, $file, $include = false) {
872         global $CFG; // Necessary in case it is referenced by included PHP scripts.
873         $pluginfiles = array();
875         if (isset(self::$filemap[$file])) {
876             // If the file was supposed to be mapped, then it should have been set in the array.
877             if (isset(self::$filemap[$file][$plugintype])) {
878                 $pluginfiles = self::$filemap[$file][$plugintype];
879             }
880         } else {
881             // Old-style search for non-cached files.
882             $plugins = self::get_plugin_list($plugintype);
883             foreach ($plugins as $plugin => $fulldir) {
884                 $path = $fulldir . '/' . $file;
885                 if (file_exists($path)) {
886                     $pluginfiles[$plugin] = $path;
887                 }
888             }
889         }
891         if ($include) {
892             foreach ($pluginfiles as $path) {
893                 include_once($path);
894             }
895         }
897         return $pluginfiles;
898     }
900     /**
901      * Returns all classes in a component matching the provided namespace.
902      *
903      * It checks that the class exists.
904      *
905      * e.g. get_component_classes_in_namespace('mod_forum', 'event')
906      *
907      * @param string $component A valid moodle component (frankenstyle)
908      * @param string $namespace Namespace from the component name or empty if all $component namespace classes.
909      * @return array The full class name as key and the class path as value.
910      */
911     public static function get_component_classes_in_namespace($component, $namespace = '') {
913         $component = self::normalize_componentname($component);
915         if ($namespace) {
917             // We will add them later.
918             $namespace = trim($namespace, '\\');
920             // We need add double backslashes as it is how classes are stored into self::$classmap.
921             $namespace = implode('\\\\', explode('\\', $namespace));
922             $namespace = $namespace . '\\\\';
923         }
925         $regex = '|^' . $component . '\\\\' . $namespace . '|';
926         $it = new RegexIterator(new ArrayIterator(self::$classmap), $regex, RegexIterator::GET_MATCH, RegexIterator::USE_KEY);
928         // We want to be sure that they exist.
929         $classes = array();
930         foreach ($it as $classname => $classpath) {
931             if (class_exists($classname)) {
932                 $classes[$classname] = $classpath;
933             }
934         }
936         return $classes;
937     }
939     /**
940      * Returns the exact absolute path to plugin directory.
941      *
942      * @param string $plugintype type of plugin
943      * @param string $pluginname name of the plugin
944      * @return string full path to plugin directory; null if not found
945      */
946     public static function get_plugin_directory($plugintype, $pluginname) {
947         if (empty($pluginname)) {
948             // Invalid plugin name, sorry.
949             return null;
950         }
952         self::init();
954         if (!isset(self::$plugins[$plugintype][$pluginname])) {
955             return null;
956         }
957         return self::$plugins[$plugintype][$pluginname];
958     }
960     /**
961      * Returns the exact absolute path to plugin directory.
962      *
963      * @param string $subsystem type of core subsystem
964      * @return string full path to subsystem directory; null if not found
965      */
966     public static function get_subsystem_directory($subsystem) {
967         self::init();
969         if (!isset(self::$subsystems[$subsystem])) {
970             return null;
971         }
972         return self::$subsystems[$subsystem];
973     }
975     /**
976      * This method validates a plug name. It is much faster than calling clean_param.
977      *
978      * @param string $plugintype type of plugin
979      * @param string $pluginname a string that might be a plugin name.
980      * @return bool if this string is a valid plugin name.
981      */
982     public static function is_valid_plugin_name($plugintype, $pluginname) {
983         if ($plugintype === 'mod') {
984             // Modules must not have the same name as core subsystems.
985             if (!isset(self::$subsystems)) {
986                 // Watch out, this is called from init!
987                 self::init();
988             }
989             if (isset(self::$subsystems[$pluginname])) {
990                 return false;
991             }
992             // Modules MUST NOT have any underscores,
993             // component normalisation would break very badly otherwise!
994             return (bool)preg_match('/^[a-z][a-z0-9]*$/', $pluginname);
996         } else {
997             return (bool)preg_match('/^[a-z](?:[a-z0-9_](?!__))*[a-z0-9]+$/', $pluginname);
998         }
999     }
1001     /**
1002      * Normalize the component name.
1003      *
1004      * Note: this does not verify the validity of the plugin or component.
1005      *
1006      * @param string $component
1007      * @return string
1008      */
1009     public static function normalize_componentname($componentname) {
1010         list($plugintype, $pluginname) = self::normalize_component($componentname);
1011         if ($plugintype === 'core' && is_null($pluginname)) {
1012             return $plugintype;
1013         }
1014         return $plugintype . '_' . $pluginname;
1015     }
1017     /**
1018      * Normalize the component name using the "frankenstyle" rules.
1019      *
1020      * Note: this does not verify the validity of plugin or type names.
1021      *
1022      * @param string $component
1023      * @return array as (string)$type => (string)$plugin
1024      */
1025     public static function normalize_component($component) {
1026         if ($component === 'moodle' or $component === 'core' or $component === '') {
1027             return array('core', null);
1028         }
1030         if (strpos($component, '_') === false) {
1031             self::init();
1032             if (array_key_exists($component, self::$subsystems)) {
1033                 $type   = 'core';
1034                 $plugin = $component;
1035             } else {
1036                 // Everything else without underscore is a module.
1037                 $type   = 'mod';
1038                 $plugin = $component;
1039             }
1041         } else {
1042             list($type, $plugin) = explode('_', $component, 2);
1043             if ($type === 'moodle') {
1044                 $type = 'core';
1045             }
1046             // Any unknown type must be a subplugin.
1047         }
1049         return array($type, $plugin);
1050     }
1052     /**
1053      * Return exact absolute path to a plugin directory.
1054      *
1055      * @param string $component name such as 'moodle', 'mod_forum'
1056      * @return string full path to component directory; NULL if not found
1057      */
1058     public static function get_component_directory($component) {
1059         global $CFG;
1061         list($type, $plugin) = self::normalize_component($component);
1063         if ($type === 'core') {
1064             if ($plugin === null) {
1065                 return $path = $CFG->libdir;
1066             }
1067             return self::get_subsystem_directory($plugin);
1068         }
1070         return self::get_plugin_directory($type, $plugin);
1071     }
1073     /**
1074      * Returns list of plugin types that allow subplugins.
1075      * @return array as (string)plugintype => (string)fulldir
1076      */
1077     public static function get_plugin_types_with_subplugins() {
1078         self::init();
1080         $return = array();
1081         foreach (self::$supportsubplugins as $type) {
1082             $return[$type] = self::$plugintypes[$type];
1083         }
1084         return $return;
1085     }
1087     /**
1088      * Returns parent of this subplugin type.
1089      *
1090      * @param string $type
1091      * @return string parent component or null
1092      */
1093     public static function get_subtype_parent($type) {
1094         self::init();
1096         if (isset(self::$parents[$type])) {
1097             return self::$parents[$type];
1098         }
1100         return null;
1101     }
1103     /**
1104      * Return all subplugins of this component.
1105      * @param string $component.
1106      * @return array $subtype=>array($component, ..), null if no subtypes defined
1107      */
1108     public static function get_subplugins($component) {
1109         self::init();
1111         if (isset(self::$subplugins[$component])) {
1112             return self::$subplugins[$component];
1113         }
1115         return null;
1116     }
1118     /**
1119      * Returns hash of all versions including core and all plugins.
1120      *
1121      * This is relatively slow and not fully cached, use with care!
1122      *
1123      * @return string sha1 hash
1124      */
1125     public static function get_all_versions_hash() {
1126         global $CFG;
1128         self::init();
1130         $versions = array();
1132         // Main version first.
1133         $versions['core'] = self::fetch_core_version();
1135         // The problem here is tha the component cache might be stable,
1136         // we want this to work also on frontpage without resetting the component cache.
1137         $usecache = false;
1138         if (CACHE_DISABLE_ALL or (defined('IGNORE_COMPONENT_CACHE') and IGNORE_COMPONENT_CACHE)) {
1139             $usecache = true;
1140         }
1142         // Now all plugins.
1143         $plugintypes = core_component::get_plugin_types();
1144         foreach ($plugintypes as $type => $typedir) {
1145             if ($usecache) {
1146                 $plugs = core_component::get_plugin_list($type);
1147             } else {
1148                 $plugs = self::fetch_plugins($type, $typedir);
1149             }
1150             foreach ($plugs as $plug => $fullplug) {
1151                 $plugin = new stdClass();
1152                 $plugin->version = null;
1153                 $module = $plugin;
1154                 include($fullplug.'/version.php');
1155                 $versions[$type.'_'.$plug] = $plugin->version;
1156             }
1157         }
1159         return sha1(serialize($versions));
1160     }
1162     /**
1163      * Invalidate opcode cache for given file, this is intended for
1164      * php files that are stored in dataroot.
1165      *
1166      * Note: we need it here because this class must be self-contained.
1167      *
1168      * @param string $file
1169      */
1170     public static function invalidate_opcode_php_cache($file) {
1171         if (function_exists('opcache_invalidate')) {
1172             if (!file_exists($file)) {
1173                 return;
1174             }
1175             opcache_invalidate($file, true);
1176         }
1177     }
1179     /**
1180      * Return true if subsystemname is core subsystem.
1181      *
1182      * @param string $subsystemname name of the subsystem.
1183      * @return bool true if core subsystem.
1184      */
1185     public static function is_core_subsystem($subsystemname) {
1186         return isset(self::$subsystems[$subsystemname]);
1187     }
1189     /**
1190      * Records all class renames that have been made to facilitate autoloading.
1191      */
1192     protected static function fill_classmap_renames_cache() {
1193         global $CFG;
1195         self::$classmaprenames = array();
1197         self::load_renamed_classes("$CFG->dirroot/lib/");
1199         foreach (self::$subsystems as $subsystem => $fulldir) {
1200             self::load_renamed_classes($fulldir);
1201         }
1203         foreach (self::$plugins as $plugintype => $plugins) {
1204             foreach ($plugins as $pluginname => $fulldir) {
1205                 self::load_renamed_classes($fulldir);
1206             }
1207         }
1208     }
1210     /**
1211      * Loads the db/renamedclasses.php file from the given directory.
1212      *
1213      * The renamedclasses.php should contain a key => value array ($renamedclasses) where the key is old class name,
1214      * and the value is the new class name.
1215      * It is only included when we are populating the component cache. After that is not needed.
1216      *
1217      * @param string $fulldir
1218      */
1219     protected static function load_renamed_classes($fulldir) {
1220         $file = $fulldir . '/db/renamedclasses.php';
1221         if (is_readable($file)) {
1222             $renamedclasses = null;
1223             require($file);
1224             if (is_array($renamedclasses)) {
1225                 foreach ($renamedclasses as $oldclass => $newclass) {
1226                     self::$classmaprenames[(string)$oldclass] = (string)$newclass;
1227                 }
1228             }
1229         }
1230     }