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 * Functions for generating the HTML that Moodle should output.
19 *
20 * Please see http://docs.moodle.org/en/Developement:How_Moodle_outputs_HTML
21 * for an overview.
22 *
23 * @copyright 2009 Tim Hunt
24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 * @package core
26 * @category output
27 */
28
29defined('MOODLE_INTERNAL') || die();
30
31require_once($CFG->libdir.'/outputcomponents.php');
32require_once($CFG->libdir.'/outputactions.php');
33require_once($CFG->libdir.'/outputfactories.php');
34require_once($CFG->libdir.'/outputrenderers.php');
35require_once($CFG->libdir.'/outputrequirementslib.php');
36
37/**
38 * Returns current theme revision number.
39 *
40 * @return int
41 */
42function theme_get_revision() {
43    global $CFG;
44
45    if (empty($CFG->themedesignermode)) {
46        if (empty($CFG->themerev)) {
47            // This only happens during install. It doesn't matter what themerev we use as long as it's positive.
48            return 1;
49        } else {
50            return $CFG->themerev;
51        }
52
53    } else {
54        return -1;
55    }
56}
57
58/**
59 * Returns current theme sub revision number. This is the revision for
60 * this theme exclusively, not the global theme revision.
61 *
62 * @param string $themename The non-frankenstyle name of the theme
63 * @return int
64 */
65function theme_get_sub_revision_for_theme($themename) {
66    global $CFG;
67
68    if (empty($CFG->themedesignermode)) {
69        $pluginname = "theme_{$themename}";
70        $revision = during_initial_install() ? null : get_config($pluginname, 'themerev');
71
72        if (empty($revision)) {
73            // This only happens during install. It doesn't matter what themerev we use as long as it's positive.
74            return 1;
75        } else {
76            return $revision;
77        }
78    } else {
79        return -1;
80    }
81}
82
83/**
84 * Calculates and returns the next theme revision number.
85 *
86 * @return int
87 */
88function theme_get_next_revision() {
89    global $CFG;
90
91    $next = time();
92    if (isset($CFG->themerev) and $next <= $CFG->themerev and $CFG->themerev - $next < 60*60) {
93        // This resolves problems when reset is requested repeatedly within 1s,
94        // the < 1h condition prevents accidental switching to future dates
95        // because we might not recover from it.
96        $next = $CFG->themerev+1;
97    }
98
99    return $next;
100}
101
102/**
103 * Calculates and returns the next theme revision number.
104 *
105 * @param string $themename The non-frankenstyle name of the theme
106 * @return int
107 */
108function theme_get_next_sub_revision_for_theme($themename) {
109    global $CFG;
110
111    $next = time();
112    $current = theme_get_sub_revision_for_theme($themename);
113    if ($next <= $current and $current - $next < 60 * 60) {
114        // This resolves problems when reset is requested repeatedly within 1s,
115        // the < 1h condition prevents accidental switching to future dates
116        // because we might not recover from it.
117        $next = $current + 1;
118    }
119
120    return $next;
121}
122
123/**
124 * Sets the current theme revision number.
125 *
126 * @param int $revision The new theme revision number
127 */
128function theme_set_revision($revision) {
129    set_config('themerev', $revision);
130}
131
132/**
133 * Sets the current theme revision number for a specific theme.
134 * This does not affect the global themerev value.
135 *
136 * @param string $themename The non-frankenstyle name of the theme
137 * @param int    $revision  The new theme revision number
138 */
139function theme_set_sub_revision_for_theme($themename, $revision) {
140    set_config('themerev', $revision, "theme_{$themename}");
141}
142
143/**
144 * Get the path to a theme config.php file.
145 *
146 * @param string $themename The non-frankenstyle name of the theme to check
147 */
148function theme_get_config_file_path($themename) {
149    global $CFG;
150
151    if (file_exists("{$CFG->dirroot}/theme/{$themename}/config.php")) {
152        return "{$CFG->dirroot}/theme/{$themename}/config.php";
153    } else if (!empty($CFG->themedir) and file_exists("{$CFG->themedir}/{$themename}/config.php")) {
154        return "{$CFG->themedir}/{$themename}/config.php";
155    } else {
156        return null;
157    }
158}
159
160/**
161 * Get the path to the local cached CSS file.
162 *
163 * @param string $themename      The non-frankenstyle theme name.
164 * @param int    $globalrevision The global theme revision.
165 * @param int    $themerevision  The theme specific revision.
166 * @param string $direction      Either 'ltr' or 'rtl' (case sensitive).
167 */
168function theme_get_css_filename($themename, $globalrevision, $themerevision, $direction) {
169    global $CFG;
170
171    $path = "{$CFG->localcachedir}/theme/{$globalrevision}/{$themename}/css";
172    $filename = $direction == 'rtl' ? "all-rtl_{$themerevision}" : "all_{$themerevision}";
173    return "{$path}/{$filename}.css";
174}
175
176/**
177 * Generates and saves the CSS files for the given theme configs.
178 *
179 * @param theme_config[] $themeconfigs An array of theme_config instances.
180 * @param array          $directions   Must be a subset of ['rtl', 'ltr'].
181 * @param bool           $cache        Should the generated files be stored in local cache.
182 * @return array         The built theme content in a multi-dimensional array of name => direction => content
183 */
184function theme_build_css_for_themes($themeconfigs = [], $directions = ['rtl', 'ltr'],
185        $cache = true, $mtraceprogress = false): array {
186    global $CFG;
187
188    if (empty($themeconfigs)) {
189        return [];
190    }
191
192    require_once("{$CFG->libdir}/csslib.php");
193
194    $themescss = [];
195    $themerev = theme_get_revision();
196    // Make sure the local cache directory exists.
197    make_localcache_directory('theme');
198
199    foreach ($themeconfigs as $themeconfig) {
200        $themecss = [];
201        $oldrevision = theme_get_sub_revision_for_theme($themeconfig->name);
202        $newrevision = theme_get_next_sub_revision_for_theme($themeconfig->name);
203
204        // First generate all the new css.
205        foreach ($directions as $direction) {
206            if ($mtraceprogress) {
207                $timestart = microtime(true);
208                mtrace('Building theme CSS for ' . $themeconfig->name . ' [' .
209                        $direction . '] ...', '');
210            }
211            // Lock it on. Technically we should build all themes for SVG and no SVG - but ie9 is out of support.
212            $themeconfig->force_svg_use(true);
213            $themeconfig->set_rtl_mode(($direction === 'rtl'));
214
215            $themecss[$direction] = $themeconfig->get_css_content();
216            if ($cache) {
217                $themeconfig->set_css_content_cache($themecss[$direction]);
218                $filename = theme_get_css_filename($themeconfig->name, $themerev, $newrevision, $direction);
219                css_store_css($themeconfig, $filename, $themecss[$direction]);
220            }
221            if ($mtraceprogress) {
222                mtrace(' done in ' . round(microtime(true) - $timestart, 2) . ' seconds.');
223            }
224        }
225        $themescss[$themeconfig->name] = $themecss;
226
227        if ($cache) {
228            // Only update the theme revision after we've successfully created the
229            // new CSS cache.
230            theme_set_sub_revision_for_theme($themeconfig->name, $newrevision);
231
232            // Now purge old files. We must purge all old files in the local cache
233            // because we've incremented the theme sub revision. This will leave any
234            // files with the old revision inaccessbile so we might as well removed
235            // them from disk.
236            foreach (['ltr', 'rtl'] as $direction) {
237                $oldcss = theme_get_css_filename($themeconfig->name, $themerev, $oldrevision, $direction);
238                if (file_exists($oldcss)) {
239                    unlink($oldcss);
240                }
241            }
242        }
243    }
244
245    return $themescss;
246}
247
248/**
249 * Invalidate all server and client side caches.
250 *
251 * This method deletes the physical directory that is used to cache the theme
252 * files used for serving.
253 * Because it deletes the main theme cache directory all themes are reset by
254 * this function.
255 */
256function theme_reset_all_caches() {
257    global $CFG, $PAGE;
258    require_once("{$CFG->libdir}/filelib.php");
259
260    $next = theme_get_next_revision();
261    theme_set_revision($next);
262
263    if (!empty($CFG->themedesignermode)) {
264        $cache = cache::make_from_params(cache_store::MODE_APPLICATION, 'core', 'themedesigner');
265        $cache->purge();
266    }
267
268    // Purge compiled post processed css.
269    cache::make('core', 'postprocessedcss')->purge();
270
271    // Delete all old theme localcaches.
272    $themecachedirs = glob("{$CFG->localcachedir}/theme/*", GLOB_ONLYDIR);
273    foreach ($themecachedirs as $localcachedir) {
274        fulldelete($localcachedir);
275    }
276
277    if ($PAGE) {
278        $PAGE->reload_theme();
279    }
280}
281
282/**
283 * Reset static caches.
284 *
285 * This method indicates that all running cron processes should exit at the
286 * next opportunity.
287 */
288function theme_reset_static_caches() {
289    \core\task\manager::clear_static_caches();
290}
291
292/**
293 * Enable or disable theme designer mode.
294 *
295 * @param bool $state
296 */
297function theme_set_designer_mod($state) {
298    set_config('themedesignermode', (int)!empty($state));
299    // Reset caches after switching mode so that any designer mode caches get purged too.
300    theme_reset_all_caches();
301}
302
303/**
304 * Checks if the given device has a theme defined in config.php.
305 *
306 * @return bool
307 */
308function theme_is_device_locked($device) {
309    global $CFG;
310    $themeconfigname = core_useragent::get_device_type_cfg_var_name($device);
311    return isset($CFG->config_php_settings[$themeconfigname]);
312}
313
314/**
315 * Returns the theme named defined in config.php for the given device.
316 *
317 * @return string or null
318 */
319function theme_get_locked_theme_for_device($device) {
320    global $CFG;
321
322    if (!theme_is_device_locked($device)) {
323        return null;
324    }
325
326    $themeconfigname = core_useragent::get_device_type_cfg_var_name($device);
327    return $CFG->config_php_settings[$themeconfigname];
328}
329
330/**
331 * This class represents the configuration variables of a Moodle theme.
332 *
333 * All the variables with access: public below (with a few exceptions that are marked)
334 * are the properties you can set in your themes config.php file.
335 *
336 * There are also some methods and protected variables that are part of the inner
337 * workings of Moodle's themes system. If you are just editing a themes config.php
338 * file, you can just ignore those, and the following information for developers.
339 *
340 * Normally, to create an instance of this class, you should use the
341 * {@link theme_config::load()} factory method to load a themes config.php file.
342 * However, normally you don't need to bother, because moodle_page (that is, $PAGE)
343 * will create one for you, accessible as $PAGE->theme.
344 *
345 * @copyright 2009 Tim Hunt
346 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
347 * @since Moodle 2.0
348 * @package core
349 * @category output
350 */
351class theme_config {
352
353    /**
354     * @var string Default theme, used when requested theme not found.
355     */
356    const DEFAULT_THEME = 'boost';
357
358    /** The key under which the SCSS file is stored amongst the CSS files. */
359    const SCSS_KEY = '__SCSS__';
360
361    /**
362     * @var array You can base your theme on other themes by linking to the other theme as
363     * parents. This lets you use the CSS and layouts from the other themes
364     * (see {@link theme_config::$layouts}).
365     * That makes it easy to create a new theme that is similar to another one
366     * but with a few changes. In this themes CSS you only need to override
367     * those rules you want to change.
368     */
369    public $parents;
370
371    /**
372     * @var array The names of all the stylesheets from this theme that you would
373     * like included, in order. Give the names of the files without .css.
374     */
375    public $sheets = array();
376
377    /**
378     * @var array The names of all the stylesheets from parents that should be excluded.
379     * true value may be used to specify all parents or all themes from one parent.
380     * If no value specified value from parent theme used.
381     */
382    public $parents_exclude_sheets = null;
383
384    /**
385     * @var array List of plugin sheets to be excluded.
386     * If no value specified value from parent theme used.
387     */
388    public $plugins_exclude_sheets = null;
389
390    /**
391     * @var array List of style sheets that are included in the text editor bodies.
392     * Sheets from parent themes are used automatically and can not be excluded.
393     */
394    public $editor_sheets = array();
395
396    /**
397     * @var bool Whether a fallback version of the stylesheet will be used
398     * whilst the final version is generated.
399     */
400    public $usefallback = false;
401
402    /**
403     * @var array The names of all the javascript files this theme that you would
404     * like included from head, in order. Give the names of the files without .js.
405     */
406    public $javascripts = array();
407
408    /**
409     * @var array The names of all the javascript files this theme that you would
410     * like included from footer, in order. Give the names of the files without .js.
411     */
412    public $javascripts_footer = array();
413
414    /**
415     * @var array The names of all the javascript files from parents that should
416     * be excluded. true value may be used to specify all parents or all themes
417     * from one parent.
418     * If no value specified value from parent theme used.
419     */
420    public $parents_exclude_javascripts = null;
421
422    /**
423     * @var array Which file to use for each page layout.
424     *
425     * This is an array of arrays. The keys of the outer array are the different layouts.
426     * Pages in Moodle are using several different layouts like 'normal', 'course', 'home',
427     * 'popup', 'form', .... The most reliable way to get a complete list is to look at
428     * {@link http://cvs.moodle.org/moodle/theme/base/config.php?view=markup the base theme config.php file}.
429     * That file also has a good example of how to set this setting.
430     *
431     * For each layout, the value in the outer array is an array that describes
432     * how you want that type of page to look. For example
433     * <pre>
434     *   $THEME->layouts = array(
435     *       // Most pages - if we encounter an unknown or a missing page type, this one is used.
436     *       'standard' => array(
437     *           'theme' = 'mytheme',
438     *           'file' => 'normal.php',
439     *           'regions' => array('side-pre', 'side-post'),
440     *           'defaultregion' => 'side-post'
441     *       ),
442     *       // The site home page.
443     *       'home' => array(
444     *           'theme' = 'mytheme',
445     *           'file' => 'home.php',
446     *           'regions' => array('side-pre', 'side-post'),
447     *           'defaultregion' => 'side-post'
448     *       ),
449     *       // ...
450     *   );
451     * </pre>
452     *
453     * 'theme' name of the theme where is the layout located
454     * 'file' is the layout file to use for this type of page.
455     * layout files are stored in layout subfolder
456     * 'regions' This lists the regions on the page where blocks may appear. For
457     * each region you list here, your layout file must include a call to
458     * <pre>
459     *   echo $OUTPUT->blocks_for_region($regionname);
460     * </pre>
461     * or equivalent so that the blocks are actually visible.
462     *
463     * 'defaultregion' If the list of regions is non-empty, then you must pick
464     * one of the one of them as 'default'. This has two meanings. First, this is
465     * where new blocks are added. Second, if there are any blocks associated with
466     * the page, but in non-existent regions, they appear here. (Imaging, for example,
467     * that someone added blocks using a different theme that used different region
468     * names, and then switched to this theme.)
469     */
470    public $layouts = array();
471
472    /**
473     * @var string Name of the renderer factory class to use. Must implement the
474     * {@link renderer_factory} interface.
475     *
476     * This is an advanced feature. Moodle output is generated by 'renderers',
477     * you can customise the HTML that is output by writing custom renderers,
478     * and then you need to specify 'renderer factory' so that Moodle can find
479     * your renderers.
480     *
481     * There are some renderer factories supplied with Moodle. Please follow these
482     * links to see what they do.
483     * <ul>
484     * <li>{@link standard_renderer_factory} - the default.</li>
485     * <li>{@link theme_overridden_renderer_factory} - use this if you want to write
486     *      your own custom renderers in a lib.php file in this theme (or the parent theme).</li>
487     * </ul>
488     */
489    public $rendererfactory = 'standard_renderer_factory';
490
491    /**
492     * @var string Function to do custom CSS post-processing.
493     *
494     * This is an advanced feature. If you want to do custom post-processing on the
495     * CSS before it is output (for example, to replace certain variable names
496     * with particular values) you can give the name of a function here.
497     */
498    public $csspostprocess = null;
499
500    /**
501     * @var string Function to do custom CSS post-processing on a parsed CSS tree.
502     *
503     * This is an advanced feature. If you want to do custom post-processing on the
504     * CSS before it is output, you can provide the name of the function here. The
505     * function will receive a CSS tree document as first parameter, and the theme_config
506     * object as second parameter. A return value is not required, the tree can
507     * be edited in place.
508     */
509    public $csstreepostprocessor = null;
510
511    /**
512     * @var string Accessibility: Right arrow-like character is
513     * used in the breadcrumb trail, course navigation menu
514     * (previous/next activity), calendar, and search forum block.
515     * If the theme does not set characters, appropriate defaults
516     * are set automatically. Please DO NOT
517     * use &lt; &gt; &raquo; - these are confusing for blind users.
518     */
519    public $rarrow = null;
520
521    /**
522     * @var string Accessibility: Left arrow-like character is
523     * used in the breadcrumb trail, course navigation menu
524     * (previous/next activity), calendar, and search forum block.
525     * If the theme does not set characters, appropriate defaults
526     * are set automatically. Please DO NOT
527     * use &lt; &gt; &raquo; - these are confusing for blind users.
528     */
529    public $larrow = null;
530
531    /**
532     * @var string Accessibility: Up arrow-like character is used in
533     * the book heirarchical navigation.
534     * If the theme does not set characters, appropriate defaults
535     * are set automatically. Please DO NOT
536     * use ^ - this is confusing for blind users.
537     */
538    public $uarrow = null;
539
540    /**
541     * @var string Accessibility: Down arrow-like character.
542     * If the theme does not set characters, appropriate defaults
543     * are set automatically.
544     */
545    public $darrow = null;
546
547    /**
548     * @var bool Some themes may want to disable ajax course editing.
549     */
550    public $enablecourseajax = true;
551
552    /**
553     * @var string Determines served document types
554     *  - 'html5' the only officially supported doctype in Moodle
555     *  - 'xhtml5' may be used in development for validation (not intended for production servers!)
556     *  - 'xhtml' XHTML 1.0 Strict for legacy themes only
557     */
558    public $doctype = 'html5';
559
560    /**
561     * @var string requiredblocks If set to a string, will list the block types that cannot be deleted. Defaults to
562     *                                   navigation and settings.
563     */
564    public $requiredblocks = false;
565
566    //==Following properties are not configurable from theme config.php==
567
568    /**
569     * @var string The name of this theme. Set automatically when this theme is
570     * loaded. This can not be set in theme config.php
571     */
572    public $name;
573
574    /**
575     * @var string The folder where this themes files are stored. This is set
576     * automatically. This can not be set in theme config.php
577     */
578    public $dir;
579
580    /**
581     * @var stdClass Theme settings stored in config_plugins table.
582     * This can not be set in theme config.php
583     */
584    public $settings = null;
585
586    /**
587     * @var bool If set to true and the theme enables the dock then  blocks will be able
588     * to be moved to the special dock
589     */
590    public $enable_dock = false;
591
592    /**
593     * @var bool If set to true then this theme will not be shown in the theme selector unless
594     * theme designer mode is turned on.
595     */
596    public $hidefromselector = false;
597
598    /**
599     * @var array list of YUI CSS modules to be included on each page. This may be used
600     * to remove cssreset and use cssnormalise module instead.
601     */
602    public $yuicssmodules = array('cssreset', 'cssfonts', 'cssgrids', 'cssbase');
603
604    /**
605     * An associative array of block manipulations that should be made if the user is using an rtl language.
606     * The key is the original block region, and the value is the block region to change to.
607     * This is used when displaying blocks for regions only.
608     * @var array
609     */
610    public $blockrtlmanipulations = array();
611
612    /**
613     * @var renderer_factory Instance of the renderer_factory implementation
614     * we are using. Implementation detail.
615     */
616    protected $rf = null;
617
618    /**
619     * @var array List of parent config objects.
620     **/
621    protected $parent_configs = array();
622
623    /**
624     * Used to determine whether we can serve SVG images or not.
625     * @var bool
626     */
627    private $usesvg = null;
628
629    /**
630     * Whether in RTL mode or not.
631     * @var bool
632     */
633    protected $rtlmode = false;
634
635    /**
636     * The SCSS file to compile (without .scss), located in the scss/ folder of the theme.
637     * Or a Closure, which receives the theme_config as argument and must
638     * return the SCSS content.
639     * @var string|Closure
640     */
641    public $scss = false;
642
643    /**
644     * Local cache of the SCSS property.
645     * @var false|array
646     */
647    protected $scsscache = null;
648
649    /**
650     * The name of the function to call to get the SCSS code to inject.
651     * @var string
652     */
653    public $extrascsscallback = null;
654
655    /**
656     * The name of the function to call to get SCSS to prepend.
657     * @var string
658     */
659    public $prescsscallback = null;
660
661    /**
662     * Sets the render method that should be used for rendering custom block regions by scripts such as my/index.php
663     * Defaults to {@link core_renderer::blocks_for_region()}
664     * @var string
665     */
666    public $blockrendermethod = null;
667
668    /**
669     * Remember the results of icon remapping for the current page.
670     * @var array
671     */
672    public $remapiconcache = [];
673
674    /**
675     * The name of the function to call to get precompiled CSS.
676     * @var string
677     */
678    public $precompiledcsscallback = null;
679
680    /**
681     * Load the config.php file for a particular theme, and return an instance
682     * of this class. (That is, this is a factory method.)
683     *
684     * @param string $themename the name of the theme.
685     * @return theme_config an instance of this class.
686     */
687    public static function load($themename) {
688        global $CFG;
689
690        // load theme settings from db
691        try {
692            $settings = get_config('theme_'.$themename);
693        } catch (dml_exception $e) {
694            // most probably moodle tables not created yet
695            $settings = new stdClass();
696        }
697
698        if ($config = theme_config::find_theme_config($themename, $settings)) {
699            return new theme_config($config);
700
701        } else if ($themename == theme_config::DEFAULT_THEME) {
702            throw new coding_exception('Default theme '.theme_config::DEFAULT_THEME.' not available or broken!');
703
704        } else if ($config = theme_config::find_theme_config($CFG->theme, $settings)) {
705            debugging('This page should be using theme ' . $themename .
706                    ' which cannot be initialised. Falling back to the site theme ' . $CFG->theme, DEBUG_NORMAL);
707            return new theme_config($config);
708
709        } else {
710            // bad luck, the requested theme has some problems - admin see details in theme config
711            debugging('This page should be using theme ' . $themename .
712                    ' which cannot be initialised. Nor can the site theme ' . $CFG->theme .
713                    '. Falling back to ' . theme_config::DEFAULT_THEME, DEBUG_NORMAL);
714            return new theme_config(theme_config::find_theme_config(theme_config::DEFAULT_THEME, $settings));
715        }
716    }
717
718    /**
719     * Theme diagnostic code. It is very problematic to send debug output
720     * to the actual CSS file, instead this functions is supposed to
721     * diagnose given theme and highlights all potential problems.
722     * This information should be available from the theme selection page
723     * or some other debug page for theme designers.
724     *
725     * @param string $themename
726     * @return array description of problems
727     */
728    public static function diagnose($themename) {
729        //TODO: MDL-21108
730        return array();
731    }
732
733    /**
734     * Private constructor, can be called only from the factory method.
735     * @param stdClass $config
736     */
737    private function __construct($config) {
738        global $CFG; //needed for included lib.php files
739
740        $this->settings = $config->settings;
741        $this->name     = $config->name;
742        $this->dir      = $config->dir;
743
744        if ($this->name != self::DEFAULT_THEME) {
745            $baseconfig = self::find_theme_config(self::DEFAULT_THEME, $this->settings);
746        } else {
747            $baseconfig = $config;
748        }
749
750        $configurable = array(
751            'parents', 'sheets', 'parents_exclude_sheets', 'plugins_exclude_sheets', 'usefallback',
752            'javascripts', 'javascripts_footer', 'parents_exclude_javascripts',
753            'layouts', 'enablecourseajax', 'requiredblocks',
754            'rendererfactory', 'csspostprocess', 'editor_sheets', 'editor_scss', 'rarrow', 'larrow', 'uarrow', 'darrow',
755            'hidefromselector', 'doctype', 'yuicssmodules', 'blockrtlmanipulations', 'blockrendermethod',
756            'scss', 'extrascsscallback', 'prescsscallback', 'csstreepostprocessor', 'addblockposition',
757            'iconsystem', 'precompiledcsscallback');
758
759        foreach ($config as $key=>$value) {
760            if (in_array($key, $configurable)) {
761                $this->$key = $value;
762            }
763        }
764
765        // verify all parents and load configs and renderers
766        foreach ($this->parents as $parent) {
767            if (!$parent_config = theme_config::find_theme_config($parent, $this->settings)) {
768                // this is not good - better exclude faulty parents
769                continue;
770            }
771            $libfile = $parent_config->dir.'/lib.php';
772            if (is_readable($libfile)) {
773                // theme may store various function here
774                include_once($libfile);
775            }
776            $renderersfile = $parent_config->dir.'/renderers.php';
777            if (is_readable($renderersfile)) {
778                // may contain core and plugin renderers and renderer factory
779                include_once($renderersfile);
780            }
781            $this->parent_configs[$parent] = $parent_config;
782        }
783        $libfile = $this->dir.'/lib.php';
784        if (is_readable($libfile)) {
785            // theme may store various function here
786            include_once($libfile);
787        }
788        $rendererfile = $this->dir.'/renderers.php';
789        if (is_readable($rendererfile)) {
790            // may contain core and plugin renderers and renderer factory
791            include_once($rendererfile);
792        } else {
793            // check if renderers.php file is missnamed renderer.php
794            if (is_readable($this->dir.'/renderer.php')) {
795                debugging('Developer hint: '.$this->dir.'/renderer.php should be renamed to ' . $this->dir."/renderers.php.
796                    See: http://docs.moodle.org/dev/Output_renderers#Theme_renderers.", DEBUG_DEVELOPER);
797            }
798        }
799
800        // cascade all layouts properly
801        foreach ($baseconfig->layouts as $layout=>$value) {
802            if (!isset($this->layouts[$layout])) {
803                foreach ($this->parent_configs as $parent_config) {
804                    if (isset($parent_config->layouts[$layout])) {
805                        $this->layouts[$layout] = $parent_config->layouts[$layout];
806                        continue 2;
807                    }
808                }
809                $this->layouts[$layout] = $value;
810            }
811        }
812
813        //fix arrows if needed
814        $this->check_theme_arrows();
815    }
816
817    /**
818     * Let the theme initialise the page object (usually $PAGE).
819     *
820     * This may be used for example to request jQuery in add-ons.
821     *
822     * @param moodle_page $page
823     */
824    public function init_page(moodle_page $page) {
825        $themeinitfunction = 'theme_'.$this->name.'_page_init';
826        if (function_exists($themeinitfunction)) {
827            $themeinitfunction($page);
828        }
829    }
830
831    /**
832     * Checks if arrows $THEME->rarrow, $THEME->larrow, $THEME->uarrow, $THEME->darrow have been set (theme/-/config.php).
833     * If not it applies sensible defaults.
834     *
835     * Accessibility: right and left arrow Unicode characters for breadcrumb, calendar,
836     * search forum block, etc. Important: these are 'silent' in a screen-reader
837     * (unlike &gt; &raquo;), and must be accompanied by text.
838     */
839    private function check_theme_arrows() {
840        if (!isset($this->rarrow) and !isset($this->larrow)) {
841            // Default, looks good in Win XP/IE 6, Win/Firefox 1.5, Win/Netscape 8...
842            // Also OK in Win 9x/2K/IE 5.x
843            $this->rarrow = '&#x25BA;';
844            $this->larrow = '&#x25C4;';
845            $this->uarrow = '&#x25B2;';
846            $this->darrow = '&#x25BC;';
847            if (empty($_SERVER['HTTP_USER_AGENT'])) {
848                $uagent = '';
849            } else {
850                $uagent = $_SERVER['HTTP_USER_AGENT'];
851            }
852            if (false !== strpos($uagent, 'Opera')
853                || false !== strpos($uagent, 'Mac')) {
854                // Looks good in Win XP/Mac/Opera 8/9, Mac/Firefox 2, Camino, Safari.
855                // Not broken in Mac/IE 5, Mac/Netscape 7 (?).
856                $this->rarrow = '&#x25B6;&#xFE0E;';
857                $this->larrow = '&#x25C0;&#xFE0E;';
858            }
859            elseif ((false !== strpos($uagent, 'Konqueror'))
860                || (false !== strpos($uagent, 'Android')))  {
861                // The fonts on Android don't include the characters required for this to work as expected.
862                // So we use the same ones Konqueror uses.
863                $this->rarrow = '&rarr;';
864                $this->larrow = '&larr;';
865                $this->uarrow = '&uarr;';
866                $this->darrow = '&darr;';
867            }
868            elseif (isset($_SERVER['HTTP_ACCEPT_CHARSET'])
869                && false === stripos($_SERVER['HTTP_ACCEPT_CHARSET'], 'utf-8')) {
870                // (Win/IE 5 doesn't set ACCEPT_CHARSET, but handles Unicode.)
871                // To be safe, non-Unicode browsers!
872                $this->rarrow = '&gt;';
873                $this->larrow = '&lt;';
874                $this->uarrow = '^';
875                $this->darrow = 'v';
876            }
877
878            // RTL support - in RTL languages, swap r and l arrows
879            if (right_to_left()) {
880                $t = $this->rarrow;
881                $this->rarrow = $this->larrow;
882                $this->larrow = $t;
883            }
884        }
885    }
886
887    /**
888     * Returns output renderer prefixes, these are used when looking
889     * for the overridden renderers in themes.
890     *
891     * @return array
892     */
893    public function renderer_prefixes() {
894        global $CFG; // just in case the included files need it
895
896        $prefixes = array('theme_'.$this->name);
897
898        foreach ($this->parent_configs as $parent) {
899            $prefixes[] = 'theme_'.$parent->name;
900        }
901
902        return $prefixes;
903    }
904
905    /**
906     * Returns the stylesheet URL of this editor content
907     *
908     * @param bool $encoded false means use & and true use &amp; in URLs
909     * @return moodle_url
910     */
911    public function editor_css_url($encoded=true) {
912        global $CFG;
913        $rev = theme_get_revision();
914        if ($rev > -1) {
915            $themesubrevision = theme_get_sub_revision_for_theme($this->name);
916
917            // Provide the sub revision to allow us to invalidate cached theme CSS
918            // on a per theme basis, rather than globally.
919            if ($themesubrevision && $themesubrevision > 0) {
920                $rev .= "_{$themesubrevision}";
921            }
922
923            $url = new moodle_url("/theme/styles.php");
924            if (!empty($CFG->slasharguments)) {
925                $url->set_slashargument('/'.$this->name.'/'.$rev.'/editor', 'noparam', true);
926            } else {
927                $url->params(array('theme'=>$this->name,'rev'=>$rev, 'type'=>'editor'));
928            }
929        } else {
930            $params = array('theme'=>$this->name, 'type'=>'editor');
931            $url = new moodle_url('/theme/styles_debug.php', $params);
932        }
933        return $url;
934    }
935
936    /**
937     * Returns the content of the CSS to be used in editor content
938     *
939     * @return array
940     */
941    public function editor_css_files() {
942        $files = array();
943
944        // First editor plugins.
945        $plugins = core_component::get_plugin_list('editor');
946        foreach ($plugins as $plugin=>$fulldir) {
947            $sheetfile = "$fulldir/editor_styles.css";
948            if (is_readable($sheetfile)) {
949                $files['plugin_'.$plugin] = $sheetfile;
950            }
951        }
952        // Then parent themes - base first, the immediate parent last.
953        foreach (array_reverse($this->parent_configs) as $parent_config) {
954            if (empty($parent_config->editor_sheets)) {
955                continue;
956            }
957            foreach ($parent_config->editor_sheets as $sheet) {
958                $sheetfile = "$parent_config->dir/style/$sheet.css";
959                if (is_readable($sheetfile)) {
960                    $files['parent_'.$parent_config->name.'_'.$sheet] = $sheetfile;
961                }
962            }
963        }
964        // Finally this theme.
965        if (!empty($this->editor_sheets)) {
966            foreach ($this->editor_sheets as $sheet) {
967                $sheetfile = "$this->dir/style/$sheet.css";
968                if (is_readable($sheetfile)) {
969                    $files['theme_'.$sheet] = $sheetfile;
970                }
971            }
972        }
973
974        return $files;
975    }
976
977    /**
978     * Compiles and returns the content of the SCSS to be used in editor content
979     *
980     * @return string Compiled CSS from the editor SCSS
981     */
982    public function editor_scss_to_css() {
983        $css = '';
984        $dir = $this->dir;
985        $filenames = [];
986
987        // Use editor_scss file(s) provided by this theme if set.
988        if (!empty($this->editor_scss)) {
989            $filenames = $this->editor_scss;
990        } else {
991            // If no editor_scss set, move up theme hierarchy until one is found (if at all).
992            // This is so child themes only need to set editor_scss if an override is required.
993            foreach (array_reverse($this->parent_configs) as $parentconfig) {
994                if (!empty($parentconfig->editor_scss)) {
995                    $dir = $parentconfig->dir;
996                    $filenames = $parentconfig->editor_scss;
997
998                    // Config found, stop looking.
999                    break;
1000                }
1001            }
1002        }
1003
1004        if (!empty($filenames)) {
1005            $compiler = new core_scss();
1006
1007            foreach ($filenames as $filename) {
1008                $compiler->set_file("{$dir}/scss/{$filename}.scss");
1009
1010                try {
1011                    $css .= $compiler->to_css();
1012                } catch (\Exception $e) {
1013                    debugging('Error while compiling editor SCSS: ' . $e->getMessage(), DEBUG_DEVELOPER);
1014                }
1015            }
1016        }
1017
1018        return $css;
1019    }
1020
1021    /**
1022     * Get the stylesheet URL of this theme.
1023     *
1024     * @param moodle_page $page Not used... deprecated?
1025     * @return moodle_url[]
1026     */
1027    public function css_urls(moodle_page $page) {
1028        global $CFG;
1029
1030        $rev = theme_get_revision();
1031
1032        $urls = array();
1033
1034        $svg = $this->use_svg_icons();
1035        $separate = (core_useragent::is_ie() && !core_useragent::check_ie_version('10'));
1036
1037        if ($rev > -1) {
1038            $filename = right_to_left() ? 'all-rtl' : 'all';
1039            $url = new moodle_url("/theme/styles.php");
1040            $themesubrevision = theme_get_sub_revision_for_theme($this->name);
1041
1042            // Provide the sub revision to allow us to invalidate cached theme CSS
1043            // on a per theme basis, rather than globally.
1044            if ($themesubrevision && $themesubrevision > 0) {
1045                $rev .= "_{$themesubrevision}";
1046            }
1047
1048            if (!empty($CFG->slasharguments)) {
1049                $slashargs = '';
1050                if (!$svg) {
1051                    // We add a simple /_s to the start of the path.
1052                    // The underscore is used to ensure that it isn't a valid theme name.
1053                    $slashargs .= '/_s'.$slashargs;
1054                }
1055                $slashargs .= '/'.$this->name.'/'.$rev.'/'.$filename;
1056                if ($separate) {
1057                    $slashargs .= '/chunk0';
1058                }
1059                $url->set_slashargument($slashargs, 'noparam', true);
1060            } else {
1061                $params = array('theme' => $this->name, 'rev' => $rev, 'type' => $filename);
1062                if (!$svg) {
1063                    // We add an SVG param so that we know not to serve SVG images.
1064                    // We do this because all modern browsers support SVG and this param will one day be removed.
1065                    $params['svg'] = '0';
1066                }
1067                if ($separate) {
1068                    $params['chunk'] = '0';
1069                }
1070                $url->params($params);
1071            }
1072            $urls[] = $url;
1073
1074        } else {
1075            $baseurl = new moodle_url('/theme/styles_debug.php');
1076
1077            $css = $this->get_css_files(true);
1078            if (!$svg) {
1079                // We add an SVG param so that we know not to serve SVG images.
1080                // We do this because all modern browsers support SVG and this param will one day be removed.
1081                $baseurl->param('svg', '0');
1082            }
1083            if (right_to_left()) {
1084                $baseurl->param('rtl', 1);
1085            }
1086            if ($separate) {
1087                // We might need to chunk long files.
1088                $baseurl->param('chunk', '0');
1089            }
1090            if (core_useragent::is_ie()) {
1091                // Lalala, IE does not allow more than 31 linked CSS files from main document.
1092                $urls[] = new moodle_url($baseurl, array('theme'=>$this->name, 'type'=>'ie', 'subtype'=>'plugins'));
1093                foreach ($css['parents'] as $parent=>$sheets) {
1094                    // We need to serve parents individually otherwise we may easily exceed the style limit IE imposes (4096).
1095                    $urls[] = new moodle_url($baseurl, array('theme'=>$this->name,'type'=>'ie', 'subtype'=>'parents', 'sheet'=>$parent));
1096                }
1097                if ($this->get_scss_property()) {
1098                    // No need to define the type as IE here.
1099                    $urls[] = new moodle_url($baseurl, array('theme' => $this->name, 'type' => 'scss'));
1100                }
1101                $urls[] = new moodle_url($baseurl, array('theme'=>$this->name, 'type'=>'ie', 'subtype'=>'theme'));
1102
1103            } else {
1104                foreach ($css['plugins'] as $plugin=>$unused) {
1105                    $urls[] = new moodle_url($baseurl, array('theme'=>$this->name,'type'=>'plugin', 'subtype'=>$plugin));
1106                }
1107                foreach ($css['parents'] as $parent=>$sheets) {
1108                    foreach ($sheets as $sheet=>$unused2) {
1109                        $urls[] = new moodle_url($baseurl, array('theme'=>$this->name,'type'=>'parent', 'subtype'=>$parent, 'sheet'=>$sheet));
1110                    }
1111                }
1112                foreach ($css['theme'] as $sheet => $filename) {
1113                    if ($sheet === self::SCSS_KEY) {
1114                        // This is the theme SCSS file.
1115                        $urls[] = new moodle_url($baseurl, array('theme' => $this->name, 'type' => 'scss'));
1116                    } else {
1117                        // Sheet first in order to make long urls easier to read.
1118                        $urls[] = new moodle_url($baseurl, array('sheet'=>$sheet, 'theme'=>$this->name, 'type'=>'theme'));
1119                    }
1120                }
1121            }
1122        }
1123
1124        // Allow themes to change the css url to something like theme/mytheme/mycss.php.
1125        component_callback('theme_' . $this->name, 'alter_css_urls', [&$urls]);
1126        return $urls;
1127    }
1128
1129    /**
1130     * Get the whole css stylesheet for production mode.
1131     *
1132     * NOTE: this method is not expected to be used from any addons.
1133     *
1134     * @return string CSS markup compressed
1135     */
1136    public function get_css_content() {
1137
1138        $csscontent = '';
1139        foreach ($this->get_css_files(false) as $type => $value) {
1140            foreach ($value as $identifier => $val) {
1141                if (is_array($val)) {
1142                    foreach ($val as $v) {
1143                        $csscontent .= file_get_contents($v) . "\n";
1144                    }
1145                } else {
1146                    if ($type === 'theme' && $identifier === self::SCSS_KEY) {
1147                        // We need the content from SCSS because this is the SCSS file from the theme.
1148                        if ($compiled = $this->get_css_content_from_scss(false)) {
1149                            $csscontent .= $compiled;
1150                        } else {
1151                            // The compiler failed so default back to any precompiled css that might
1152                            // exist.
1153                            $csscontent .= $this->get_precompiled_css_content();
1154                        }
1155                    } else {
1156                        $csscontent .= file_get_contents($val) . "\n";
1157                    }
1158                }
1159            }
1160        }
1161        $csscontent = $this->post_process($csscontent);
1162        $csscontent = core_minify::css($csscontent);
1163
1164        return $csscontent;
1165    }
1166    /**
1167     * Set post processed CSS content cache.
1168     *
1169     * @param string $csscontent The post processed CSS content.
1170     * @return bool True if the content was successfully cached.
1171     */
1172    public function set_css_content_cache($csscontent) {
1173
1174        $cache = cache::make('core', 'postprocessedcss');
1175        $key = $this->get_css_cache_key();
1176
1177        return $cache->set($key, $csscontent);
1178    }
1179
1180    /**
1181     * Return whether the post processed CSS content has been cached.
1182     *
1183     * @return bool Whether the post-processed CSS is available in the cache.
1184     */
1185    public function has_css_cached_content() {
1186
1187        $key = $this->get_css_cache_key();
1188        $cache = cache::make('core', 'postprocessedcss');
1189
1190        return $cache->has($key);
1191    }
1192
1193    /**
1194     * Return cached post processed CSS content.
1195     *
1196     * @return bool|string The cached css content or false if not found.
1197     */
1198    public function get_css_cached_content() {
1199
1200        $key = $this->get_css_cache_key();
1201        $cache = cache::make('core', 'postprocessedcss');
1202
1203        return $cache->get($key);
1204    }
1205
1206    /**
1207     * Generate the css content cache key.
1208     *
1209     * @return string The post processed css cache key.
1210     */
1211    public function get_css_cache_key() {
1212        $nosvg = (!$this->use_svg_icons()) ? 'nosvg_' : '';
1213        $rtlmode = ($this->rtlmode == true) ? 'rtl' : 'ltr';
1214
1215        return $nosvg . $this->name . '_' . $rtlmode;
1216    }
1217
1218    /**
1219     * Get the theme designer css markup,
1220     * the parameters are coming from css_urls().
1221     *
1222     * NOTE: this method is not expected to be used from any addons.
1223     *
1224     * @param string $type
1225     * @param string $subtype
1226     * @param string $sheet
1227     * @return string CSS markup
1228     */
1229    public function get_css_content_debug($type, $subtype, $sheet) {
1230        if ($type === 'scss') {
1231            // The SCSS file of the theme is requested.
1232            $csscontent = $this->get_css_content_from_scss(true);
1233            if ($csscontent !== false) {
1234                return $this->post_process($csscontent);
1235            }
1236            return '';
1237        }
1238
1239        $cssfiles = array();
1240        $css = $this->get_css_files(true);
1241
1242        if ($type === 'ie') {
1243            // IE is a sloppy browser with weird limits, sorry.
1244            if ($subtype === 'plugins') {
1245                $cssfiles = $css['plugins'];
1246
1247            } else if ($subtype === 'parents') {
1248                if (empty($sheet)) {
1249                    // Do not bother with the empty parent here.
1250                } else {
1251                    // Build up the CSS for that parent so we can serve it as one file.
1252                    foreach ($css[$subtype][$sheet] as $parent => $css) {
1253                        $cssfiles[] = $css;
1254                    }
1255                }
1256            } else if ($subtype === 'theme') {
1257                $cssfiles = $css['theme'];
1258                foreach ($cssfiles as $key => $value) {
1259                    if (in_array($key, [self::SCSS_KEY])) {
1260                        // Remove the SCSS file from the theme CSS files.
1261                        // The SCSS files use the type 'scss', not 'ie'.
1262                        unset($cssfiles[$key]);
1263                    }
1264                }
1265            }
1266
1267        } else if ($type === 'plugin') {
1268            if (isset($css['plugins'][$subtype])) {
1269                $cssfiles[] = $css['plugins'][$subtype];
1270            }
1271
1272        } else if ($type === 'parent') {
1273            if (isset($css['parents'][$subtype][$sheet])) {
1274                $cssfiles[] = $css['parents'][$subtype][$sheet];
1275            }
1276
1277        } else if ($type === 'theme') {
1278            if (isset($css['theme'][$sheet])) {
1279                $cssfiles[] = $css['theme'][$sheet];
1280            }
1281        }
1282
1283        $csscontent = '';
1284        foreach ($cssfiles as $file) {
1285            $contents = file_get_contents($file);
1286            $contents = $this->post_process($contents);
1287            $comment = "/** Path: $type $subtype $sheet.' **/\n";
1288            $stats = '';
1289            $csscontent .= $comment.$stats.$contents."\n\n";
1290        }
1291
1292        return $csscontent;
1293    }
1294
1295    /**
1296     * Get the whole css stylesheet for editor iframe.
1297     *
1298     * NOTE: this method is not expected to be used from any addons.
1299     *
1300     * @return string CSS markup
1301     */
1302    public function get_css_content_editor() {
1303        $css = '';
1304        $cssfiles = $this->editor_css_files();
1305
1306        // If editor has static CSS, include it.
1307        foreach ($cssfiles as $file) {
1308            $css .= file_get_contents($file)."\n";
1309        }
1310
1311        // If editor has SCSS, compile and include it.
1312        if (($convertedscss = $this->editor_scss_to_css())) {
1313            $css .= $convertedscss;
1314        }
1315
1316        $output = $this->post_process($css);
1317
1318        return $output;
1319    }
1320
1321    /**
1322     * Returns an array of organised CSS files required for this output.
1323     *
1324     * @param bool $themedesigner
1325     * @return array nested array of file paths
1326     */
1327    protected function get_css_files($themedesigner) {
1328        global $CFG;
1329
1330        $cache = null;
1331        $cachekey = 'cssfiles';
1332        if ($themedesigner) {
1333            require_once($CFG->dirroot.'/lib/csslib.php');
1334            // We need some kind of caching here because otherwise the page navigation becomes
1335            // way too slow in theme designer mode. Feel free to create full cache definition later...
1336            $cache = cache::make_from_params(cache_store::MODE_APPLICATION, 'core', 'themedesigner', array('theme' => $this->name));
1337            if ($files = $cache->get($cachekey)) {
1338                if ($files['created'] > time() - THEME_DESIGNER_CACHE_LIFETIME) {
1339                    unset($files['created']);
1340                    return $files;
1341                }
1342            }
1343        }
1344
1345        $cssfiles = array('plugins'=>array(), 'parents'=>array(), 'theme'=>array());
1346
1347        // Get all plugin sheets.
1348        $excludes = $this->resolve_excludes('plugins_exclude_sheets');
1349        if ($excludes !== true) {
1350            foreach (core_component::get_plugin_types() as $type=>$unused) {
1351                if ($type === 'theme' || (!empty($excludes[$type]) and $excludes[$type] === true)) {
1352                    continue;
1353                }
1354                $plugins = core_component::get_plugin_list($type);
1355                foreach ($plugins as $plugin=>$fulldir) {
1356                    if (!empty($excludes[$type]) and is_array($excludes[$type])
1357                            and in_array($plugin, $excludes[$type])) {
1358                        continue;
1359                    }
1360
1361                    // Get the CSS from the plugin.
1362                    $sheetfile = "$fulldir/styles.css";
1363                    if (is_readable($sheetfile)) {
1364                        $cssfiles['plugins'][$type.'_'.$plugin] = $sheetfile;
1365                    }
1366
1367                    // Create a list of candidate sheets from parents (direct parent last) and current theme.
1368                    $candidates = array();
1369                    foreach (array_reverse($this->parent_configs) as $parent_config) {
1370                        $candidates[] = $parent_config->name;
1371                    }
1372                    $candidates[] = $this->name;
1373
1374                    // Add the sheets found.
1375                    foreach ($candidates as $candidate) {
1376                        $sheetthemefile = "$fulldir/styles_{$candidate}.css";
1377                        if (is_readable($sheetthemefile)) {
1378                            $cssfiles['plugins'][$type.'_'.$plugin.'_'.$candidate] = $sheetthemefile;
1379                        }
1380                    }
1381                }
1382            }
1383        }
1384
1385        // Find out wanted parent sheets.
1386        $excludes = $this->resolve_excludes('parents_exclude_sheets');
1387        if ($excludes !== true) {
1388            foreach (array_reverse($this->parent_configs) as $parent_config) { // Base first, the immediate parent last.
1389                $parent = $parent_config->name;
1390                if (empty($parent_config->sheets) || (!empty($excludes[$parent]) and $excludes[$parent] === true)) {
1391                    continue;
1392                }
1393                foreach ($parent_config->sheets as $sheet) {
1394                    if (!empty($excludes[$parent]) && is_array($excludes[$parent])
1395                            && in_array($sheet, $excludes[$parent])) {
1396                        continue;
1397                    }
1398
1399                    // We never refer to the parent LESS files.
1400                    $sheetfile = "$parent_config->dir/style/$sheet.css";
1401                    if (is_readable($sheetfile)) {
1402                        $cssfiles['parents'][$parent][$sheet] = $sheetfile;
1403                    }
1404                }
1405            }
1406        }
1407
1408
1409        // Current theme sheets.
1410        // We first add the SCSS file because we want the CSS ones to
1411        // be included after the SCSS code.
1412        if ($this->get_scss_property()) {
1413            $cssfiles['theme'][self::SCSS_KEY] = true;
1414        }
1415        if (is_array($this->sheets)) {
1416            foreach ($this->sheets as $sheet) {
1417                $sheetfile = "$this->dir/style/$sheet.css";
1418                if (is_readable($sheetfile) && !isset($cssfiles['theme'][$sheet])) {
1419                    $cssfiles['theme'][$sheet] = $sheetfile;
1420                }
1421            }
1422        }
1423
1424        if ($cache) {
1425            $files = $cssfiles;
1426            $files['created'] = time();
1427            $cache->set($cachekey, $files);
1428        }
1429        return $cssfiles;
1430    }
1431
1432    /**
1433     * Return the CSS content generated from the SCSS file.
1434     *
1435     * @param bool $themedesigner True if theme designer is enabled.
1436     * @return bool|string Return false when the compilation failed. Else the compiled string.
1437     */
1438    protected function get_css_content_from_scss($themedesigner) {
1439        global $CFG;
1440
1441        list($paths, $scss) = $this->get_scss_property();
1442        if (!$scss) {
1443            throw new coding_exception('The theme did not define a SCSS file, or it is not readable.');
1444        }
1445
1446        // We might need more memory/time to do this, so let's play safe.
1447        raise_memory_limit(MEMORY_EXTRA);
1448        core_php_time_limit::raise(300);
1449
1450        // TODO: MDL-62757 When changing anything in this method please do not forget to check
1451        // if the validate() method in class admin_setting_configthemepreset needs updating too.
1452
1453        $cachedir = make_localcache_directory('scsscache-' . $this->name, false);
1454        $cacheoptions = [];
1455        if ($themedesigner) {
1456            $cacheoptions = array(
1457                  'cacheDir' => $cachedir,
1458                  'prefix' => 'scssphp_',
1459                  'forceRefresh' => false,
1460            );
1461        } else {
1462            if (file_exists($cachedir)) {
1463                remove_dir($cachedir);
1464            }
1465        }
1466
1467        // Set-up the compiler.
1468        $compiler = new core_scss($cacheoptions);
1469
1470        if ($this->supports_source_maps($themedesigner)) {
1471            // Enable source maps.
1472            $compiler->setSourceMapOptions([
1473                'sourceMapBasepath' => str_replace('\\', '/', $CFG->dirroot),
1474                'sourceMapRootpath' => $CFG->wwwroot . '/'
1475            ]);
1476            $compiler->setSourceMap($compiler::SOURCE_MAP_INLINE);
1477        }
1478
1479        $compiler->prepend_raw_scss($this->get_pre_scss_code());
1480        if (is_string($scss)) {
1481            $compiler->set_file($scss);
1482        } else {
1483            $compiler->append_raw_scss($scss($this));
1484            $compiler->setImportPaths($paths);
1485        }
1486        $compiler->append_raw_scss($this->get_extra_scss_code());
1487
1488        try {
1489            // Compile!
1490            $compiled = $compiler->to_css();
1491
1492        } catch (\Exception $e) {
1493            $compiled = false;
1494            debugging('Error while compiling SCSS: ' . $e->getMessage(), DEBUG_DEVELOPER);
1495        }
1496
1497        // Try to save memory.
1498        $compiler = null;
1499        unset($compiler);
1500
1501        return $compiled;
1502    }
1503
1504    /**
1505     * Return the precompiled CSS if the precompiledcsscallback exists.
1506     *
1507     * @return string Return compiled css.
1508     */
1509    public function get_precompiled_css_content() {
1510        $configs = array_reverse($this->parent_configs) + [$this];
1511        $css = '';
1512
1513        foreach ($configs as $config) {
1514            if (isset($config->precompiledcsscallback)) {
1515                $function = $config->precompiledcsscallback;
1516                if (function_exists($function)) {
1517                    $css .= $function($this);
1518                }
1519            }
1520        }
1521        return $css;
1522    }
1523
1524    /**
1525     * Get the icon system to use.
1526     *
1527     * @return string
1528     */
1529    public function get_icon_system() {
1530
1531        // Getting all the candidate functions.
1532        $system = false;
1533        if (isset($this->iconsystem) && \core\output\icon_system::is_valid_system($this->iconsystem)) {
1534            return $this->iconsystem;
1535        }
1536        foreach ($this->parent_configs as $parent_config) {
1537            if (isset($parent_config->iconsystem) && \core\output\icon_system::is_valid_system($parent_config->iconsystem)) {
1538                return $parent_config->iconsystem;
1539            }
1540        }
1541        return \core\output\icon_system::STANDARD;
1542    }
1543
1544    /**
1545     * Return extra SCSS code to add when compiling.
1546     *
1547     * This is intended to be used by themes to inject some SCSS code
1548     * before it gets compiled. If you want to inject variables you
1549     * should use {@link self::get_scss_variables()}.
1550     *
1551     * @return string The SCSS code to inject.
1552     */
1553    public function get_extra_scss_code() {
1554        $content = '';
1555
1556        // Getting all the candidate functions.
1557        $candidates = array();
1558        foreach ($this->parent_configs as $parent_config) {
1559            if (!isset($parent_config->extrascsscallback)) {
1560                continue;
1561            }
1562            $candidates[] = $parent_config->extrascsscallback;
1563        }
1564        $candidates[] = $this->extrascsscallback;
1565
1566        // Calling the functions.
1567        foreach ($candidates as $function) {
1568            if (function_exists($function)) {
1569                $content .= "\n/** Extra SCSS from $function **/\n" . $function($this) . "\n";
1570            }
1571        }
1572
1573        return $content;
1574    }
1575
1576    /**
1577     * SCSS code to prepend when compiling.
1578     *
1579     * This is intended to be used by themes to inject SCSS code before it gets compiled.
1580     *
1581     * @return string The SCSS code to inject.
1582     */
1583    public function get_pre_scss_code() {
1584        $content = '';
1585
1586        // Getting all the candidate functions.
1587        $candidates = array();
1588        foreach ($this->parent_configs as $parent_config) {
1589            if (!isset($parent_config->prescsscallback)) {
1590                continue;
1591            }
1592            $candidates[] = $parent_config->prescsscallback;
1593        }
1594        $candidates[] = $this->prescsscallback;
1595
1596        // Calling the functions.
1597        foreach ($candidates as $function) {
1598            if (function_exists($function)) {
1599                $content .= "\n/** Pre-SCSS from $function **/\n" . $function($this) . "\n";
1600            }
1601        }
1602
1603        return $content;
1604    }
1605
1606    /**
1607     * Get the SCSS property.
1608     *
1609     * This resolves whether a SCSS file (or content) has to be used when generating
1610     * the stylesheet for the theme. It will look at parents themes and check the
1611     * SCSS properties there.
1612     *
1613     * @return False when SCSS is not used.
1614     *         An array with the import paths, and the path to the SCSS file or Closure as second.
1615     */
1616    public function get_scss_property() {
1617        if ($this->scsscache === null) {
1618            $configs = [$this] + $this->parent_configs;
1619            $scss = null;
1620
1621            foreach ($configs as $config) {
1622                $path = "{$config->dir}/scss";
1623
1624                // We collect the SCSS property until we've found one.
1625                if (empty($scss) && !empty($config->scss)) {
1626                    $candidate = is_string($config->scss) ? "{$path}/{$config->scss}.scss" : $config->scss;
1627                    if ($candidate instanceof Closure) {
1628                        $scss = $candidate;
1629                    } else if (is_string($candidate) && is_readable($candidate)) {
1630                        $scss = $candidate;
1631                    }
1632                }
1633
1634                // We collect the import paths once we've found a SCSS property.
1635                if ($scss && is_dir($path)) {
1636                    $paths[] = $path;
1637                }
1638
1639            }
1640
1641            $this->scsscache = $scss !== null ? [$paths, $scss] : false;
1642        }
1643
1644        return $this->scsscache;
1645    }
1646
1647    /**
1648     * Generate a URL to the file that serves theme JavaScript files.
1649     *
1650     * If we determine that the theme has no relevant files, then we return
1651     * early with a null value.
1652     *
1653     * @param bool $inhead true means head url, false means footer
1654     * @return moodle_url|null
1655     */
1656    public function javascript_url($inhead) {
1657        global $CFG;
1658
1659        $rev = theme_get_revision();
1660        $params = array('theme'=>$this->name,'rev'=>$rev);
1661        $params['type'] = $inhead ? 'head' : 'footer';
1662
1663        // Return early if there are no files to serve
1664        if (count($this->javascript_files($params['type'])) === 0) {
1665            return null;
1666        }
1667
1668        if (!empty($CFG->slasharguments) and $rev > 0) {
1669            $url = new moodle_url("/theme/javascript.php");
1670            $url->set_slashargument('/'.$this->name.'/'.$rev.'/'.$params['type'], 'noparam', true);
1671            return $url;
1672        } else {
1673            return new moodle_url('/theme/javascript.php', $params);
1674        }
1675    }
1676
1677    /**
1678     * Get the URL's for the JavaScript files used by this theme.
1679     * They won't be served directly, instead they'll be mediated through
1680     * theme/javascript.php.
1681     *
1682     * @param string $type Either javascripts_footer, or javascripts
1683     * @return array
1684     */
1685    public function javascript_files($type) {
1686        if ($type === 'footer') {
1687            $type = 'javascripts_footer';
1688        } else {
1689            $type = 'javascripts';
1690        }
1691
1692        $js = array();
1693        // find out wanted parent javascripts
1694        $excludes = $this->resolve_excludes('parents_exclude_javascripts');
1695        if ($excludes !== true) {
1696            foreach (array_reverse($this->parent_configs) as $parent_config) { // base first, the immediate parent last
1697                $parent = $parent_config->name;
1698                if (empty($parent_config->$type)) {
1699                    continue;
1700                }
1701                if (!empty($excludes[$parent]) and $excludes[$parent] === true) {
1702                    continue;
1703                }
1704                foreach ($parent_config->$type as $javascript) {
1705                    if (!empty($excludes[$parent]) and is_array($excludes[$parent])
1706                        and in_array($javascript, $excludes[$parent])) {
1707                        continue;
1708                    }
1709                    $javascriptfile = "$parent_config->dir/javascript/$javascript.js";
1710                    if (is_readable($javascriptfile)) {
1711                        $js[] = $javascriptfile;
1712                    }
1713                }
1714            }
1715        }
1716
1717        // current theme javascripts
1718        if (is_array($this->$type)) {
1719            foreach ($this->$type as $javascript) {
1720                $javascriptfile = "$this->dir/javascript/$javascript.js";
1721                if (is_readable($javascriptfile)) {
1722                    $js[] = $javascriptfile;
1723                }
1724            }
1725        }
1726        return $js;
1727    }
1728
1729    /**
1730     * Resolves an exclude setting to the themes setting is applicable or the
1731     * setting of its closest parent.
1732     *
1733     * @param string $variable The name of the setting the exclude setting to resolve
1734     * @param string $default
1735     * @return mixed
1736     */
1737    protected function resolve_excludes($variable, $default = null) {
1738        $setting = $default;
1739        if (is_array($this->{$variable}) or $this->{$variable} === true) {
1740            $setting = $this->{$variable};
1741        } else {
1742            foreach ($this->parent_configs as $parent_config) { // the immediate parent first, base last
1743                if (!isset($parent_config->{$variable})) {
1744                    continue;
1745                }
1746                if (is_array($parent_config->{$variable}) or $parent_config->{$variable} === true) {
1747                    $setting = $parent_config->{$variable};
1748                    break;
1749                }
1750            }
1751        }
1752        return $setting;
1753    }
1754
1755    /**
1756     * Returns the content of the one huge javascript file merged from all theme javascript files.
1757     *
1758     * @param bool $type
1759     * @return string
1760     */
1761    public function javascript_content($type) {
1762        $jsfiles = $this->javascript_files($type);
1763        $js = '';
1764        foreach ($jsfiles as $jsfile) {
1765            $js .= file_get_contents($jsfile)."\n";
1766        }
1767        return $js;
1768    }
1769
1770    /**
1771     * Post processes CSS.
1772     *
1773     * This method post processes all of the CSS before it is served for this theme.
1774     * This is done so that things such as image URL's can be swapped in and to
1775     * run any specific CSS post process method the theme has requested.
1776     * This allows themes to use CSS settings.
1777     *
1778     * @param string $css The CSS to process.
1779     * @return string The processed CSS.
1780     */
1781    public function post_process($css) {
1782        // now resolve all image locations
1783        if (preg_match_all('/\[\[pix:([a-z0-9_]+\|)?([^\]]+)\]\]/', $css, $matches, PREG_SET_ORDER)) {
1784            $replaced = array();
1785            foreach ($matches as $match) {
1786                if (isset($replaced[$match[0]])) {
1787                    continue;
1788                }
1789                $replaced[$match[0]] = true;
1790                $imagename = $match[2];
1791                $component = rtrim($match[1], '|');
1792                $imageurl = $this->image_url($imagename, $component)->out(false);
1793                 // we do not need full url because the image.php is always in the same dir
1794                $imageurl = preg_replace('|^http.?://[^/]+|', '', $imageurl);
1795                $css = str_replace($match[0], $imageurl, $css);
1796            }
1797        }
1798
1799        // Now resolve all font locations.
1800        if (preg_match_all('/\[\[font:([a-z0-9_]+\|)?([^\]]+)\]\]/', $css, $matches, PREG_SET_ORDER)) {
1801            $replaced = array();
1802            foreach ($matches as $match) {
1803                if (isset($replaced[$match[0]])) {
1804                    continue;
1805                }
1806                $replaced[$match[0]] = true;
1807                $fontname = $match[2];
1808                $component = rtrim($match[1], '|');
1809                $fonturl = $this->font_url($fontname, $component)->out(false);
1810                // We do not need full url because the font.php is always in the same dir.
1811                $fonturl = preg_replace('|^http.?://[^/]+|', '', $fonturl);
1812                $css = str_replace($match[0], $fonturl, $css);
1813            }
1814        }
1815
1816        // Now resolve all theme settings or do any other postprocessing.
1817        // This needs to be done before calling core parser, since the parser strips [[settings]] tags.
1818        $csspostprocess = $this->csspostprocess;
1819        if (function_exists($csspostprocess)) {
1820            $css = $csspostprocess($css, $this);
1821        }
1822
1823        // Post processing using an object representation of CSS.
1824        $treeprocessor = $this->get_css_tree_post_processor();
1825        $needsparsing = !empty($treeprocessor) || !empty($this->rtlmode);
1826        if ($needsparsing) {
1827
1828            // We might need more memory/time to do this, so let's play safe.
1829            raise_memory_limit(MEMORY_EXTRA);
1830            core_php_time_limit::raise(300);
1831
1832            $parser = new core_cssparser($css);
1833            $csstree = $parser->parse();
1834            unset($parser);
1835
1836            if ($this->rtlmode) {
1837                $this->rtlize($csstree);
1838            }
1839
1840            if ($treeprocessor) {
1841                $treeprocessor($csstree, $this);
1842            }
1843
1844            $css = $csstree->render();
1845            unset($csstree);
1846        }
1847
1848        return $css;
1849    }
1850
1851    /**
1852     * Flip a stylesheet to RTL.
1853     *
1854     * @param Object $csstree The parsed CSS tree structure to flip.
1855     * @return void
1856     */
1857    protected function rtlize($csstree) {
1858        $rtlcss = new core_rtlcss($csstree);
1859        $rtlcss->flip();
1860    }
1861
1862    /**
1863     * Return the direct URL for an image from the pix folder.
1864     *
1865     * Use this function sparingly and never for icons. For icons use pix_icon or the pix helper in a mustache template.
1866     *
1867     * @deprecated since Moodle 3.3
1868     * @param string $imagename the name of the icon.
1869     * @param string $component specification of one plugin like in get_string()
1870     * @return moodle_url
1871     */
1872    public function pix_url($imagename, $component) {
1873        debugging('pix_url is deprecated. Use image_url for images and pix_icon for icons.', DEBUG_DEVELOPER);
1874        return $this->image_url($imagename, $component);
1875    }
1876
1877    /**
1878     * Return the direct URL for an image from the pix folder.
1879     *
1880     * Use this function sparingly and never for icons. For icons use pix_icon or the pix helper in a mustache template.
1881     *
1882     * @param string $imagename the name of the icon.
1883     * @param string $component specification of one plugin like in get_string()
1884     * @return moodle_url
1885     */
1886    public function image_url($imagename, $component) {
1887        global $CFG;
1888
1889        $params = array('theme'=>$this->name);
1890        $svg = $this->use_svg_icons();
1891
1892        if (empty($component) or $component === 'moodle' or $component === 'core') {
1893            $params['component'] = 'core';
1894        } else {
1895            $params['component'] = $component;
1896        }
1897
1898        $rev = theme_get_revision();
1899        if ($rev != -1) {
1900            $params['rev'] = $rev;
1901        }
1902
1903        $params['image'] = $imagename;
1904
1905        $url = new moodle_url("/theme/image.php");
1906        if (!empty($CFG->slasharguments) and $rev > 0) {
1907            $path = '/'.$params['theme'].'/'.$params['component'].'/'.$params['rev'].'/'.$params['image'];
1908            if (!$svg) {
1909                // We add a simple /_s to the start of the path.
1910                // The underscore is used to ensure that it isn't a valid theme name.
1911                $path = '/_s'.$path;
1912            }
1913            $url->set_slashargument($path, 'noparam', true);
1914        } else {
1915            if (!$svg) {
1916                // We add an SVG param so that we know not to serve SVG images.
1917                // We do this because all modern browsers support SVG and this param will one day be removed.
1918                $params['svg'] = '0';
1919            }
1920            $url->params($params);
1921        }
1922
1923        return $url;
1924    }
1925
1926    /**
1927     * Return the URL for a font
1928     *
1929     * @param string $font the name of the font (including extension).
1930     * @param string $component specification of one plugin like in get_string()
1931     * @return moodle_url
1932     */
1933    public function font_url($font, $component) {
1934        global $CFG;
1935
1936        $params = array('theme'=>$this->name);
1937
1938        if (empty($component) or $component === 'moodle' or $component === 'core') {
1939            $params['component'] = 'core';
1940        } else {
1941            $params['component'] = $component;
1942        }
1943
1944        $rev = theme_get_revision();
1945        if ($rev != -1) {
1946            $params['rev'] = $rev;
1947        }
1948
1949        $params['font'] = $font;
1950
1951        $url = new moodle_url("/theme/font.php");
1952        if (!empty($CFG->slasharguments) and $rev > 0) {
1953            $path = '/'.$params['theme'].'/'.$params['component'].'/'.$params['rev'].'/'.$params['font'];
1954            $url->set_slashargument($path, 'noparam', true);
1955        } else {
1956            $url->params($params);
1957        }
1958
1959        return $url;
1960    }
1961
1962    /**
1963     * Returns URL to the stored file via pluginfile.php.
1964     *
1965     * Note the theme must also implement pluginfile.php handler,
1966     * theme revision is used instead of the itemid.
1967     *
1968     * @param string $setting
1969     * @param string $filearea
1970     * @return string protocol relative URL or null if not present
1971     */
1972    public function setting_file_url($setting, $filearea) {
1973        global $CFG;
1974
1975        if (empty($this->settings->$setting)) {
1976            return null;
1977        }
1978
1979        $component = 'theme_'.$this->name;
1980        $itemid = theme_get_revision();
1981        $filepath = $this->settings->$setting;
1982        $syscontext = context_system::instance();
1983
1984        $url = moodle_url::make_file_url("$CFG->wwwroot/pluginfile.php", "/$syscontext->id/$component/$filearea/$itemid".$filepath);
1985
1986        // Now this is tricky because the we can not hardcode http or https here, lets use the relative link.
1987        // Note: unfortunately moodle_url does not support //urls yet.
1988
1989        $url = preg_replace('|^https?://|i', '//', $url->out(false));
1990
1991        return $url;
1992    }
1993
1994    /**
1995     * Serve the theme setting file.
1996     *
1997     * @param string $filearea
1998     * @param array $args
1999     * @param bool $forcedownload
2000     * @param array $options
2001     * @return bool may terminate if file not found or donotdie not specified
2002     */
2003    public function setting_file_serve($filearea, $args, $forcedownload, $options) {
2004        global $CFG;
2005        require_once("$CFG->libdir/filelib.php");
2006
2007        $syscontext = context_system::instance();
2008        $component = 'theme_'.$this->name;
2009
2010        $revision = array_shift($args);
2011        if ($revision < 0) {
2012            $lifetime = 0;
2013        } else {
2014            $lifetime = 60*60*24*60;
2015            // By default, theme files must be cache-able by both browsers and proxies.
2016            if (!array_key_exists('cacheability', $options)) {
2017                $options['cacheability'] = 'public';
2018            }
2019        }
2020
2021        $fs = get_file_storage();
2022        $relativepath = implode('/', $args);
2023
2024        $fullpath = "/{$syscontext->id}/{$component}/{$filearea}/0/{$relativepath}";
2025        $fullpath = rtrim($fullpath, '/');
2026        if ($file = $fs->get_file_by_hash(sha1($fullpath))) {
2027            send_stored_file($file, $lifetime, 0, $forcedownload, $options);
2028            return true;
2029        } else {
2030            send_file_not_found();
2031        }
2032    }
2033
2034    /**
2035     * Resolves the real image location.
2036     *
2037     * $svg was introduced as an arg in 2.4. It is important because not all supported browsers support the use of SVG
2038     * and we need a way in which to turn it off.
2039     * By default SVG won't be used unless asked for. This is done for two reasons:
2040     *   1. It ensures that we don't serve svg images unless we really want to. The admin has selected to force them, of the users
2041     *      browser supports SVG.
2042     *   2. We only serve SVG images from locations we trust. This must NOT include any areas where the image may have been uploaded
2043     *      by the user due to security concerns.
2044     *
2045     * @param string $image name of image, may contain relative path
2046     * @param string $component
2047     * @param bool $svg|null Should SVG images also be looked for? If null, resorts to $CFG->svgicons if that is set; falls back to
2048     * auto-detection of browser support otherwise
2049     * @return string full file path
2050     */
2051    public function resolve_image_location($image, $component, $svg = false) {
2052        global $CFG;
2053
2054        if (!is_bool($svg)) {
2055            // If $svg isn't a bool then we need to decide for ourselves.
2056            $svg = $this->use_svg_icons();
2057        }
2058
2059        if ($component === 'moodle' or $component === 'core' or empty($component)) {
2060            if ($imagefile = $this->image_exists("$this->dir/pix_core/$image", $svg)) {
2061                return $imagefile;
2062            }
2063            foreach (array_reverse($this->parent_configs) as $parent_config) { // base first, the immediate parent last
2064                if ($imagefile = $this->image_exists("$parent_config->dir/pix_core/$image", $svg)) {
2065                    return $imagefile;
2066                }
2067            }
2068            if ($imagefile = $this->image_exists("$CFG->dataroot/pix/$image", $svg)) {
2069                return $imagefile;
2070            }
2071            if ($imagefile = $this->image_exists("$CFG->dirroot/pix/$image", $svg)) {
2072                return $imagefile;
2073            }
2074            return null;
2075
2076        } else if ($component === 'theme') { //exception
2077            if ($image === 'favicon') {
2078                return "$this->dir/pix/favicon.ico";
2079            }
2080            if ($imagefile = $this->image_exists("$this->dir/pix/$image", $svg)) {
2081                return $imagefile;
2082            }
2083            foreach (array_reverse($this->parent_configs) as $parent_config) { // base first, the immediate parent last
2084                if ($imagefile = $this->image_exists("$parent_config->dir/pix/$image", $svg)) {
2085                    return $imagefile;
2086                }
2087            }
2088            return null;
2089
2090        } else {
2091            if (strpos($component, '_') === false) {
2092                $component = 'mod_'.$component;
2093            }
2094            list($type, $plugin) = explode('_', $component, 2);
2095
2096            if ($imagefile = $this->image_exists("$this->dir/pix_plugins/$type/$plugin/$image", $svg)) {
2097                return $imagefile;
2098            }
2099            foreach (array_reverse($this->parent_configs) as $parent_config) { // base first, the immediate parent last
2100                if ($imagefile = $this->image_exists("$parent_config->dir/pix_plugins/$type/$plugin/$image", $svg)) {
2101                    return $imagefile;
2102                }
2103            }
2104            if ($imagefile = $this->image_exists("$CFG->dataroot/pix_plugins/$type/$plugin/$image", $svg)) {
2105                return $imagefile;
2106            }
2107            $dir = core_component::get_plugin_directory($type, $plugin);
2108            if ($imagefile = $this->image_exists("$dir/pix/$image", $svg)) {
2109                return $imagefile;
2110            }
2111            return null;
2112        }
2113    }
2114
2115    /**
2116     * Resolves the real font location.
2117     *
2118     * @param string $font name of font file
2119     * @param string $component
2120     * @return string full file path
2121     */
2122    public function resolve_font_location($font, $component) {
2123        global $CFG;
2124
2125        if ($component === 'moodle' or $component === 'core' or empty($component)) {
2126            if (file_exists("$this->dir/fonts_core/$font")) {
2127                return "$this->dir/fonts_core/$font";
2128            }
2129            foreach (array_reverse($this->parent_configs) as $parent_config) { // Base first, the immediate parent last.
2130                if (file_exists("$parent_config->dir/fonts_core/$font")) {
2131                    return "$parent_config->dir/fonts_core/$font";
2132                }
2133            }
2134            if (file_exists("$CFG->dataroot/fonts/$font")) {
2135                return "$CFG->dataroot/fonts/$font";
2136            }
2137            if (file_exists("$CFG->dirroot/lib/fonts/$font")) {
2138                return "$CFG->dirroot/lib/fonts/$font";
2139            }
2140            return null;
2141
2142        } else if ($component === 'theme') { // Exception.
2143            if (file_exists("$this->dir/fonts/$font")) {
2144                return "$this->dir/fonts/$font";
2145            }
2146            foreach (array_reverse($this->parent_configs) as $parent_config) { // Base first, the immediate parent last.
2147                if (file_exists("$parent_config->dir/fonts/$font")) {
2148                    return "$parent_config->dir/fonts/$font";
2149                }
2150            }
2151            return null;
2152
2153        } else {
2154            if (strpos($component, '_') === false) {
2155                $component = 'mod_'.$component;
2156            }
2157            list($type, $plugin) = explode('_', $component, 2);
2158
2159            if (file_exists("$this->dir/fonts_plugins/$type/$plugin/$font")) {
2160                return "$this->dir/fonts_plugins/$type/$plugin/$font";
2161            }
2162            foreach (array_reverse($this->parent_configs) as $parent_config) { // Base first, the immediate parent last.
2163                if (file_exists("$parent_config->dir/fonts_plugins/$type/$plugin/$font")) {
2164                    return "$parent_config->dir/fonts_plugins/$type/$plugin/$font";
2165                }
2166            }
2167            if (file_exists("$CFG->dataroot/fonts_plugins/$type/$plugin/$font")) {
2168                return "$CFG->dataroot/fonts_plugins/$type/$plugin/$font";
2169            }
2170            $dir = core_component::get_plugin_directory($type, $plugin);
2171            if (file_exists("$dir/fonts/$font")) {
2172                return "$dir/fonts/$font";
2173            }
2174            return null;
2175        }
2176    }
2177
2178    /**
2179     * Return true if we should look for SVG images as well.
2180     *
2181     * @return bool
2182     */
2183    public function use_svg_icons() {
2184        global $CFG;
2185        if ($this->usesvg === null) {
2186
2187            if (!isset($CFG->svgicons)) {
2188                $this->usesvg = core_useragent::supports_svg();
2189            } else {
2190                // Force them on/off depending upon the setting.
2191                $this->usesvg = (bool)$CFG->svgicons;
2192            }
2193        }
2194        return $this->usesvg;
2195    }
2196
2197    /**
2198     * Forces the usesvg setting to either true or false, avoiding any decision making.
2199     *
2200     * This function should only ever be used when absolutely required, and before any generation of image URL's has occurred.
2201     * DO NOT ABUSE THIS FUNCTION... not that you'd want to right ;)
2202     *
2203     * @param bool $setting True to force the use of svg when available, null otherwise.
2204     */
2205    public function force_svg_use($setting) {
2206        $this->usesvg = (bool)$setting;
2207    }
2208
2209    /**
2210     * Set to be in RTL mode.
2211     *
2212     * This will likely be used when post processing the CSS before serving it.
2213     *
2214     * @param bool $inrtl True when in RTL mode.
2215     */
2216    public function set_rtl_mode($inrtl = true) {
2217        $this->rtlmode = $inrtl;
2218    }
2219
2220    /**
2221     * Checks if source maps are supported
2222     *
2223     * @param bool $themedesigner True if theme designer is enabled.
2224     * @return boolean True if source maps are supported.
2225     */
2226    public function supports_source_maps($themedesigner): bool {
2227        if (empty($this->rtlmode) && $themedesigner) {
2228            return true;
2229        }
2230        return false;
2231    }
2232
2233    /**
2234     * Whether the theme is being served in RTL mode.
2235     *
2236     * @return bool True when in RTL mode.
2237     */
2238    public function get_rtl_mode() {
2239        return $this->rtlmode;
2240    }
2241
2242    /**
2243     * Checks if file with any image extension exists.
2244     *
2245     * The order to these images was adjusted prior to the release of 2.4
2246     * At that point the were the following image counts in Moodle core:
2247     *
2248     *     - png = 667 in pix dirs (1499 total)
2249     *     - gif = 385 in pix dirs (606 total)
2250     *     - jpg = 62  in pix dirs (74 total)
2251     *     - jpeg = 0  in pix dirs (1 total)
2252     *
2253     * There is work in progress to move towards SVG presently hence that has been prioritiesed.
2254     *
2255     * @param string $filepath
2256     * @param bool $svg If set to true SVG images will also be looked for.
2257     * @return string image name with extension
2258     */
2259    private static function image_exists($filepath, $svg = false) {
2260        if ($svg && file_exists("$filepath.svg")) {
2261            return "$filepath.svg";
2262        } else  if (file_exists("$filepath.png")) {
2263            return "$filepath.png";
2264        } else if (file_exists("$filepath.gif")) {
2265            return "$filepath.gif";
2266        } else  if (file_exists("$filepath.jpg")) {
2267            return "$filepath.jpg";
2268        } else  if (file_exists("$filepath.jpeg")) {
2269            return "$filepath.jpeg";
2270        } else {
2271            return false;
2272        }
2273    }
2274
2275    /**
2276     * Loads the theme config from config.php file.
2277     *
2278     * @param string $themename
2279     * @param stdClass $settings from config_plugins table
2280     * @param boolean $parentscheck true to also check the parents.    .
2281     * @return stdClass The theme configuration
2282     */
2283    private static function find_theme_config($themename, $settings, $parentscheck = true) {
2284        // We have to use the variable name $THEME (upper case) because that
2285        // is what is used in theme config.php files.
2286
2287        if (!$dir = theme_config::find_theme_location($themename)) {
2288            return null;
2289        }
2290
2291        $THEME = new stdClass();
2292        $THEME->name     = $themename;
2293        $THEME->dir      = $dir;
2294        $THEME->settings = $settings;
2295
2296        global $CFG; // just in case somebody tries to use $CFG in theme config
2297        include("$THEME->dir/config.php");
2298
2299        // verify the theme configuration is OK
2300        if (!is_array($THEME->parents)) {
2301            // parents option is mandatory now
2302            return null;
2303        } else {
2304            // We use $parentscheck to only check the direct parents (avoid infinite loop).
2305            if ($parentscheck) {
2306                // Find all parent theme configs.
2307                foreach ($THEME->parents as $parent) {
2308                    $parentconfig = theme_config::find_theme_config($parent, $settings, false);
2309                    if (empty($parentconfig)) {
2310                        return null;
2311                    }
2312                }
2313            }
2314        }
2315
2316        return $THEME;
2317    }
2318
2319    /**
2320     * Finds the theme location and verifies the theme has all needed files
2321     * and is not obsoleted.
2322     *
2323     * @param string $themename
2324     * @return string full dir path or null if not found
2325     */
2326    private static function find_theme_location($themename) {
2327        global $CFG;
2328
2329        if (file_exists("$CFG->dirroot/theme/$themename/config.php")) {
2330            $dir = "$CFG->dirroot/theme/$themename";
2331
2332        } else if (!empty($CFG->themedir) and file_exists("$CFG->themedir/$themename/config.php")) {
2333            $dir = "$CFG->themedir/$themename";
2334
2335        } else {
2336            return null;
2337        }
2338
2339        if (file_exists("$dir/styles.php")) {
2340            //legacy theme - needs to be upgraded - upgrade info is displayed on the admin settings page
2341            return null;
2342        }
2343
2344        return $dir;
2345    }
2346
2347    /**
2348     * Get the renderer for a part of Moodle for this theme.
2349     *
2350     * @param moodle_page $page the page we are rendering
2351     * @param string $component the name of part of moodle. E.g. 'core', 'quiz', 'qtype_multichoice'.
2352     * @param string $subtype optional subtype such as 'news' resulting to 'mod_forum_news'
2353     * @param string $target one of rendering target constants
2354     * @return renderer_base the requested renderer.
2355     */
2356    public function get_renderer(moodle_page $page, $component, $subtype = null, $target = null) {
2357        if (is_null($this->rf)) {
2358            $classname = $this->rendererfactory;
2359            $this->rf = new $classname($this);
2360        }
2361
2362        return $this->rf->get_renderer($page, $component, $subtype, $target);
2363    }
2364
2365    /**
2366     * Get the information from {@link $layouts} for this type of page.
2367     *
2368     * @param string $pagelayout the the page layout name.
2369     * @return array the appropriate part of {@link $layouts}.
2370     */
2371    protected function layout_info_for_page($pagelayout) {
2372        if (array_key_exists($pagelayout, $this->layouts)) {
2373            return $this->layouts[$pagelayout];
2374        } else {
2375            debugging('Invalid page layout specified: ' . $pagelayout);
2376            return $this->layouts['standard'];
2377        }
2378    }
2379
2380    /**
2381     * Given the settings of this theme, and the page pagelayout, return the
2382     * full path of the page layout file to use.
2383     *
2384     * Used by {@link core_renderer::header()}.
2385     *
2386     * @param string $pagelayout the the page layout name.
2387     * @return string Full path to the lyout file to use
2388     */
2389    public function layout_file($pagelayout) {
2390        global $CFG;
2391
2392        $layoutinfo = $this->layout_info_for_page($pagelayout);
2393        $layoutfile = $layoutinfo['file'];
2394
2395        if (array_key_exists('theme', $layoutinfo)) {
2396            $themes = array($layoutinfo['theme']);
2397        } else {
2398            $themes = array_merge(array($this->name),$this->parents);
2399        }
2400
2401        foreach ($themes as $theme) {
2402            if ($dir = $this->find_theme_location($theme)) {
2403                $path = "$dir/layout/$layoutfile";
2404
2405                // Check the template exists, return general base theme template if not.
2406                if (is_readable($path)) {
2407                    return $path;
2408                }
2409            }
2410        }
2411
2412        debugging('Can not find layout file for: ' . $pagelayout);
2413        // fallback to standard normal layout
2414        return "$CFG->dirroot/theme/base/layout/general.php";
2415    }
2416
2417    /**
2418     * Returns auxiliary page layout options specified in layout configuration array.
2419     *
2420     * @param string $pagelayout
2421     * @return array
2422     */
2423    public function pagelayout_options($pagelayout) {
2424        $info = $this->layout_info_for_page($pagelayout);
2425        if (!empty($info['options'])) {
2426            return $info['options'];
2427        }
2428        return array();
2429    }
2430
2431    /**
2432     * Inform a block_manager about the block regions this theme wants on this
2433     * page layout.
2434     *
2435     * @param string $pagelayout the general type of the page.
2436     * @param block_manager $blockmanager the block_manger to set up.
2437     */
2438    public function setup_blocks($pagelayout, $blockmanager) {
2439        $layoutinfo = $this->layout_info_for_page($pagelayout);
2440        if (!empty($layoutinfo['regions'])) {
2441            $blockmanager->add_regions($layoutinfo['regions'], false);
2442            $blockmanager->set_default_region($layoutinfo['defaultregion']);
2443        }
2444    }
2445
2446    /**
2447     * Gets the visible name for the requested block region.
2448     *
2449     * @param string $region The region name to get
2450     * @param string $theme The theme the region belongs to (may come from the parent theme)
2451     * @return string
2452     */
2453    protected function get_region_name($region, $theme) {
2454        $regionstring = get_string('region-' . $region, 'theme_' . $theme);
2455        // A name exists in this theme, so use it
2456        if (substr($regionstring, 0, 1) != '[') {
2457            return $regionstring;
2458        }
2459
2460        // Otherwise, try to find one elsewhere
2461        // Check parents, if any
2462        foreach ($this->parents as $parentthemename) {
2463            $regionstring = get_string('region-' . $region, 'theme_' . $parentthemename);
2464            if (substr($regionstring, 0, 1) != '[') {
2465                return $regionstring;
2466            }
2467        }
2468
2469        // Last resort, try the boost theme for names
2470        return get_string('region-' . $region, 'theme_boost');
2471    }
2472
2473    /**
2474     * Get the list of all block regions known to this theme in all templates.
2475     *
2476     * @return array internal region name => human readable name.
2477     */
2478    public function get_all_block_regions() {
2479        $regions = array();
2480        foreach ($this->layouts as $layoutinfo) {
2481            foreach ($layoutinfo['regions'] as $region) {
2482                $regions[$region] = $this->get_region_name($region, $this->name);
2483            }
2484        }
2485        return $regions;
2486    }
2487
2488    /**
2489     * Returns the human readable name of the theme
2490     *
2491     * @return string
2492     */
2493    public function get_theme_name() {
2494        return get_string('pluginname', 'theme_'.$this->name);
2495    }
2496
2497    /**
2498     * Returns the block render method.
2499     *
2500     * It is set by the theme via:
2501     *     $THEME->blockrendermethod = '...';
2502     *
2503     * It can be one of two values, blocks or blocks_for_region.
2504     * It should be set to the method being used by the theme layouts.
2505     *
2506     * @return string
2507     */
2508    public function get_block_render_method() {
2509        if ($this->blockrendermethod) {
2510            // Return the specified block render method.
2511            return $this->blockrendermethod;
2512        }
2513        // Its not explicitly set, check the parent theme configs.
2514        foreach ($this->parent_configs as $config) {
2515            if (isset($config->blockrendermethod)) {
2516                return $config->blockrendermethod;
2517            }
2518        }
2519        // Default it to blocks.
2520        return 'blocks';
2521    }
2522
2523    /**
2524     * Get the callable for CSS tree post processing.
2525     *
2526     * @return string|null
2527     */
2528    public function get_css_tree_post_processor() {
2529        $configs = [$this] + $this->parent_configs;
2530        foreach ($configs as $config) {
2531            if (!empty($config->csstreepostprocessor) && is_callable($config->csstreepostprocessor)) {
2532                return $config->csstreepostprocessor;
2533            }
2534        }
2535        return null;
2536    }
2537
2538}
2539
2540/**
2541 * This class keeps track of which HTML tags are currently open.
2542 *
2543 * This makes it much easier to always generate well formed XHTML output, even
2544 * if execution terminates abruptly. Any time you output some opening HTML
2545 * without the matching closing HTML, you should push the necessary close tags
2546 * onto the stack.
2547 *
2548 * @copyright 2009 Tim Hunt
2549 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2550 * @since Moodle 2.0
2551 * @package core
2552 * @category output
2553 */
2554class xhtml_container_stack {
2555
2556    /**
2557     * @var array Stores the list of open containers.
2558     */
2559    protected $opencontainers = array();
2560
2561    /**
2562     * @var array In developer debug mode, stores a stack trace of all opens and
2563     * closes, so we can output helpful error messages when there is a mismatch.
2564     */
2565    protected $log = array();
2566
2567    /**
2568     * @var boolean Store whether we are developer debug mode. We need this in
2569     * several places including in the destructor where we may not have access to $CFG.
2570     */
2571    protected $isdebugging;
2572
2573    /**
2574     * Constructor
2575     */
2576    public function __construct() {
2577        global $CFG;
2578        $this->isdebugging = $CFG->debugdeveloper;
2579    }
2580
2581    /**
2582     * Push the close HTML for a recently opened container onto the stack.
2583     *
2584     * @param string $type The type of container. This is checked when {@link pop()}
2585     *      is called and must match, otherwise a developer debug warning is output.
2586     * @param string $closehtml The HTML required to close the container.
2587     */
2588    public function push($type, $closehtml) {
2589        $container = new stdClass;
2590        $container->type = $type;
2591        $container->closehtml = $closehtml;
2592        if ($this->isdebugging) {
2593            $this->log('Open', $type);
2594        }
2595        array_push($this->opencontainers, $container);
2596    }
2597
2598    /**
2599     * Pop the HTML for the next closing container from the stack. The $type
2600     * must match the type passed when the container was opened, otherwise a
2601     * warning will be output.
2602     *
2603     * @param string $type The type of container.
2604     * @return string the HTML required to close the container.
2605     */
2606    public function pop($type) {
2607        if (empty($this->opencontainers)) {
2608            debugging('<p>There are no more open containers. This suggests there is a nesting problem.</p>' .
2609                    $this->output_log(), DEBUG_DEVELOPER);
2610            return;
2611        }
2612
2613        $container = array_pop($this->opencontainers);
2614        if ($container->type != $type) {
2615            debugging('<p>The type of container to be closed (' . $container->type .
2616                    ') does not match the type of the next open container (' . $type .
2617                    '). This suggests there is a nesting problem.</p>' .
2618                    $this->output_log(), DEBUG_DEVELOPER);
2619        }
2620        if ($this->isdebugging) {
2621            $this->log('Close', $type);
2622        }
2623        return $container->closehtml;
2624    }
2625
2626    /**
2627     * Close all but the last open container. This is useful in places like error
2628     * handling, where you want to close all the open containers (apart from <body>)
2629     * before outputting the error message.
2630     *
2631     * @param bool $shouldbenone assert that the stack should be empty now - causes a
2632     *      developer debug warning if it isn't.
2633     * @return string the HTML required to close any open containers inside <body>.
2634     */
2635    public function pop_all_but_last($shouldbenone = false) {
2636        if ($shouldbenone && count($this->opencontainers) != 1) {
2637            debugging('<p>Some HTML tags were opened in the body of the page but not closed.</p>' .
2638                    $this->output_log(), DEBUG_DEVELOPER);
2639        }
2640        $output = '';
2641        while (count($this->opencontainers) > 1) {
2642            $container = array_pop($this->opencontainers);
2643            $output .= $container->closehtml;
2644        }
2645        return $output;
2646    }
2647
2648    /**
2649     * You can call this function if you want to throw away an instance of this
2650     * class without properly emptying the stack (for example, in a unit test).
2651     * Calling this method stops the destruct method from outputting a developer
2652     * debug warning. After calling this method, the instance can no longer be used.
2653     */
2654    public function discard() {
2655        $this->opencontainers = null;
2656    }
2657
2658    /**
2659     * Adds an entry to the log.
2660     *
2661     * @param string $action The name of the action
2662     * @param string $type The type of action
2663     */
2664    protected function log($action, $type) {
2665        $this->log[] = '<li>' . $action . ' ' . $type . ' at:' .
2666                format_backtrace(debug_backtrace()) . '</li>';
2667    }
2668
2669    /**
2670     * Outputs the log's contents as a HTML list.
2671     *
2672     * @return string HTML list of the log
2673     */
2674    protected function output_log() {
2675        return '<ul>' . implode("\n", $this->log) . '</ul>';
2676    }
2677}
2678