source: moodle/trunk/fuentes/lib/classes/component.php @ 1331

Last change on this file since 1331 was 1331, checked in by jrpelegrina, 3 years ago

Updated to moodle 3.0.3

File size: 43.7 KB
Line 
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/>.
16
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 */
24
25defined('MOODLE_INTERNAL') || die();
26
27// Constants used in version.php files, these must exist when core_component executes.
28
29/** Software maturity level - internals can be tested using white box techniques. */
30define('MATURITY_ALPHA',    50);
31/** Software maturity level - feature complete, ready for preview and testing. */
32define('MATURITY_BETA',     100);
33/** Software maturity level - tested, will be released unless there are fatal bugs. */
34define('MATURITY_RC',       150);
35/** Software maturity level - ready for production deployment. */
36define('MATURITY_STABLE',   200);
37/** Any version - special value that can be used in $plugin->dependencies in version.php files. */
38define('ANY_VERSION', 'any');
39
40
41/**
42 * Collection of components related methods.
43 */
44class 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');
49
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 cache of PSR loadable systems */
71    protected static $psrclassmap = null;
72
73    /**
74     * Class loader for Frankenstyle named classes in standard locations.
75     * Frankenstyle namespaces are supported.
76     *
77     * The expected location for core classes is:
78     *    1/ core_xx_yy_zz ---> lib/classes/xx_yy_zz.php
79     *    2/ \core\xx_yy_zz ---> lib/classes/xx_yy_zz.php
80     *    3/ \core\xx\yy_zz ---> lib/classes/xx/yy_zz.php
81     *
82     * The expected location for plugin classes is:
83     *    1/ mod_name_xx_yy_zz ---> mod/name/classes/xx_yy_zz.php
84     *    2/ \mod_name\xx_yy_zz ---> mod/name/classes/xx_yy_zz.php
85     *    3/ \mod_name\xx\yy_zz ---> mod/name/classes/xx/yy_zz.php
86     *
87     * @param string $classname
88     */
89    public static function classloader($classname) {
90        self::init();
91
92        if (isset(self::$classmap[$classname])) {
93            // Global $CFG is expected in included scripts.
94            global $CFG;
95            // Function include would be faster, but for BC it is better to include only once.
96            include_once(self::$classmap[$classname]);
97            return;
98        }
99        if (isset(self::$classmaprenames[$classname]) && isset(self::$classmap[self::$classmaprenames[$classname]])) {
100            $newclassname = self::$classmaprenames[$classname];
101            $debugging = "Class '%s' has been renamed for the autoloader and is now deprecated. Please use '%s' instead.";
102            debugging(sprintf($debugging, $classname, $newclassname), DEBUG_DEVELOPER);
103            if (PHP_VERSION_ID >= 70000 && preg_match('#\\\null(\\\|$)#', $classname)) {
104                throw new \coding_exception("Cannot alias $classname to $newclassname");
105            }
106            class_alias($newclassname, $classname);
107            return;
108        }
109
110        // Attempt to normalize the classname.
111        $normalizedclassname = str_replace(array('/', '\\'), '_', $classname);
112        if (isset(self::$psrclassmap[$normalizedclassname])) {
113            // Function include would be faster, but for BC it is better to include only once.
114            include_once(self::$psrclassmap[$normalizedclassname]);
115            return;
116        }
117    }
118
119    /**
120     * Initialise caches, always call before accessing self:: caches.
121     */
122    protected static function init() {
123        global $CFG;
124
125        // Init only once per request/CLI execution, we ignore changes done afterwards.
126        if (isset(self::$plugintypes)) {
127            return;
128        }
129
130        if (defined('IGNORE_COMPONENT_CACHE') and IGNORE_COMPONENT_CACHE) {
131            self::fill_all_caches();
132            return;
133        }
134
135        if (!empty($CFG->alternative_component_cache)) {
136            // Hack for heavily clustered sites that want to manage component cache invalidation manually.
137            $cachefile = $CFG->alternative_component_cache;
138
139            if (file_exists($cachefile)) {
140                if (CACHE_DISABLE_ALL) {
141                    // Verify the cache state only on upgrade pages.
142                    $content = self::get_cache_content();
143                    if (sha1_file($cachefile) !== sha1($content)) {
144                        die('Outdated component cache file defined in $CFG->alternative_component_cache, can not continue');
145                    }
146                    return;
147                }
148                $cache = array();
149                include($cachefile);
150                self::$plugintypes      = $cache['plugintypes'];
151                self::$plugins          = $cache['plugins'];
152                self::$subsystems       = $cache['subsystems'];
153                self::$parents          = $cache['parents'];
154                self::$subplugins       = $cache['subplugins'];
155                self::$classmap         = $cache['classmap'];
156                self::$classmaprenames  = $cache['classmaprenames'];
157                self::$filemap          = $cache['filemap'];
158                self::$psrclassmap      = $cache['psrclassmap'];
159                return;
160            }
161
162            if (!is_writable(dirname($cachefile))) {
163                die('Can not create alternative component cache file defined in $CFG->alternative_component_cache, can not continue');
164            }
165
166            // Lets try to create the file, it might be in some writable directory or a local cache dir.
167
168        } else {
169            // Note: $CFG->cachedir MUST be shared by all servers in a cluster,
170            //       use $CFG->alternative_component_cache if you do not like it.
171            $cachefile = "$CFG->cachedir/core_component.php";
172        }
173
174        if (!CACHE_DISABLE_ALL and !self::is_developer()) {
175            // 1/ Use the cache only outside of install and upgrade.
176            // 2/ Let developers add/remove classes in developer mode.
177            if (is_readable($cachefile)) {
178                $cache = false;
179                include($cachefile);
180                if (!is_array($cache)) {
181                    // Something is very wrong.
182                } else if (!isset($cache['version'])) {
183                    // Something is very wrong.
184                } else if ((float) $cache['version'] !== (float) self::fetch_core_version()) {
185                    // Outdated cache. We trigger an error log to track an eventual repetitive failure of float comparison.
186                    error_log('Resetting core_component cache after core upgrade to version ' . self::fetch_core_version());
187                } else if ($cache['plugintypes']['mod'] !== "$CFG->dirroot/mod") {
188                    // $CFG->dirroot was changed.
189                } else {
190                    // The cache looks ok, let's use it.
191                    self::$plugintypes      = $cache['plugintypes'];
192                    self::$plugins          = $cache['plugins'];
193                    self::$subsystems       = $cache['subsystems'];
194                    self::$parents          = $cache['parents'];
195                    self::$subplugins       = $cache['subplugins'];
196                    self::$classmap         = $cache['classmap'];
197                    self::$classmaprenames  = $cache['classmaprenames'];
198                    self::$filemap          = $cache['filemap'];
199                    self::$psrclassmap      = $cache['psrclassmap'];
200                    return;
201                }
202                // Note: we do not verify $CFG->admin here intentionally,
203                //       they must visit admin/index.php after any change.
204            }
205        }
206
207        if (!isset(self::$plugintypes)) {
208            // This needs to be atomic and self-fixing as much as possible.
209
210            $content = self::get_cache_content();
211            if (file_exists($cachefile)) {
212                if (sha1_file($cachefile) === sha1($content)) {
213                    return;
214                }
215                // Stale cache detected!
216                unlink($cachefile);
217            }
218
219            // Permissions might not be setup properly in installers.
220            $dirpermissions = !isset($CFG->directorypermissions) ? 02777 : $CFG->directorypermissions;
221            $filepermissions = !isset($CFG->filepermissions) ? ($dirpermissions & 0666) : $CFG->filepermissions;
222
223            clearstatcache();
224            $cachedir = dirname($cachefile);
225            if (!is_dir($cachedir)) {
226                mkdir($cachedir, $dirpermissions, true);
227            }
228
229            if ($fp = @fopen($cachefile.'.tmp', 'xb')) {
230                fwrite($fp, $content);
231                fclose($fp);
232                @rename($cachefile.'.tmp', $cachefile);
233                @chmod($cachefile, $filepermissions);
234            }
235            @unlink($cachefile.'.tmp'); // Just in case anything fails (race condition).
236            self::invalidate_opcode_php_cache($cachefile);
237        }
238    }
239
240    /**
241     * Are we in developer debug mode?
242     *
243     * Note: You need to set "$CFG->debug = (E_ALL | E_STRICT);" in config.php,
244     *       the reason is we need to use this before we setup DB connection or caches for CFG.
245     *
246     * @return bool
247     */
248    protected static function is_developer() {
249        global $CFG;
250
251        // Note we can not rely on $CFG->debug here because DB is not initialised yet.
252        if (isset($CFG->config_php_settings['debug'])) {
253            $debug = (int)$CFG->config_php_settings['debug'];
254        } else {
255            return false;
256        }
257
258        if ($debug & E_ALL and $debug & E_STRICT) {
259            return true;
260        }
261
262        return false;
263    }
264
265    /**
266     * Create cache file content.
267     *
268     * @private this is intended for $CFG->alternative_component_cache only.
269     *
270     * @return string
271     */
272    public static function get_cache_content() {
273        if (!isset(self::$plugintypes)) {
274            self::fill_all_caches();
275        }
276
277        $cache = array(
278            'subsystems'        => self::$subsystems,
279            'plugintypes'       => self::$plugintypes,
280            'plugins'           => self::$plugins,
281            'parents'           => self::$parents,
282            'subplugins'        => self::$subplugins,
283            'classmap'          => self::$classmap,
284            'classmaprenames'   => self::$classmaprenames,
285            'filemap'           => self::$filemap,
286            'version'           => self::$version,
287            'psrclassmap'       => self::$psrclassmap,
288        );
289
290        return '<?php
291$cache = '.var_export($cache, true).';
292';
293    }
294
295    /**
296     * Fill all caches.
297     */
298    protected static function fill_all_caches() {
299        self::$subsystems = self::fetch_subsystems();
300
301        list(self::$plugintypes, self::$parents, self::$subplugins) = self::fetch_plugintypes();
302
303        self::$plugins = array();
304        foreach (self::$plugintypes as $type => $fulldir) {
305            self::$plugins[$type] = self::fetch_plugins($type, $fulldir);
306        }
307
308        self::fill_classmap_cache();
309        self::fill_classmap_renames_cache();
310        self::fill_filemap_cache();
311        self::fill_psr_cache();
312        self::fetch_core_version();
313    }
314
315    /**
316     * Get the core version.
317     *
318     * In order for this to work properly, opcache should be reset beforehand.
319     *
320     * @return float core version.
321     */
322    protected static function fetch_core_version() {
323        global $CFG;
324        if (self::$version === null) {
325            $version = null; // Prevent IDE complaints.
326            require($CFG->dirroot . '/version.php');
327            self::$version = $version;
328        }
329        return self::$version;
330    }
331
332    /**
333     * Returns list of core subsystems.
334     * @return array
335     */
336    protected static function fetch_subsystems() {
337        global $CFG;
338
339        // NOTE: Any additions here must be verified to not collide with existing add-on modules and subplugins!!!
340
341        $info = array(
342            'access'      => null,
343            'admin'       => $CFG->dirroot.'/'.$CFG->admin,
344            'auth'        => $CFG->dirroot.'/auth',
345            'availability' => $CFG->dirroot . '/availability',
346            'backup'      => $CFG->dirroot.'/backup/util/ui',
347            'badges'      => $CFG->dirroot.'/badges',
348            'block'       => $CFG->dirroot.'/blocks',
349            'blog'        => $CFG->dirroot.'/blog',
350            'bulkusers'   => null,
351            'cache'       => $CFG->dirroot.'/cache',
352            'calendar'    => $CFG->dirroot.'/calendar',
353            'cohort'      => $CFG->dirroot.'/cohort',
354            'comment'     => $CFG->dirroot.'/comment',
355            'completion'  => $CFG->dirroot.'/completion',
356            'countries'   => null,
357            'course'      => $CFG->dirroot.'/course',
358            'currencies'  => null,
359            'dbtransfer'  => null,
360            'debug'       => null,
361            'editor'      => $CFG->dirroot.'/lib/editor',
362            'edufields'   => null,
363            'enrol'       => $CFG->dirroot.'/enrol',
364            'error'       => null,
365            'filepicker'  => null,
366            'files'       => $CFG->dirroot.'/files',
367            'filters'     => null,
368            //'fonts'       => null, // Bogus.
369            'form'        => $CFG->dirroot.'/lib/form',
370            'grades'      => $CFG->dirroot.'/grade',
371            'grading'     => $CFG->dirroot.'/grade/grading',
372            'group'       => $CFG->dirroot.'/group',
373            'help'        => null,
374            'hub'         => null,
375            'imscc'       => null,
376            'install'     => null,
377            'iso6392'     => null,
378            'langconfig'  => null,
379            'license'     => null,
380            'mathslib'    => null,
381            'media'       => null,
382            'message'     => $CFG->dirroot.'/message',
383            'mimetypes'   => null,
384            'mnet'        => $CFG->dirroot.'/mnet',
385            //'moodle.org'  => null, // Not used any more.
386            'my'          => $CFG->dirroot.'/my',
387            'notes'       => $CFG->dirroot.'/notes',
388            'pagetype'    => null,
389            'pix'         => null,
390            'plagiarism'  => $CFG->dirroot.'/plagiarism',
391            'plugin'      => null,
392            'portfolio'   => $CFG->dirroot.'/portfolio',
393            'publish'     => $CFG->dirroot.'/course/publish',
394            'question'    => $CFG->dirroot.'/question',
395            'rating'      => $CFG->dirroot.'/rating',
396            'register'    => $CFG->dirroot.'/'.$CFG->admin.'/registration', // Broken badly if $CFG->admin changed.
397            'repository'  => $CFG->dirroot.'/repository',
398            'rss'         => $CFG->dirroot.'/rss',
399            'role'        => $CFG->dirroot.'/'.$CFG->admin.'/roles',
400            'search'      => null,
401            'table'       => null,
402            'tag'         => $CFG->dirroot.'/tag',
403            'timezones'   => null,
404            'user'        => $CFG->dirroot.'/user',
405            'userkey'     => null,
406            'webservice'  => $CFG->dirroot.'/webservice',
407        );
408
409        return $info;
410    }
411
412    /**
413     * Returns list of known plugin types.
414     * @return array
415     */
416    protected static function fetch_plugintypes() {
417        global $CFG;
418
419        $types = array(
420            'availability'  => $CFG->dirroot . '/availability/condition',
421            'qtype'         => $CFG->dirroot.'/question/type',
422            'mod'           => $CFG->dirroot.'/mod',
423            'auth'          => $CFG->dirroot.'/auth',
424            'calendartype'  => $CFG->dirroot.'/calendar/type',
425            'enrol'         => $CFG->dirroot.'/enrol',
426            'message'       => $CFG->dirroot.'/message/output',
427            'block'         => $CFG->dirroot.'/blocks',
428            'filter'        => $CFG->dirroot.'/filter',
429            'editor'        => $CFG->dirroot.'/lib/editor',
430            'format'        => $CFG->dirroot.'/course/format',
431            'profilefield'  => $CFG->dirroot.'/user/profile/field',
432            'report'        => $CFG->dirroot.'/report',
433            'coursereport'  => $CFG->dirroot.'/course/report', // Must be after system reports.
434            'gradeexport'   => $CFG->dirroot.'/grade/export',
435            'gradeimport'   => $CFG->dirroot.'/grade/import',
436            'gradereport'   => $CFG->dirroot.'/grade/report',
437            'gradingform'   => $CFG->dirroot.'/grade/grading/form',
438            'mnetservice'   => $CFG->dirroot.'/mnet/service',
439            'webservice'    => $CFG->dirroot.'/webservice',
440            'repository'    => $CFG->dirroot.'/repository',
441            'portfolio'     => $CFG->dirroot.'/portfolio',
442            'qbehaviour'    => $CFG->dirroot.'/question/behaviour',
443            'qformat'       => $CFG->dirroot.'/question/format',
444            'plagiarism'    => $CFG->dirroot.'/plagiarism',
445            'tool'          => $CFG->dirroot.'/'.$CFG->admin.'/tool',
446            'cachestore'    => $CFG->dirroot.'/cache/stores',
447            'cachelock'     => $CFG->dirroot.'/cache/locks',
448        );
449        $parents = array();
450        $subplugins = array();
451
452        if (!empty($CFG->themedir) and is_dir($CFG->themedir) ) {
453            $types['theme'] = $CFG->themedir;
454        } else {
455            $types['theme'] = $CFG->dirroot.'/theme';
456        }
457
458        foreach (self::$supportsubplugins as $type) {
459            if ($type === 'local') {
460                // Local subplugins must be after local plugins.
461                continue;
462            }
463            $plugins = self::fetch_plugins($type, $types[$type]);
464            foreach ($plugins as $plugin => $fulldir) {
465                $subtypes = self::fetch_subtypes($fulldir);
466                if (!$subtypes) {
467                    continue;
468                }
469                $subplugins[$type.'_'.$plugin] = array();
470                foreach($subtypes as $subtype => $subdir) {
471                    if (isset($types[$subtype])) {
472                        error_log("Invalid subtype '$subtype', duplicate detected.");
473                        continue;
474                    }
475                    $types[$subtype] = $subdir;
476                    $parents[$subtype] = $type.'_'.$plugin;
477                    $subplugins[$type.'_'.$plugin][$subtype] = array_keys(self::fetch_plugins($subtype, $subdir));
478                }
479            }
480        }
481        // Local is always last!
482        $types['local'] = $CFG->dirroot.'/local';
483
484        if (in_array('local', self::$supportsubplugins)) {
485            $type = 'local';
486            $plugins = self::fetch_plugins($type, $types[$type]);
487            foreach ($plugins as $plugin => $fulldir) {
488                $subtypes = self::fetch_subtypes($fulldir);
489                if (!$subtypes) {
490                    continue;
491                }
492                $subplugins[$type.'_'.$plugin] = array();
493                foreach($subtypes as $subtype => $subdir) {
494                    if (isset($types[$subtype])) {
495                        error_log("Invalid subtype '$subtype', duplicate detected.");
496                        continue;
497                    }
498                    $types[$subtype] = $subdir;
499                    $parents[$subtype] = $type.'_'.$plugin;
500                    $subplugins[$type.'_'.$plugin][$subtype] = array_keys(self::fetch_plugins($subtype, $subdir));
501                }
502            }
503        }
504
505        return array($types, $parents, $subplugins);
506    }
507
508    /**
509     * Returns list of subtypes.
510     * @param string $ownerdir
511     * @return array
512     */
513    protected static function fetch_subtypes($ownerdir) {
514        global $CFG;
515
516        $types = array();
517        if (file_exists("$ownerdir/db/subplugins.php")) {
518            $subplugins = array();
519            include("$ownerdir/db/subplugins.php");
520            foreach ($subplugins as $subtype => $dir) {
521                if (!preg_match('/^[a-z][a-z0-9]*$/', $subtype)) {
522                    error_log("Invalid subtype '$subtype'' detected in '$ownerdir', invalid characters present.");
523                    continue;
524                }
525                if (isset(self::$subsystems[$subtype])) {
526                    error_log("Invalid subtype '$subtype'' detected in '$ownerdir', duplicates core subsystem.");
527                    continue;
528                }
529                if ($CFG->admin !== 'admin' and strpos($dir, 'admin/') === 0) {
530                    $dir = preg_replace('|^admin/|', "$CFG->admin/", $dir);
531                }
532                if (!is_dir("$CFG->dirroot/$dir")) {
533                    error_log("Invalid subtype directory '$dir' detected in '$ownerdir'.");
534                    continue;
535                }
536                $types[$subtype] = "$CFG->dirroot/$dir";
537            }
538        }
539        return $types;
540    }
541
542    /**
543     * Returns list of plugins of given type in given directory.
544     * @param string $plugintype
545     * @param string $fulldir
546     * @return array
547     */
548    protected static function fetch_plugins($plugintype, $fulldir) {
549        global $CFG;
550
551        $fulldirs = (array)$fulldir;
552        if ($plugintype === 'theme') {
553            if (realpath($fulldir) !== realpath($CFG->dirroot.'/theme')) {
554                // Include themes in standard location too.
555                array_unshift($fulldirs, $CFG->dirroot.'/theme');
556            }
557        }
558
559        $result = array();
560
561        foreach ($fulldirs as $fulldir) {
562            if (!is_dir($fulldir)) {
563                continue;
564            }
565            $items = new \DirectoryIterator($fulldir);
566            foreach ($items as $item) {
567                if ($item->isDot() or !$item->isDir()) {
568                    continue;
569                }
570                $pluginname = $item->getFilename();
571                if ($plugintype === 'auth' and $pluginname === 'db') {
572                    // Special exception for this wrong plugin name.
573                } else if (isset(self::$ignoreddirs[$pluginname])) {
574                    continue;
575                }
576                if (!self::is_valid_plugin_name($plugintype, $pluginname)) {
577                    // Always ignore plugins with problematic names here.
578                    continue;
579                }
580                $result[$pluginname] = $fulldir.'/'.$pluginname;
581                unset($item);
582            }
583            unset($items);
584        }
585
586        ksort($result);
587        return $result;
588    }
589
590    /**
591     * Find all classes that can be autoloaded including frankenstyle namespaces.
592     */
593    protected static function fill_classmap_cache() {
594        global $CFG;
595
596        self::$classmap = array();
597
598        self::load_classes('core', "$CFG->dirroot/lib/classes");
599
600        foreach (self::$subsystems as $subsystem => $fulldir) {
601            if (!$fulldir) {
602                continue;
603            }
604            self::load_classes('core_'.$subsystem, "$fulldir/classes");
605        }
606
607        foreach (self::$plugins as $plugintype => $plugins) {
608            foreach ($plugins as $pluginname => $fulldir) {
609                self::load_classes($plugintype.'_'.$pluginname, "$fulldir/classes");
610            }
611        }
612        ksort(self::$classmap);
613    }
614
615    /**
616     * Fills up the cache defining what plugins have certain files.
617     *
618     * @see self::get_plugin_list_with_file
619     * @return void
620     */
621    protected static function fill_filemap_cache() {
622        global $CFG;
623
624        self::$filemap = array();
625
626        foreach (self::$filestomap as $file) {
627            if (!isset(self::$filemap[$file])) {
628                self::$filemap[$file] = array();
629            }
630            foreach (self::$plugins as $plugintype => $plugins) {
631                if (!isset(self::$filemap[$file][$plugintype])) {
632                    self::$filemap[$file][$plugintype] = array();
633                }
634                foreach ($plugins as $pluginname => $fulldir) {
635                    if (file_exists("$fulldir/$file")) {
636                        self::$filemap[$file][$plugintype][$pluginname] = "$fulldir/$file";
637                    }
638                }
639            }
640        }
641    }
642
643    /**
644     * Find classes in directory and recurse to subdirs.
645     * @param string $component
646     * @param string $fulldir
647     * @param string $namespace
648     */
649    protected static function load_classes($component, $fulldir, $namespace = '') {
650        if (!is_dir($fulldir)) {
651            return;
652        }
653
654        if (!is_readable($fulldir)) {
655            // TODO: MDL-51711 We should generate some diagnostic debugging information in this case
656            // because its pretty likely to lead to a missing class error further down the line.
657            // But our early setup code can't handle errors this early at the moment.
658            return;
659        }
660
661        $items = new \DirectoryIterator($fulldir);
662        foreach ($items as $item) {
663            if ($item->isDot()) {
664                continue;
665            }
666            if ($item->isDir()) {
667                $dirname = $item->getFilename();
668                self::load_classes($component, "$fulldir/$dirname", $namespace.'\\'.$dirname);
669                continue;
670            }
671
672            $filename = $item->getFilename();
673            $classname = preg_replace('/\.php$/', '', $filename);
674
675            if ($filename === $classname) {
676                // Not a php file.
677                continue;
678            }
679            if ($namespace === '') {
680                // Legacy long frankenstyle class name.
681                self::$classmap[$component.'_'.$classname] = "$fulldir/$filename";
682            }
683            // New namespaced classes.
684            self::$classmap[$component.$namespace.'\\'.$classname] = "$fulldir/$filename";
685        }
686        unset($item);
687        unset($items);
688    }
689
690    /**
691     * Fill caches for classes following the PSR-0 standard for the
692     * specified Vendors.
693     *
694     * PSR Autoloading is detailed at http://www.php-fig.org/psr/psr-0/.
695     */
696    protected static function fill_psr_cache() {
697        global $CFG;
698
699        $psrsystems = array(
700            'Horde' => 'horde/framework',
701        );
702        self::$psrclassmap = array();
703
704        foreach ($psrsystems as $system => $fulldir) {
705            if (!$fulldir) {
706                continue;
707            }
708            self::load_psr_classes($CFG->libdir . DIRECTORY_SEPARATOR . $fulldir);
709        }
710    }
711
712    /**
713     * Find all PSR-0 style classes in within the base directory.
714     *
715     * @param string $basedir The base directory that the PSR-type library can be found in.
716     * @param string $subdir The directory within the basedir to search for classes within.
717     */
718    protected static function load_psr_classes($basedir, $subdir = null) {
719        if ($subdir) {
720            $fulldir = realpath($basedir . DIRECTORY_SEPARATOR . $subdir);
721            $classnameprefix = preg_replace('#' . preg_quote(DIRECTORY_SEPARATOR) . '#', '_', $subdir);
722        } else {
723            $fulldir = $basedir;
724        }
725        if (!$fulldir || !is_dir($fulldir)) {
726            return;
727        }
728
729        $items = new \DirectoryIterator($fulldir);
730        foreach ($items as $item) {
731            if ($item->isDot()) {
732                continue;
733            }
734            if ($item->isDir()) {
735                $dirname = $item->getFilename();
736                $newsubdir = $dirname;
737                if ($subdir) {
738                    $newsubdir = implode(DIRECTORY_SEPARATOR, array($subdir, $dirname));
739                }
740                self::load_psr_classes($basedir, $newsubdir);
741                continue;
742            }
743
744            $filename = $item->getFilename();
745            $classname = preg_replace('/\.php$/', '', $filename);
746
747            if ($filename === $classname) {
748                // Not a php file.
749                continue;
750            }
751
752            if ($classnameprefix) {
753                $classname = $classnameprefix . '_' . $classname;
754            }
755
756            self::$psrclassmap[$classname] = $fulldir . DIRECTORY_SEPARATOR . $filename;
757        }
758        unset($item);
759        unset($items);
760    }
761
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    }
780
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    }
790
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();
799
800        if (!isset(self::$plugins[$plugintype])) {
801            return array();
802        }
803        return self::$plugins[$plugintype];
804    }
805
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.
822
823        if ($class) {
824            $suffix = '_' . $class;
825        } else {
826            $suffix = '';
827        }
828
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            }
840
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            }
847
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        }
857
858        return $pluginclasses;
859    }
860
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();
874
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        }
890
891        if ($include) {
892            foreach ($pluginfiles as $path) {
893                include_once($path);
894            }
895        }
896
897        return $pluginfiles;
898    }
899
900    /**
901     * Returns the exact absolute path to plugin directory.
902     *
903     * @param string $plugintype type of plugin
904     * @param string $pluginname name of the plugin
905     * @return string full path to plugin directory; null if not found
906     */
907    public static function get_plugin_directory($plugintype, $pluginname) {
908        if (empty($pluginname)) {
909            // Invalid plugin name, sorry.
910            return null;
911        }
912
913        self::init();
914
915        if (!isset(self::$plugins[$plugintype][$pluginname])) {
916            return null;
917        }
918        return self::$plugins[$plugintype][$pluginname];
919    }
920
921    /**
922     * Returns the exact absolute path to plugin directory.
923     *
924     * @param string $subsystem type of core subsystem
925     * @return string full path to subsystem directory; null if not found
926     */
927    public static function get_subsystem_directory($subsystem) {
928        self::init();
929
930        if (!isset(self::$subsystems[$subsystem])) {
931            return null;
932        }
933        return self::$subsystems[$subsystem];
934    }
935
936    /**
937     * This method validates a plug name. It is much faster than calling clean_param.
938     *
939     * @param string $plugintype type of plugin
940     * @param string $pluginname a string that might be a plugin name.
941     * @return bool if this string is a valid plugin name.
942     */
943    public static function is_valid_plugin_name($plugintype, $pluginname) {
944        if ($plugintype === 'mod') {
945            // Modules must not have the same name as core subsystems.
946            if (!isset(self::$subsystems)) {
947                // Watch out, this is called from init!
948                self::init();
949            }
950            if (isset(self::$subsystems[$pluginname])) {
951                return false;
952            }
953            // Modules MUST NOT have any underscores,
954            // component normalisation would break very badly otherwise!
955            return (bool)preg_match('/^[a-z][a-z0-9]*$/', $pluginname);
956
957        } else {
958            return (bool)preg_match('/^[a-z](?:[a-z0-9_](?!__))*[a-z0-9]+$/', $pluginname);
959        }
960    }
961
962    /**
963     * Normalize the component name.
964     *
965     * Note: this does not verify the validity of the plugin or component.
966     *
967     * @param string $component
968     * @return string
969     */
970    public static function normalize_componentname($componentname) {
971        list($plugintype, $pluginname) = self::normalize_component($componentname);
972        if ($plugintype === 'core' && is_null($pluginname)) {
973            return $plugintype;
974        }
975        return $plugintype . '_' . $pluginname;
976    }
977
978    /**
979     * Normalize the component name using the "frankenstyle" rules.
980     *
981     * Note: this does not verify the validity of plugin or type names.
982     *
983     * @param string $component
984     * @return array as (string)$type => (string)$plugin
985     */
986    public static function normalize_component($component) {
987        if ($component === 'moodle' or $component === 'core' or $component === '') {
988            return array('core', null);
989        }
990
991        if (strpos($component, '_') === false) {
992            self::init();
993            if (array_key_exists($component, self::$subsystems)) {
994                $type   = 'core';
995                $plugin = $component;
996            } else {
997                // Everything else without underscore is a module.
998                $type   = 'mod';
999                $plugin = $component;
1000            }
1001
1002        } else {
1003            list($type, $plugin) = explode('_', $component, 2);
1004            if ($type === 'moodle') {
1005                $type = 'core';
1006            }
1007            // Any unknown type must be a subplugin.
1008        }
1009
1010        return array($type, $plugin);
1011    }
1012
1013    /**
1014     * Return exact absolute path to a plugin directory.
1015     *
1016     * @param string $component name such as 'moodle', 'mod_forum'
1017     * @return string full path to component directory; NULL if not found
1018     */
1019    public static function get_component_directory($component) {
1020        global $CFG;
1021
1022        list($type, $plugin) = self::normalize_component($component);
1023
1024        if ($type === 'core') {
1025            if ($plugin === null) {
1026                return $path = $CFG->libdir;
1027            }
1028            return self::get_subsystem_directory($plugin);
1029        }
1030
1031        return self::get_plugin_directory($type, $plugin);
1032    }
1033
1034    /**
1035     * Returns list of plugin types that allow subplugins.
1036     * @return array as (string)plugintype => (string)fulldir
1037     */
1038    public static function get_plugin_types_with_subplugins() {
1039        self::init();
1040
1041        $return = array();
1042        foreach (self::$supportsubplugins as $type) {
1043            $return[$type] = self::$plugintypes[$type];
1044        }
1045        return $return;
1046    }
1047
1048    /**
1049     * Returns parent of this subplugin type.
1050     *
1051     * @param string $type
1052     * @return string parent component or null
1053     */
1054    public static function get_subtype_parent($type) {
1055        self::init();
1056
1057        if (isset(self::$parents[$type])) {
1058            return self::$parents[$type];
1059        }
1060
1061        return null;
1062    }
1063
1064    /**
1065     * Return all subplugins of this component.
1066     * @param string $component.
1067     * @return array $subtype=>array($component, ..), null if no subtypes defined
1068     */
1069    public static function get_subplugins($component) {
1070        self::init();
1071
1072        if (isset(self::$subplugins[$component])) {
1073            return self::$subplugins[$component];
1074        }
1075
1076        return null;
1077    }
1078
1079    /**
1080     * Returns hash of all versions including core and all plugins.
1081     *
1082     * This is relatively slow and not fully cached, use with care!
1083     *
1084     * @return string sha1 hash
1085     */
1086    public static function get_all_versions_hash() {
1087        global $CFG;
1088
1089        self::init();
1090
1091        $versions = array();
1092
1093        // Main version first.
1094        $versions['core'] = self::fetch_core_version();
1095
1096        // The problem here is tha the component cache might be stable,
1097        // we want this to work also on frontpage without resetting the component cache.
1098        $usecache = false;
1099        if (CACHE_DISABLE_ALL or (defined('IGNORE_COMPONENT_CACHE') and IGNORE_COMPONENT_CACHE)) {
1100            $usecache = true;
1101        }
1102
1103        // Now all plugins.
1104        $plugintypes = core_component::get_plugin_types();
1105        foreach ($plugintypes as $type => $typedir) {
1106            if ($usecache) {
1107                $plugs = core_component::get_plugin_list($type);
1108            } else {
1109                $plugs = self::fetch_plugins($type, $typedir);
1110            }
1111            foreach ($plugs as $plug => $fullplug) {
1112                $plugin = new stdClass();
1113                $plugin->version = null;
1114                $module = $plugin;
1115                include($fullplug.'/version.php');
1116                $versions[$type.'_'.$plug] = $plugin->version;
1117            }
1118        }
1119
1120        return sha1(serialize($versions));
1121    }
1122
1123    /**
1124     * Invalidate opcode cache for given file, this is intended for
1125     * php files that are stored in dataroot.
1126     *
1127     * Note: we need it here because this class must be self-contained.
1128     *
1129     * @param string $file
1130     */
1131    public static function invalidate_opcode_php_cache($file) {
1132        if (function_exists('opcache_invalidate')) {
1133            if (!file_exists($file)) {
1134                return;
1135            }
1136            opcache_invalidate($file, true);
1137        }
1138    }
1139
1140    /**
1141     * Return true if subsystemname is core subsystem.
1142     *
1143     * @param string $subsystemname name of the subsystem.
1144     * @return bool true if core subsystem.
1145     */
1146    public static function is_core_subsystem($subsystemname) {
1147        return isset(self::$subsystems[$subsystemname]);
1148    }
1149
1150    /**
1151     * Records all class renames that have been made to facilitate autoloading.
1152     */
1153    protected static function fill_classmap_renames_cache() {
1154        global $CFG;
1155
1156        self::$classmaprenames = array();
1157
1158        self::load_renamed_classes("$CFG->dirroot/lib/");
1159
1160        foreach (self::$subsystems as $subsystem => $fulldir) {
1161            self::load_renamed_classes($fulldir);
1162        }
1163
1164        foreach (self::$plugins as $plugintype => $plugins) {
1165            foreach ($plugins as $pluginname => $fulldir) {
1166                self::load_renamed_classes($fulldir);
1167            }
1168        }
1169    }
1170
1171    /**
1172     * Loads the db/renamedclasses.php file from the given directory.
1173     *
1174     * The renamedclasses.php should contain a key => value array ($renamedclasses) where the key is old class name,
1175     * and the value is the new class name.
1176     * It is only included when we are populating the component cache. After that is not needed.
1177     *
1178     * @param string $fulldir
1179     */
1180    protected static function load_renamed_classes($fulldir) {
1181        $file = $fulldir . '/db/renamedclasses.php';
1182        if (is_readable($file)) {
1183            $renamedclasses = null;
1184            require($file);
1185            if (is_array($renamedclasses)) {
1186                foreach ($renamedclasses as $oldclass => $newclass) {
1187                    self::$classmaprenames[(string)$oldclass] = (string)$newclass;
1188                }
1189            }
1190        }
1191    }
1192}
Note: See TracBrowser for help on using the repository browser.