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