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