Merge branch 'MDL-56959-master' of git://github.com/FMCorz/moodle
[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 undeletableblocktypes If set to a string, will list the block types that cannot be deleted. Defaults to
353      *                                   navigation and settings.
354      */
355     public $undeletableblocktypes = 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      * Load the config.php file for a particular theme, and return an instance
479      * of this class. (That is, this is a factory method.)
480      *
481      * @param string $themename the name of the theme.
482      * @return theme_config an instance of this class.
483      */
484     public static function load($themename) {
485         global $CFG;
487         // load theme settings from db
488         try {
489             $settings = get_config('theme_'.$themename);
490         } catch (dml_exception $e) {
491             // most probably moodle tables not created yet
492             $settings = new stdClass();
493         }
495         if ($config = theme_config::find_theme_config($themename, $settings)) {
496             return new theme_config($config);
498         } else if ($themename == theme_config::DEFAULT_THEME) {
499             throw new coding_exception('Default theme '.theme_config::DEFAULT_THEME.' not available or broken!');
501         } else if ($config = theme_config::find_theme_config($CFG->theme, $settings)) {
502             return new theme_config($config);
504         } else {
505             // bad luck, the requested theme has some problems - admin see details in theme config
506             return new theme_config(theme_config::find_theme_config(theme_config::DEFAULT_THEME, $settings));
507         }
508     }
510     /**
511      * Theme diagnostic code. It is very problematic to send debug output
512      * to the actual CSS file, instead this functions is supposed to
513      * diagnose given theme and highlights all potential problems.
514      * This information should be available from the theme selection page
515      * or some other debug page for theme designers.
516      *
517      * @param string $themename
518      * @return array description of problems
519      */
520     public static function diagnose($themename) {
521         //TODO: MDL-21108
522         return array();
523     }
525     /**
526      * Private constructor, can be called only from the factory method.
527      * @param stdClass $config
528      */
529     private function __construct($config) {
530         global $CFG; //needed for included lib.php files
532         $this->settings = $config->settings;
533         $this->name     = $config->name;
534         $this->dir      = $config->dir;
536         if ($this->name != 'bootstrapbase') {
537             $baseconfig = theme_config::find_theme_config('bootstrapbase', $this->settings);
538         } else {
539             $baseconfig = $config;
540         }
542         $configurable = array(
543             'parents', 'sheets', 'parents_exclude_sheets', 'plugins_exclude_sheets',
544             'javascripts', 'javascripts_footer', 'parents_exclude_javascripts',
545             'layouts', 'enable_dock', 'enablecourseajax', 'undeletableblocktypes',
546             'rendererfactory', 'csspostprocess', 'editor_sheets', 'rarrow', 'larrow', 'uarrow', 'darrow',
547             'hidefromselector', 'doctype', 'yuicssmodules', 'blockrtlmanipulations',
548             'lessfile', 'extralesscallback', 'lessvariablescallback', 'blockrendermethod',
549             'scss', 'extrascsscallback', 'prescsscallback', 'csstreepostprocessor');
551         foreach ($config as $key=>$value) {
552             if (in_array($key, $configurable)) {
553                 $this->$key = $value;
554             }
555         }
557         // verify all parents and load configs and renderers
558         foreach ($this->parents as $parent) {
559             if (!$parent_config = theme_config::find_theme_config($parent, $this->settings)) {
560                 // this is not good - better exclude faulty parents
561                 continue;
562             }
563             $libfile = $parent_config->dir.'/lib.php';
564             if (is_readable($libfile)) {
565                 // theme may store various function here
566                 include_once($libfile);
567             }
568             $renderersfile = $parent_config->dir.'/renderers.php';
569             if (is_readable($renderersfile)) {
570                 // may contain core and plugin renderers and renderer factory
571                 include_once($renderersfile);
572             }
573             $this->parent_configs[$parent] = $parent_config;
574         }
575         $libfile = $this->dir.'/lib.php';
576         if (is_readable($libfile)) {
577             // theme may store various function here
578             include_once($libfile);
579         }
580         $rendererfile = $this->dir.'/renderers.php';
581         if (is_readable($rendererfile)) {
582             // may contain core and plugin renderers and renderer factory
583             include_once($rendererfile);
584         } else {
585             // check if renderers.php file is missnamed renderer.php
586             if (is_readable($this->dir.'/renderer.php')) {
587                 debugging('Developer hint: '.$this->dir.'/renderer.php should be renamed to ' . $this->dir."/renderers.php.
588                     See: http://docs.moodle.org/dev/Output_renderers#Theme_renderers.", DEBUG_DEVELOPER);
589             }
590         }
592         // cascade all layouts properly
593         foreach ($baseconfig->layouts as $layout=>$value) {
594             if (!isset($this->layouts[$layout])) {
595                 foreach ($this->parent_configs as $parent_config) {
596                     if (isset($parent_config->layouts[$layout])) {
597                         $this->layouts[$layout] = $parent_config->layouts[$layout];
598                         continue 2;
599                     }
600                 }
601                 $this->layouts[$layout] = $value;
602             }
603         }
605         //fix arrows if needed
606         $this->check_theme_arrows();
607     }
609     /**
610      * Let the theme initialise the page object (usually $PAGE).
611      *
612      * This may be used for example to request jQuery in add-ons.
613      *
614      * @param moodle_page $page
615      */
616     public function init_page(moodle_page $page) {
617         $themeinitfunction = 'theme_'.$this->name.'_page_init';
618         if (function_exists($themeinitfunction)) {
619             $themeinitfunction($page);
620         }
621     }
623     /**
624      * Checks if arrows $THEME->rarrow, $THEME->larrow, $THEME->uarrow, $THEME->darrow have been set (theme/-/config.php).
625      * If not it applies sensible defaults.
626      *
627      * Accessibility: right and left arrow Unicode characters for breadcrumb, calendar,
628      * search forum block, etc. Important: these are 'silent' in a screen-reader
629      * (unlike &gt; &raquo;), and must be accompanied by text.
630      */
631     private function check_theme_arrows() {
632         if (!isset($this->rarrow) and !isset($this->larrow)) {
633             // Default, looks good in Win XP/IE 6, Win/Firefox 1.5, Win/Netscape 8...
634             // Also OK in Win 9x/2K/IE 5.x
635             $this->rarrow = '&#x25BA;';
636             $this->larrow = '&#x25C4;';
637             $this->uarrow = '&#x25B2;';
638             $this->darrow = '&#x25BC;';
639             if (empty($_SERVER['HTTP_USER_AGENT'])) {
640                 $uagent = '';
641             } else {
642                 $uagent = $_SERVER['HTTP_USER_AGENT'];
643             }
644             if (false !== strpos($uagent, 'Opera')
645                 || false !== strpos($uagent, 'Mac')) {
646                 // Looks good in Win XP/Mac/Opera 8/9, Mac/Firefox 2, Camino, Safari.
647                 // Not broken in Mac/IE 5, Mac/Netscape 7 (?).
648                 $this->rarrow = '&#x25B6;&#xFE0E;';
649                 $this->larrow = '&#x25C0;&#xFE0E;';
650             }
651             elseif ((false !== strpos($uagent, 'Konqueror'))
652                 || (false !== strpos($uagent, 'Android')))  {
653                 // The fonts on Android don't include the characters required for this to work as expected.
654                 // So we use the same ones Konqueror uses.
655                 $this->rarrow = '&rarr;';
656                 $this->larrow = '&larr;';
657                 $this->uarrow = '&uarr;';
658                 $this->darrow = '&darr;';
659             }
660             elseif (isset($_SERVER['HTTP_ACCEPT_CHARSET'])
661                 && false === stripos($_SERVER['HTTP_ACCEPT_CHARSET'], 'utf-8')) {
662                 // (Win/IE 5 doesn't set ACCEPT_CHARSET, but handles Unicode.)
663                 // To be safe, non-Unicode browsers!
664                 $this->rarrow = '&gt;';
665                 $this->larrow = '&lt;';
666                 $this->uarrow = '^';
667                 $this->darrow = 'v';
668             }
670             // RTL support - in RTL languages, swap r and l arrows
671             if (right_to_left()) {
672                 $t = $this->rarrow;
673                 $this->rarrow = $this->larrow;
674                 $this->larrow = $t;
675             }
676         }
677     }
679     /**
680      * Returns output renderer prefixes, these are used when looking
681      * for the overridden renderers in themes.
682      *
683      * @return array
684      */
685     public function renderer_prefixes() {
686         global $CFG; // just in case the included files need it
688         $prefixes = array('theme_'.$this->name);
690         foreach ($this->parent_configs as $parent) {
691             $prefixes[] = 'theme_'.$parent->name;
692         }
694         return $prefixes;
695     }
697     /**
698      * Returns the stylesheet URL of this editor content
699      *
700      * @param bool $encoded false means use & and true use &amp; in URLs
701      * @return moodle_url
702      */
703     public function editor_css_url($encoded=true) {
704         global $CFG;
705         $rev = theme_get_revision();
706         if ($rev > -1) {
707             $url = new moodle_url("$CFG->httpswwwroot/theme/styles.php");
708             if (!empty($CFG->slasharguments)) {
709                 $url->set_slashargument('/'.$this->name.'/'.$rev.'/editor', 'noparam', true);
710             } else {
711                 $url->params(array('theme'=>$this->name,'rev'=>$rev, 'type'=>'editor'));
712             }
713         } else {
714             $params = array('theme'=>$this->name, 'type'=>'editor');
715             $url = new moodle_url($CFG->httpswwwroot.'/theme/styles_debug.php', $params);
716         }
717         return $url;
718     }
720     /**
721      * Returns the content of the CSS to be used in editor content
722      *
723      * @return array
724      */
725     public function editor_css_files() {
726         $files = array();
728         // First editor plugins.
729         $plugins = core_component::get_plugin_list('editor');
730         foreach ($plugins as $plugin=>$fulldir) {
731             $sheetfile = "$fulldir/editor_styles.css";
732             if (is_readable($sheetfile)) {
733                 $files['plugin_'.$plugin] = $sheetfile;
734             }
735         }
736         // Then parent themes - base first, the immediate parent last.
737         foreach (array_reverse($this->parent_configs) as $parent_config) {
738             if (empty($parent_config->editor_sheets)) {
739                 continue;
740             }
741             foreach ($parent_config->editor_sheets as $sheet) {
742                 $sheetfile = "$parent_config->dir/style/$sheet.css";
743                 if (is_readable($sheetfile)) {
744                     $files['parent_'.$parent_config->name.'_'.$sheet] = $sheetfile;
745                 }
746             }
747         }
748         // Finally this theme.
749         if (!empty($this->editor_sheets)) {
750             foreach ($this->editor_sheets as $sheet) {
751                 $sheetfile = "$this->dir/style/$sheet.css";
752                 if (is_readable($sheetfile)) {
753                     $files['theme_'.$sheet] = $sheetfile;
754                 }
755             }
756         }
758         return $files;
759     }
761     /**
762      * Get the stylesheet URL of this theme.
763      *
764      * @param moodle_page $page Not used... deprecated?
765      * @return moodle_url[]
766      */
767     public function css_urls(moodle_page $page) {
768         global $CFG;
770         $rev = theme_get_revision();
772         $urls = array();
774         $svg = $this->use_svg_icons();
775         $separate = (core_useragent::is_ie() && !core_useragent::check_ie_version('10'));
777         if ($rev > -1) {
778             $filename = right_to_left() ? 'all-rtl' : 'all';
779             $url = new moodle_url("$CFG->httpswwwroot/theme/styles.php");
780             if (!empty($CFG->slasharguments)) {
781                 $slashargs = '';
782                 if (!$svg) {
783                     // We add a simple /_s to the start of the path.
784                     // The underscore is used to ensure that it isn't a valid theme name.
785                     $slashargs .= '/_s'.$slashargs;
786                 }
787                 $slashargs .= '/'.$this->name.'/'.$rev.'/'.$filename;
788                 if ($separate) {
789                     $slashargs .= '/chunk0';
790                 }
791                 $url->set_slashargument($slashargs, 'noparam', true);
792             } else {
793                 $params = array('theme' => $this->name, 'rev' => $rev, 'type' => $filename);
794                 if (!$svg) {
795                     // We add an SVG param so that we know not to serve SVG images.
796                     // We do this because all modern browsers support SVG and this param will one day be removed.
797                     $params['svg'] = '0';
798                 }
799                 if ($separate) {
800                     $params['chunk'] = '0';
801                 }
802                 $url->params($params);
803             }
804             $urls[] = $url;
806         } else {
807             $baseurl = new moodle_url($CFG->httpswwwroot.'/theme/styles_debug.php');
809             $css = $this->get_css_files(true);
810             if (!$svg) {
811                 // We add an SVG param so that we know not to serve SVG images.
812                 // We do this because all modern browsers support SVG and this param will one day be removed.
813                 $baseurl->param('svg', '0');
814             }
815             if (right_to_left()) {
816                 $baseurl->param('rtl', 1);
817             }
818             if ($separate) {
819                 // We might need to chunk long files.
820                 $baseurl->param('chunk', '0');
821             }
822             if (core_useragent::is_ie()) {
823                 // Lalala, IE does not allow more than 31 linked CSS files from main document.
824                 $urls[] = new moodle_url($baseurl, array('theme'=>$this->name, 'type'=>'ie', 'subtype'=>'plugins'));
825                 foreach ($css['parents'] as $parent=>$sheets) {
826                     // We need to serve parents individually otherwise we may easily exceed the style limit IE imposes (4096).
827                     $urls[] = new moodle_url($baseurl, array('theme'=>$this->name,'type'=>'ie', 'subtype'=>'parents', 'sheet'=>$parent));
828                 }
829                 if ($this->get_scss_property()) {
830                     // No need to define the type as IE here.
831                     $urls[] = new moodle_url($baseurl, array('theme' => $this->name, 'type' => 'scss'));
832                 } else if (!empty($this->lessfile)) {
833                     // No need to define the type as IE here.
834                     $urls[] = new moodle_url($baseurl, array('theme' => $this->name, 'type' => 'less'));
835                 }
836                 $urls[] = new moodle_url($baseurl, array('theme'=>$this->name, 'type'=>'ie', 'subtype'=>'theme'));
838             } else {
839                 foreach ($css['plugins'] as $plugin=>$unused) {
840                     $urls[] = new moodle_url($baseurl, array('theme'=>$this->name,'type'=>'plugin', 'subtype'=>$plugin));
841                 }
842                 foreach ($css['parents'] as $parent=>$sheets) {
843                     foreach ($sheets as $sheet=>$unused2) {
844                         $urls[] = new moodle_url($baseurl, array('theme'=>$this->name,'type'=>'parent', 'subtype'=>$parent, 'sheet'=>$sheet));
845                     }
846                 }
847                 foreach ($css['theme'] as $sheet => $filename) {
848                     if ($sheet === self::SCSS_KEY) {
849                         // This is the theme SCSS file.
850                         $urls[] = new moodle_url($baseurl, array('theme' => $this->name, 'type' => 'scss'));
851                     } else if ($sheet === $this->lessfile) {
852                         // This is the theme LESS file.
853                         $urls[] = new moodle_url($baseurl, array('theme' => $this->name, 'type' => 'less'));
854                     } else {
855                         // Sheet first in order to make long urls easier to read.
856                         $urls[] = new moodle_url($baseurl, array('sheet'=>$sheet, 'theme'=>$this->name, 'type'=>'theme'));
857                     }
858                 }
859             }
860         }
862         return $urls;
863     }
865     /**
866      * Get the whole css stylesheet for production mode.
867      *
868      * NOTE: this method is not expected to be used from any addons.
869      *
870      * @return string CSS markup compressed
871      */
872     public function get_css_content() {
874         $csscontent = '';
875         foreach ($this->get_css_files(false) as $type => $value) {
876             foreach ($value as $identifier => $val) {
877                 if (is_array($val)) {
878                     foreach ($val as $v) {
879                         $csscontent .= file_get_contents($v) . "\n";
880                     }
881                 } else {
882                     if ($type === 'theme' && $identifier === self::SCSS_KEY) {
883                         // We need the content from SCSS because this is the SCSS file from the theme.
884                         $csscontent .= $this->get_css_content_from_scss(false);
885                     } else if ($type === 'theme' && $identifier === $this->lessfile) {
886                         // We need the content from LESS because this is the LESS file from the theme.
887                         $csscontent .= $this->get_css_content_from_less(false);
888                     } else {
889                         $csscontent .= file_get_contents($val) . "\n";
890                     }
891                 }
892             }
893         }
894         $csscontent = $this->post_process($csscontent);
895         $csscontent = core_minify::css($csscontent);
897         return $csscontent;
898     }
900     /**
901      * Get the theme designer css markup,
902      * the parameters are coming from css_urls().
903      *
904      * NOTE: this method is not expected to be used from any addons.
905      *
906      * @param string $type
907      * @param string $subtype
908      * @param string $sheet
909      * @return string CSS markup
910      */
911     public function get_css_content_debug($type, $subtype, $sheet) {
913         if ($type === 'scss') {
914             // The SCSS file of the theme is requested.
915             $csscontent = $this->get_css_content_from_scss(true);
916             if ($csscontent !== false) {
917                 return $this->post_process($csscontent);
918             }
919             return '';
920         } else if ($type === 'less') {
921             // The LESS file of the theme is requested.
922             $csscontent = $this->get_css_content_from_less(true);
923             if ($csscontent !== false) {
924                 return $this->post_process($csscontent);
925             }
926             return '';
927         }
929         $cssfiles = array();
930         $css = $this->get_css_files(true);
932         if ($type === 'ie') {
933             // IE is a sloppy browser with weird limits, sorry.
934             if ($subtype === 'plugins') {
935                 $cssfiles = $css['plugins'];
937             } else if ($subtype === 'parents') {
938                 if (empty($sheet)) {
939                     // Do not bother with the empty parent here.
940                 } else {
941                     // Build up the CSS for that parent so we can serve it as one file.
942                     foreach ($css[$subtype][$sheet] as $parent => $css) {
943                         $cssfiles[] = $css;
944                     }
945                 }
946             } else if ($subtype === 'theme') {
947                 $cssfiles = $css['theme'];
948                 foreach ($cssfiles as $key => $value) {
949                     if (in_array($key, [$this->lessfile, self::SCSS_KEY])) {
950                         // Remove the LESS/SCSS file from the theme CSS files.
951                         // The LESS/SCSS files use the type 'less' or 'scss', not 'ie'.
952                         unset($cssfiles[$key]);
953                     }
954                 }
955             }
957         } else if ($type === 'plugin') {
958             if (isset($css['plugins'][$subtype])) {
959                 $cssfiles[] = $css['plugins'][$subtype];
960             }
962         } else if ($type === 'parent') {
963             if (isset($css['parents'][$subtype][$sheet])) {
964                 $cssfiles[] = $css['parents'][$subtype][$sheet];
965             }
967         } else if ($type === 'theme') {
968             if (isset($css['theme'][$sheet])) {
969                 $cssfiles[] = $css['theme'][$sheet];
970             }
971         }
973         $csscontent = '';
974         foreach ($cssfiles as $file) {
975             $contents = file_get_contents($file);
976             $contents = $this->post_process($contents);
977             $comment = "/** Path: $type $subtype $sheet.' **/\n";
978             $stats = '';
979             $csscontent .= $comment.$stats.$contents."\n\n";
980         }
982         return $csscontent;
983     }
985     /**
986      * Get the whole css stylesheet for editor iframe.
987      *
988      * NOTE: this method is not expected to be used from any addons.
989      *
990      * @return string CSS markup
991      */
992     public function get_css_content_editor() {
993         // Do not bother to optimise anything here, just very basic stuff.
994         $cssfiles = $this->editor_css_files();
995         $css = '';
996         foreach ($cssfiles as $file) {
997             $css .= file_get_contents($file)."\n";
998         }
999         return $this->post_process($css);
1000     }
1002     /**
1003      * Returns an array of organised CSS files required for this output.
1004      *
1005      * @param bool $themedesigner
1006      * @return array nested array of file paths
1007      */
1008     protected function get_css_files($themedesigner) {
1009         global $CFG;
1011         $cache = null;
1012         $cachekey = 'cssfiles';
1013         if ($themedesigner) {
1014             require_once($CFG->dirroot.'/lib/csslib.php');
1015             // We need some kind of caching here because otherwise the page navigation becomes
1016             // way too slow in theme designer mode. Feel free to create full cache definition later...
1017             $cache = cache::make_from_params(cache_store::MODE_APPLICATION, 'core', 'themedesigner', array('theme' => $this->name));
1018             if ($files = $cache->get($cachekey)) {
1019                 if ($files['created'] > time() - THEME_DESIGNER_CACHE_LIFETIME) {
1020                     unset($files['created']);
1021                     return $files;
1022                 }
1023             }
1024         }
1026         $cssfiles = array('plugins'=>array(), 'parents'=>array(), 'theme'=>array());
1028         // Get all plugin sheets.
1029         $excludes = $this->resolve_excludes('plugins_exclude_sheets');
1030         if ($excludes !== true) {
1031             foreach (core_component::get_plugin_types() as $type=>$unused) {
1032                 if ($type === 'theme' || (!empty($excludes[$type]) and $excludes[$type] === true)) {
1033                     continue;
1034                 }
1035                 $plugins = core_component::get_plugin_list($type);
1036                 foreach ($plugins as $plugin=>$fulldir) {
1037                     if (!empty($excludes[$type]) and is_array($excludes[$type])
1038                             and in_array($plugin, $excludes[$type])) {
1039                         continue;
1040                     }
1042                     // Get the CSS from the plugin.
1043                     $sheetfile = "$fulldir/styles.css";
1044                     if (is_readable($sheetfile)) {
1045                         $cssfiles['plugins'][$type.'_'.$plugin] = $sheetfile;
1046                     }
1048                     // Create a list of candidate sheets from parents (direct parent last) and current theme.
1049                     $candidates = array();
1050                     foreach (array_reverse($this->parent_configs) as $parent_config) {
1051                         $candidates[] = $parent_config->name;
1052                     }
1053                     $candidates[] = $this->name;
1055                     // Add the sheets found.
1056                     foreach ($candidates as $candidate) {
1057                         $sheetthemefile = "$fulldir/styles_{$candidate}.css";
1058                         if (is_readable($sheetthemefile)) {
1059                             $cssfiles['plugins'][$type.'_'.$plugin.'_'.$candidate] = $sheetthemefile;
1060                         }
1061                     }
1062                 }
1063             }
1064         }
1066         // Find out wanted parent sheets.
1067         $excludes = $this->resolve_excludes('parents_exclude_sheets');
1068         if ($excludes !== true) {
1069             foreach (array_reverse($this->parent_configs) as $parent_config) { // Base first, the immediate parent last.
1070                 $parent = $parent_config->name;
1071                 if (empty($parent_config->sheets) || (!empty($excludes[$parent]) and $excludes[$parent] === true)) {
1072                     continue;
1073                 }
1074                 foreach ($parent_config->sheets as $sheet) {
1075                     if (!empty($excludes[$parent]) && is_array($excludes[$parent])
1076                             && in_array($sheet, $excludes[$parent])) {
1077                         continue;
1078                     }
1080                     // We never refer to the parent LESS files.
1081                     $sheetfile = "$parent_config->dir/style/$sheet.css";
1082                     if (is_readable($sheetfile)) {
1083                         $cssfiles['parents'][$parent][$sheet] = $sheetfile;
1084                     }
1085                 }
1086             }
1087         }
1090         // Current theme sheets and less file.
1091         // We first add the SCSS, or LESS file because we want the CSS ones to
1092         // be included after the SCSS/LESS code. However, if both the LESS file
1093         // and a CSS file share the same name, the CSS file is ignored.
1094         if ($this->get_scss_property()) {
1095             $cssfiles['theme'][self::SCSS_KEY] = true;
1096         } else if (!empty($this->lessfile)) {
1097             $sheetfile = "{$this->dir}/less/{$this->lessfile}.less";
1098             if (is_readable($sheetfile)) {
1099                 $cssfiles['theme'][$this->lessfile] = $sheetfile;
1100             }
1101         }
1102         if (is_array($this->sheets)) {
1103             foreach ($this->sheets as $sheet) {
1104                 $sheetfile = "$this->dir/style/$sheet.css";
1105                 if (is_readable($sheetfile) && !isset($cssfiles['theme'][$sheet])) {
1106                     $cssfiles['theme'][$sheet] = $sheetfile;
1107                 }
1108             }
1109         }
1111         if ($cache) {
1112             $files = $cssfiles;
1113             $files['created'] = time();
1114             $cache->set($cachekey, $files);
1115         }
1116         return $cssfiles;
1117     }
1119     /**
1120      * Return the CSS content generated from LESS the file.
1121      *
1122      * @param bool $themedesigner True if theme designer is enabled.
1123      * @return bool|string Return false when the compilation failed. Else the compiled string.
1124      */
1125     protected function get_css_content_from_less($themedesigner) {
1126         global $CFG;
1128         $lessfile = $this->lessfile;
1129         if (!$lessfile || !is_readable($this->dir . '/less/' . $lessfile . '.less')) {
1130             throw new coding_exception('The theme did not define a LESS file, or it is not readable.');
1131         }
1133         // We might need more memory to do this, so let's play safe.
1134         raise_memory_limit(MEMORY_EXTRA);
1136         // Files list.
1137         $files = $this->get_css_files($themedesigner);
1139         // Get the LESS file path.
1140         $themelessfile = $files['theme'][$lessfile];
1142         // Setup compiler options.
1143         $options = array(
1144             // We need to set the import directory to where $lessfile is.
1145             'import_dirs' => array(dirname($themelessfile) => '/'),
1146             // Always disable default caching.
1147             'cache_method' => false,
1148             // Disable the relative URLs, we have post_process() to handle that.
1149             'relativeUrls' => false,
1150         );
1152         if ($themedesigner) {
1153             // Add the sourceMap inline to ensure that it is atomically generated.
1154             $options['sourceMap'] = true;
1155             $options['sourceMapBasepath'] = $CFG->dirroot;
1156             $options['sourceMapRootpath'] = $CFG->wwwroot;
1157         }
1159         // Instantiate the compiler.
1160         $compiler = new core_lessc($options);
1162         try {
1163             $compiler->parse_file_content($themelessfile);
1165             // Get the callbacks.
1166             $compiler->parse($this->get_extra_less_code());
1167             $compiler->ModifyVars($this->get_less_variables());
1169             // Compile the CSS.
1170             $compiled = $compiler->getCss();
1172         } catch (Less_Exception_Parser $e) {
1173             $compiled = false;
1174             debugging('Error while compiling LESS ' . $lessfile . ' file: ' . $e->getMessage(), DEBUG_DEVELOPER);
1175         }
1177         // Try to save memory.
1178         $compiler = null;
1179         unset($compiler);
1181         return $compiled;
1182     }
1184     /**
1185      * Return the CSS content generated from the SCSS file.
1186      *
1187      * @param bool $themedesigner True if theme designer is enabled.
1188      * @return bool|string Return false when the compilation failed. Else the compiled string.
1189      */
1190     protected function get_css_content_from_scss($themedesigner) {
1191         global $CFG;
1193         list($paths, $scss) = $this->get_scss_property();
1194         if (!$scss) {
1195             throw new coding_exception('The theme did not define a SCSS file, or it is not readable.');
1196         }
1198         // We might need more memory to do this, so let's play safe.
1199         raise_memory_limit(MEMORY_EXTRA);
1201         // Set-up the compiler.
1202         $compiler = new core_scss();
1203         $compiler->prepend_raw_scss($this->get_pre_scss_code());
1204         if (is_string($scss)) {
1205             $compiler->set_file($scss);
1206         } else {
1207             $compiler->append_raw_scss($scss($this));
1208             $compiler->setImportPaths($paths);
1209         }
1210         $compiler->append_raw_scss($this->get_extra_scss_code());
1212         try {
1213             // Compile!
1214             $compiled = $compiler->to_css();
1216         } catch (\Leafo\ScssPhp\Exception $e) {
1217             $compiled = false;
1218             debugging('Error while compiling SCSS: ' . $e->getMessage(), DEBUG_DEVELOPER);
1219         }
1221         // Try to save memory.
1222         $compiler = null;
1223         unset($compiler);
1225         return $compiled;
1226     }
1228     /**
1229      * Return extra LESS variables to use when compiling.
1230      *
1231      * @return array Where keys are the variable names (omitting the @), and the values are the value.
1232      */
1233     protected function get_less_variables() {
1234         $variables = array();
1236         // Getting all the candidate functions.
1237         $candidates = array();
1238         foreach ($this->parent_configs as $parent_config) {
1239             if (!isset($parent_config->lessvariablescallback)) {
1240                 continue;
1241             }
1242             $candidates[] = $parent_config->lessvariablescallback;
1243         }
1244         $candidates[] = $this->lessvariablescallback;
1246         // Calling the functions.
1247         foreach ($candidates as $function) {
1248             if (function_exists($function)) {
1249                 $vars = $function($this);
1250                 if (!is_array($vars)) {
1251                     debugging('Callback ' . $function . ' did not return an array() as expected', DEBUG_DEVELOPER);
1252                     continue;
1253                 }
1254                 $variables = array_merge($variables, $vars);
1255             }
1256         }
1258         return $variables;
1259     }
1261     /**
1262      * Return extra LESS code to add when compiling.
1263      *
1264      * This is intended to be used by themes to inject some LESS code
1265      * before it gets compiled. If you want to inject variables you
1266      * should use {@link self::get_less_variables()}.
1267      *
1268      * @return string The LESS code to inject.
1269      */
1270     protected function get_extra_less_code() {
1271         $content = '';
1273         // Getting all the candidate functions.
1274         $candidates = array();
1275         foreach ($this->parent_configs as $parent_config) {
1276             if (!isset($parent_config->extralesscallback)) {
1277                 continue;
1278             }
1279             $candidates[] = $parent_config->extralesscallback;
1280         }
1281         $candidates[] = $this->extralesscallback;
1283         // Calling the functions.
1284         foreach ($candidates as $function) {
1285             if (function_exists($function)) {
1286                 $content .= "\n/** Extra LESS from $function **/\n" . $function($this) . "\n";
1287             }
1288         }
1290         return $content;
1291     }
1293     /**
1294      * Return extra SCSS code to add when compiling.
1295      *
1296      * This is intended to be used by themes to inject some SCSS code
1297      * before it gets compiled. If you want to inject variables you
1298      * should use {@link self::get_scss_variables()}.
1299      *
1300      * @return string The SCSS code to inject.
1301      */
1302     protected function get_extra_scss_code() {
1303         $content = '';
1305         // Getting all the candidate functions.
1306         $candidates = array();
1307         foreach ($this->parent_configs as $parent_config) {
1308             if (!isset($parent_config->extrascsscallback)) {
1309                 continue;
1310             }
1311             $candidates[] = $parent_config->extrascsscallback;
1312         }
1313         $candidates[] = $this->extrascsscallback;
1315         // Calling the functions.
1316         foreach ($candidates as $function) {
1317             if (function_exists($function)) {
1318                 $content .= "\n/** Extra SCSS from $function **/\n" . $function($this) . "\n";
1319             }
1320         }
1322         return $content;
1323     }
1325     /**
1326      * SCSS code to prepend when compiling.
1327      *
1328      * This is intended to be used by themes to inject SCSS code before it gets compiled.
1329      *
1330      * @return string The SCSS code to inject.
1331      */
1332     protected function get_pre_scss_code() {
1333         $content = '';
1335         // Getting all the candidate functions.
1336         $candidates = array();
1337         foreach ($this->parent_configs as $parent_config) {
1338             if (!isset($parent_config->prescsscallback)) {
1339                 continue;
1340             }
1341             $candidates[] = $parent_config->prescsscallback;
1342         }
1343         $candidates[] = $this->prescsscallback;
1345         // Calling the functions.
1346         foreach ($candidates as $function) {
1347             if (function_exists($function)) {
1348                 $content .= "\n/** Pre-SCSS from $function **/\n" . $function($this) . "\n";
1349             }
1350         }
1352         return $content;
1353     }
1355     /**
1356      * Get the SCSS property.
1357      *
1358      * This resolves whether a SCSS file (or content) has to be used when generating
1359      * the stylesheet for the theme. It will look at parents themes and check the
1360      * SCSS properties there.
1361      *
1362      * @return False when SCSS is not used.
1363      *         An array with the import paths, and the path to the SCSS file or Closure as second.
1364      */
1365     public function get_scss_property() {
1366         if ($this->scsscache === null) {
1367             $configs = [$this] + $this->parent_configs;
1368             $scss = null;
1370             foreach ($configs as $config) {
1371                 $path = "{$config->dir}/scss";
1373                 // We collect the SCSS property until we've found one.
1374                 if (empty($scss) && !empty($config->scss)) {
1375                     $candidate = is_string($config->scss) ? "{$path}/{$config->scss}.scss" : $config->scss;
1376                     if ($candidate instanceof Closure) {
1377                         $scss = $candidate;
1378                     } else if (is_string($candidate) && is_readable($candidate)) {
1379                         $scss = $candidate;
1380                     }
1381                 }
1383                 // We collect the import paths once we've found a SCSS property.
1384                 if ($scss && is_dir($path)) {
1385                     $paths[] = $path;
1386                 }
1388             }
1390             $this->scsscache = $scss !== null ? [$paths, $scss] : false;
1391         }
1393         return $this->scsscache;
1394     }
1396     /**
1397      * Generate a URL to the file that serves theme JavaScript files.
1398      *
1399      * If we determine that the theme has no relevant files, then we return
1400      * early with a null value.
1401      *
1402      * @param bool $inhead true means head url, false means footer
1403      * @return moodle_url|null
1404      */
1405     public function javascript_url($inhead) {
1406         global $CFG;
1408         $rev = theme_get_revision();
1409         $params = array('theme'=>$this->name,'rev'=>$rev);
1410         $params['type'] = $inhead ? 'head' : 'footer';
1412         // Return early if there are no files to serve
1413         if (count($this->javascript_files($params['type'])) === 0) {
1414             return null;
1415         }
1417         if (!empty($CFG->slasharguments) and $rev > 0) {
1418             $url = new moodle_url("$CFG->httpswwwroot/theme/javascript.php");
1419             $url->set_slashargument('/'.$this->name.'/'.$rev.'/'.$params['type'], 'noparam', true);
1420             return $url;
1421         } else {
1422             return new moodle_url($CFG->httpswwwroot.'/theme/javascript.php', $params);
1423         }
1424     }
1426     /**
1427      * Get the URL's for the JavaScript files used by this theme.
1428      * They won't be served directly, instead they'll be mediated through
1429      * theme/javascript.php.
1430      *
1431      * @param string $type Either javascripts_footer, or javascripts
1432      * @return array
1433      */
1434     public function javascript_files($type) {
1435         if ($type === 'footer') {
1436             $type = 'javascripts_footer';
1437         } else {
1438             $type = 'javascripts';
1439         }
1441         $js = array();
1442         // find out wanted parent javascripts
1443         $excludes = $this->resolve_excludes('parents_exclude_javascripts');
1444         if ($excludes !== true) {
1445             foreach (array_reverse($this->parent_configs) as $parent_config) { // base first, the immediate parent last
1446                 $parent = $parent_config->name;
1447                 if (empty($parent_config->$type)) {
1448                     continue;
1449                 }
1450                 if (!empty($excludes[$parent]) and $excludes[$parent] === true) {
1451                     continue;
1452                 }
1453                 foreach ($parent_config->$type as $javascript) {
1454                     if (!empty($excludes[$parent]) and is_array($excludes[$parent])
1455                         and in_array($javascript, $excludes[$parent])) {
1456                         continue;
1457                     }
1458                     $javascriptfile = "$parent_config->dir/javascript/$javascript.js";
1459                     if (is_readable($javascriptfile)) {
1460                         $js[] = $javascriptfile;
1461                     }
1462                 }
1463             }
1464         }
1466         // current theme javascripts
1467         if (is_array($this->$type)) {
1468             foreach ($this->$type as $javascript) {
1469                 $javascriptfile = "$this->dir/javascript/$javascript.js";
1470                 if (is_readable($javascriptfile)) {
1471                     $js[] = $javascriptfile;
1472                 }
1473             }
1474         }
1475         return $js;
1476     }
1478     /**
1479      * Resolves an exclude setting to the themes setting is applicable or the
1480      * setting of its closest parent.
1481      *
1482      * @param string $variable The name of the setting the exclude setting to resolve
1483      * @param string $default
1484      * @return mixed
1485      */
1486     protected function resolve_excludes($variable, $default = null) {
1487         $setting = $default;
1488         if (is_array($this->{$variable}) or $this->{$variable} === true) {
1489             $setting = $this->{$variable};
1490         } else {
1491             foreach ($this->parent_configs as $parent_config) { // the immediate parent first, base last
1492                 if (!isset($parent_config->{$variable})) {
1493                     continue;
1494                 }
1495                 if (is_array($parent_config->{$variable}) or $parent_config->{$variable} === true) {
1496                     $setting = $parent_config->{$variable};
1497                     break;
1498                 }
1499             }
1500         }
1501         return $setting;
1502     }
1504     /**
1505      * Returns the content of the one huge javascript file merged from all theme javascript files.
1506      *
1507      * @param bool $type
1508      * @return string
1509      */
1510     public function javascript_content($type) {
1511         $jsfiles = $this->javascript_files($type);
1512         $js = '';
1513         foreach ($jsfiles as $jsfile) {
1514             $js .= file_get_contents($jsfile)."\n";
1515         }
1516         return $js;
1517     }
1519     /**
1520      * Post processes CSS.
1521      *
1522      * This method post processes all of the CSS before it is served for this theme.
1523      * This is done so that things such as image URL's can be swapped in and to
1524      * run any specific CSS post process method the theme has requested.
1525      * This allows themes to use CSS settings.
1526      *
1527      * @param string $css The CSS to process.
1528      * @return string The processed CSS.
1529      */
1530     public function post_process($css) {
1531         // now resolve all image locations
1532         if (preg_match_all('/\[\[pix:([a-z0-9_]+\|)?([^\]]+)\]\]/', $css, $matches, PREG_SET_ORDER)) {
1533             $replaced = array();
1534             foreach ($matches as $match) {
1535                 if (isset($replaced[$match[0]])) {
1536                     continue;
1537                 }
1538                 $replaced[$match[0]] = true;
1539                 $imagename = $match[2];
1540                 $component = rtrim($match[1], '|');
1541                 $imageurl = $this->pix_url($imagename, $component)->out(false);
1542                  // we do not need full url because the image.php is always in the same dir
1543                 $imageurl = preg_replace('|^http.?://[^/]+|', '', $imageurl);
1544                 $css = str_replace($match[0], $imageurl, $css);
1545             }
1546         }
1548         // Now resolve all font locations.
1549         if (preg_match_all('/\[\[font:([a-z0-9_]+\|)?([^\]]+)\]\]/', $css, $matches, PREG_SET_ORDER)) {
1550             $replaced = array();
1551             foreach ($matches as $match) {
1552                 if (isset($replaced[$match[0]])) {
1553                     continue;
1554                 }
1555                 $replaced[$match[0]] = true;
1556                 $fontname = $match[2];
1557                 $component = rtrim($match[1], '|');
1558                 $fonturl = $this->font_url($fontname, $component)->out(false);
1559                 // We do not need full url because the font.php is always in the same dir.
1560                 $fonturl = preg_replace('|^http.?://[^/]+|', '', $fonturl);
1561                 $css = str_replace($match[0], $fonturl, $css);
1562             }
1563         }
1565         // Now resolve all theme settings or do any other postprocessing.
1566         // This needs to be done before calling core parser, since the parser strips [[settings]] tags.
1567         $csspostprocess = $this->csspostprocess;
1568         if (function_exists($csspostprocess)) {
1569             $css = $csspostprocess($css, $this);
1570         }
1572         // Post processing using an object representation of CSS.
1573         $treeprocessor = $this->get_css_tree_post_processor();
1574         $needsparsing = !empty($treeprocessor) || !empty($this->rtlmode);
1575         if ($needsparsing) {
1576             $parser = new core_cssparser($css);
1577             $csstree = $parser->parse();
1578             unset($parser);
1580             if ($this->rtlmode) {
1581                 $this->rtlize($csstree);
1582             }
1584             if ($treeprocessor) {
1585                 $treeprocessor($csstree, $this);
1586             }
1588             $css = $csstree->render();
1589             unset($csstree);
1590         }
1592         return $css;
1593     }
1595     /**
1596      * Flip a stylesheet to RTL.
1597      *
1598      * @param Object $csstree The parsed CSS tree structure to flip.
1599      * @return void
1600      */
1601     protected function rtlize($csstree) {
1602         $rtlcss = new core_rtlcss($csstree);
1603         $rtlcss->flip();
1604     }
1606     /**
1607      * Return the URL for an image
1608      *
1609      * @param string $imagename the name of the icon.
1610      * @param string $component specification of one plugin like in get_string()
1611      * @return moodle_url
1612      */
1613     public function pix_url($imagename, $component) {
1614         global $CFG;
1616         $params = array('theme'=>$this->name);
1617         $svg = $this->use_svg_icons();
1619         if (empty($component) or $component === 'moodle' or $component === 'core') {
1620             $params['component'] = 'core';
1621         } else {
1622             $params['component'] = $component;
1623         }
1625         $rev = theme_get_revision();
1626         if ($rev != -1) {
1627             $params['rev'] = $rev;
1628         }
1630         $params['image'] = $imagename;
1632         $url = new moodle_url("$CFG->httpswwwroot/theme/image.php");
1633         if (!empty($CFG->slasharguments) and $rev > 0) {
1634             $path = '/'.$params['theme'].'/'.$params['component'].'/'.$params['rev'].'/'.$params['image'];
1635             if (!$svg) {
1636                 // We add a simple /_s to the start of the path.
1637                 // The underscore is used to ensure that it isn't a valid theme name.
1638                 $path = '/_s'.$path;
1639             }
1640             $url->set_slashargument($path, 'noparam', true);
1641         } else {
1642             if (!$svg) {
1643                 // We add an SVG param so that we know not to serve SVG images.
1644                 // We do this because all modern browsers support SVG and this param will one day be removed.
1645                 $params['svg'] = '0';
1646             }
1647             $url->params($params);
1648         }
1650         return $url;
1651     }
1653     /**
1654      * Return the URL for a font
1655      *
1656      * @param string $font the name of the font (including extension).
1657      * @param string $component specification of one plugin like in get_string()
1658      * @return moodle_url
1659      */
1660     public function font_url($font, $component) {
1661         global $CFG;
1663         $params = array('theme'=>$this->name);
1665         if (empty($component) or $component === 'moodle' or $component === 'core') {
1666             $params['component'] = 'core';
1667         } else {
1668             $params['component'] = $component;
1669         }
1671         $rev = theme_get_revision();
1672         if ($rev != -1) {
1673             $params['rev'] = $rev;
1674         }
1676         $params['font'] = $font;
1678         $url = new moodle_url("$CFG->httpswwwroot/theme/font.php");
1679         if (!empty($CFG->slasharguments) and $rev > 0) {
1680             $path = '/'.$params['theme'].'/'.$params['component'].'/'.$params['rev'].'/'.$params['font'];
1681             $url->set_slashargument($path, 'noparam', true);
1682         } else {
1683             $url->params($params);
1684         }
1686         return $url;
1687     }
1689     /**
1690      * Returns URL to the stored file via pluginfile.php.
1691      *
1692      * Note the theme must also implement pluginfile.php handler,
1693      * theme revision is used instead of the itemid.
1694      *
1695      * @param string $setting
1696      * @param string $filearea
1697      * @return string protocol relative URL or null if not present
1698      */
1699     public function setting_file_url($setting, $filearea) {
1700         global $CFG;
1702         if (empty($this->settings->$setting)) {
1703             return null;
1704         }
1706         $component = 'theme_'.$this->name;
1707         $itemid = theme_get_revision();
1708         $filepath = $this->settings->$setting;
1709         $syscontext = context_system::instance();
1711         $url = moodle_url::make_file_url("$CFG->wwwroot/pluginfile.php", "/$syscontext->id/$component/$filearea/$itemid".$filepath);
1713         // Now this is tricky because the we can not hardcode http or https here, lets use the relative link.
1714         // Note: unfortunately moodle_url does not support //urls yet.
1716         $url = preg_replace('|^https?://|i', '//', $url->out(false));
1718         return $url;
1719     }
1721     /**
1722      * Serve the theme setting file.
1723      *
1724      * @param string $filearea
1725      * @param array $args
1726      * @param bool $forcedownload
1727      * @param array $options
1728      * @return bool may terminate if file not found or donotdie not specified
1729      */
1730     public function setting_file_serve($filearea, $args, $forcedownload, $options) {
1731         global $CFG;
1732         require_once("$CFG->libdir/filelib.php");
1734         $syscontext = context_system::instance();
1735         $component = 'theme_'.$this->name;
1737         $revision = array_shift($args);
1738         if ($revision < 0) {
1739             $lifetime = 0;
1740         } else {
1741             $lifetime = 60*60*24*60;
1742             // By default, theme files must be cache-able by both browsers and proxies.
1743             if (!array_key_exists('cacheability', $options)) {
1744                 $options['cacheability'] = 'public';
1745             }
1746         }
1748         $fs = get_file_storage();
1749         $relativepath = implode('/', $args);
1751         $fullpath = "/{$syscontext->id}/{$component}/{$filearea}/0/{$relativepath}";
1752         $fullpath = rtrim($fullpath, '/');
1753         if ($file = $fs->get_file_by_hash(sha1($fullpath))) {
1754             send_stored_file($file, $lifetime, 0, $forcedownload, $options);
1755             return true;
1756         } else {
1757             send_file_not_found();
1758         }
1759     }
1761     /**
1762      * Resolves the real image location.
1763      *
1764      * $svg was introduced as an arg in 2.4. It is important because not all supported browsers support the use of SVG
1765      * and we need a way in which to turn it off.
1766      * By default SVG won't be used unless asked for. This is done for two reasons:
1767      *   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
1768      *      browser supports SVG.
1769      *   2. We only serve SVG images from locations we trust. This must NOT include any areas where the image may have been uploaded
1770      *      by the user due to security concerns.
1771      *
1772      * @param string $image name of image, may contain relative path
1773      * @param string $component
1774      * @param bool $svg If set to true SVG images will also be looked for.
1775      * @return string full file path
1776      */
1777     public function resolve_image_location($image, $component, $svg = false) {
1778         global $CFG;
1780         if (!is_bool($svg)) {
1781             // If $svg isn't a bool then we need to decide for ourselves.
1782             $svg = $this->use_svg_icons();
1783         }
1785         if ($component === 'moodle' or $component === 'core' or empty($component)) {
1786             if ($imagefile = $this->image_exists("$this->dir/pix_core/$image", $svg)) {
1787                 return $imagefile;
1788             }
1789             foreach (array_reverse($this->parent_configs) as $parent_config) { // base first, the immediate parent last
1790                 if ($imagefile = $this->image_exists("$parent_config->dir/pix_core/$image", $svg)) {
1791                     return $imagefile;
1792                 }
1793             }
1794             if ($imagefile = $this->image_exists("$CFG->dataroot/pix/$image", $svg)) {
1795                 return $imagefile;
1796             }
1797             if ($imagefile = $this->image_exists("$CFG->dirroot/pix/$image", $svg)) {
1798                 return $imagefile;
1799             }
1800             return null;
1802         } else if ($component === 'theme') { //exception
1803             if ($image === 'favicon') {
1804                 return "$this->dir/pix/favicon.ico";
1805             }
1806             if ($imagefile = $this->image_exists("$this->dir/pix/$image", $svg)) {
1807                 return $imagefile;
1808             }
1809             foreach (array_reverse($this->parent_configs) as $parent_config) { // base first, the immediate parent last
1810                 if ($imagefile = $this->image_exists("$parent_config->dir/pix/$image", $svg)) {
1811                     return $imagefile;
1812                 }
1813             }
1814             return null;
1816         } else {
1817             if (strpos($component, '_') === false) {
1818                 $component = 'mod_'.$component;
1819             }
1820             list($type, $plugin) = explode('_', $component, 2);
1822             if ($imagefile = $this->image_exists("$this->dir/pix_plugins/$type/$plugin/$image", $svg)) {
1823                 return $imagefile;
1824             }
1825             foreach (array_reverse($this->parent_configs) as $parent_config) { // base first, the immediate parent last
1826                 if ($imagefile = $this->image_exists("$parent_config->dir/pix_plugins/$type/$plugin/$image", $svg)) {
1827                     return $imagefile;
1828                 }
1829             }
1830             if ($imagefile = $this->image_exists("$CFG->dataroot/pix_plugins/$type/$plugin/$image", $svg)) {
1831                 return $imagefile;
1832             }
1833             $dir = core_component::get_plugin_directory($type, $plugin);
1834             if ($imagefile = $this->image_exists("$dir/pix/$image", $svg)) {
1835                 return $imagefile;
1836             }
1837             return null;
1838         }
1839     }
1841     /**
1842      * Resolves the real font location.
1843      *
1844      * @param string $font name of font file
1845      * @param string $component
1846      * @return string full file path
1847      */
1848     public function resolve_font_location($font, $component) {
1849         global $CFG;
1851         if ($component === 'moodle' or $component === 'core' or empty($component)) {
1852             if (file_exists("$this->dir/fonts_core/$font")) {
1853                 return "$this->dir/fonts_core/$font";
1854             }
1855             foreach (array_reverse($this->parent_configs) as $parent_config) { // Base first, the immediate parent last.
1856                 if (file_exists("$parent_config->dir/fonts_core/$font")) {
1857                     return "$parent_config->dir/fonts_core/$font";
1858                 }
1859             }
1860             if (file_exists("$CFG->dataroot/fonts/$font")) {
1861                 return "$CFG->dataroot/fonts/$font";
1862             }
1863             if (file_exists("$CFG->dirroot/lib/fonts/$font")) {
1864                 return "$CFG->dirroot/lib/fonts/$font";
1865             }
1866             return null;
1868         } else if ($component === 'theme') { // Exception.
1869             if (file_exists("$this->dir/fonts/$font")) {
1870                 return "$this->dir/fonts/$font";
1871             }
1872             foreach (array_reverse($this->parent_configs) as $parent_config) { // Base first, the immediate parent last.
1873                 if (file_exists("$parent_config->dir/fonts/$font")) {
1874                     return "$parent_config->dir/fonts/$font";
1875                 }
1876             }
1877             return null;
1879         } else {
1880             if (strpos($component, '_') === false) {
1881                 $component = 'mod_'.$component;
1882             }
1883             list($type, $plugin) = explode('_', $component, 2);
1885             if (file_exists("$this->dir/fonts_plugins/$type/$plugin/$font")) {
1886                 return "$this->dir/fonts_plugins/$type/$plugin/$font";
1887             }
1888             foreach (array_reverse($this->parent_configs) as $parent_config) { // Base first, the immediate parent last.
1889                 if (file_exists("$parent_config->dir/fonts_plugins/$type/$plugin/$font")) {
1890                     return "$parent_config->dir/fonts_plugins/$type/$plugin/$font";
1891                 }
1892             }
1893             if (file_exists("$CFG->dataroot/fonts_plugins/$type/$plugin/$font")) {
1894                 return "$CFG->dataroot/fonts_plugins/$type/$plugin/$font";
1895             }
1896             $dir = core_component::get_plugin_directory($type, $plugin);
1897             if (file_exists("$dir/fonts/$font")) {
1898                 return "$dir/fonts/$font";
1899             }
1900             return null;
1901         }
1902     }
1904     /**
1905      * Return true if we should look for SVG images as well.
1906      *
1907      * @return bool
1908      */
1909     public function use_svg_icons() {
1910         global $CFG;
1911         if ($this->usesvg === null) {
1913             if (!isset($CFG->svgicons)) {
1914                 $this->usesvg = core_useragent::supports_svg();
1915             } else {
1916                 // Force them on/off depending upon the setting.
1917                 $this->usesvg = (bool)$CFG->svgicons;
1918             }
1919         }
1920         return $this->usesvg;
1921     }
1923     /**
1924      * Forces the usesvg setting to either true or false, avoiding any decision making.
1925      *
1926      * This function should only ever be used when absolutely required, and before any generation of image URL's has occurred.
1927      * DO NOT ABUSE THIS FUNCTION... not that you'd want to right ;)
1928      *
1929      * @param bool $setting True to force the use of svg when available, null otherwise.
1930      */
1931     public function force_svg_use($setting) {
1932         $this->usesvg = (bool)$setting;
1933     }
1935     /**
1936      * Set to be in RTL mode.
1937      *
1938      * This will likely be used when post processing the CSS before serving it.
1939      *
1940      * @param bool $inrtl True when in RTL mode.
1941      */
1942     public function set_rtl_mode($inrtl = true) {
1943         $this->rtlmode = $inrtl;
1944     }
1946     /**
1947      * Checks if file with any image extension exists.
1948      *
1949      * The order to these images was adjusted prior to the release of 2.4
1950      * At that point the were the following image counts in Moodle core:
1951      *
1952      *     - png = 667 in pix dirs (1499 total)
1953      *     - gif = 385 in pix dirs (606 total)
1954      *     - jpg = 62  in pix dirs (74 total)
1955      *     - jpeg = 0  in pix dirs (1 total)
1956      *
1957      * There is work in progress to move towards SVG presently hence that has been prioritiesed.
1958      *
1959      * @param string $filepath
1960      * @param bool $svg If set to true SVG images will also be looked for.
1961      * @return string image name with extension
1962      */
1963     private static function image_exists($filepath, $svg = false) {
1964         if ($svg && file_exists("$filepath.svg")) {
1965             return "$filepath.svg";
1966         } else  if (file_exists("$filepath.png")) {
1967             return "$filepath.png";
1968         } else if (file_exists("$filepath.gif")) {
1969             return "$filepath.gif";
1970         } else  if (file_exists("$filepath.jpg")) {
1971             return "$filepath.jpg";
1972         } else  if (file_exists("$filepath.jpeg")) {
1973             return "$filepath.jpeg";
1974         } else {
1975             return false;
1976         }
1977     }
1979     /**
1980      * Loads the theme config from config.php file.
1981      *
1982      * @param string $themename
1983      * @param stdClass $settings from config_plugins table
1984      * @param boolean $parentscheck true to also check the parents.    .
1985      * @return stdClass The theme configuration
1986      */
1987     private static function find_theme_config($themename, $settings, $parentscheck = true) {
1988         // We have to use the variable name $THEME (upper case) because that
1989         // is what is used in theme config.php files.
1991         if (!$dir = theme_config::find_theme_location($themename)) {
1992             return null;
1993         }
1995         $THEME = new stdClass();
1996         $THEME->name     = $themename;
1997         $THEME->dir      = $dir;
1998         $THEME->settings = $settings;
2000         global $CFG; // just in case somebody tries to use $CFG in theme config
2001         include("$THEME->dir/config.php");
2003         // verify the theme configuration is OK
2004         if (!is_array($THEME->parents)) {
2005             // parents option is mandatory now
2006             return null;
2007         } else {
2008             // We use $parentscheck to only check the direct parents (avoid infinite loop).
2009             if ($parentscheck) {
2010                 // Find all parent theme configs.
2011                 foreach ($THEME->parents as $parent) {
2012                     $parentconfig = theme_config::find_theme_config($parent, $settings, false);
2013                     if (empty($parentconfig)) {
2014                         return null;
2015                     }
2016                 }
2017             }
2018         }
2020         return $THEME;
2021     }
2023     /**
2024      * Finds the theme location and verifies the theme has all needed files
2025      * and is not obsoleted.
2026      *
2027      * @param string $themename
2028      * @return string full dir path or null if not found
2029      */
2030     private static function find_theme_location($themename) {
2031         global $CFG;
2033         if (file_exists("$CFG->dirroot/theme/$themename/config.php")) {
2034             $dir = "$CFG->dirroot/theme/$themename";
2036         } else if (!empty($CFG->themedir) and file_exists("$CFG->themedir/$themename/config.php")) {
2037             $dir = "$CFG->themedir/$themename";
2039         } else {
2040             return null;
2041         }
2043         if (file_exists("$dir/styles.php")) {
2044             //legacy theme - needs to be upgraded - upgrade info is displayed on the admin settings page
2045             return null;
2046         }
2048         return $dir;
2049     }
2051     /**
2052      * Get the renderer for a part of Moodle for this theme.
2053      *
2054      * @param moodle_page $page the page we are rendering
2055      * @param string $component the name of part of moodle. E.g. 'core', 'quiz', 'qtype_multichoice'.
2056      * @param string $subtype optional subtype such as 'news' resulting to 'mod_forum_news'
2057      * @param string $target one of rendering target constants
2058      * @return renderer_base the requested renderer.
2059      */
2060     public function get_renderer(moodle_page $page, $component, $subtype = null, $target = null) {
2061         if (is_null($this->rf)) {
2062             $classname = $this->rendererfactory;
2063             $this->rf = new $classname($this);
2064         }
2066         return $this->rf->get_renderer($page, $component, $subtype, $target);
2067     }
2069     /**
2070      * Get the information from {@link $layouts} for this type of page.
2071      *
2072      * @param string $pagelayout the the page layout name.
2073      * @return array the appropriate part of {@link $layouts}.
2074      */
2075     protected function layout_info_for_page($pagelayout) {
2076         if (array_key_exists($pagelayout, $this->layouts)) {
2077             return $this->layouts[$pagelayout];
2078         } else {
2079             debugging('Invalid page layout specified: ' . $pagelayout);
2080             return $this->layouts['standard'];
2081         }
2082     }
2084     /**
2085      * Given the settings of this theme, and the page pagelayout, return the
2086      * full path of the page layout file to use.
2087      *
2088      * Used by {@link core_renderer::header()}.
2089      *
2090      * @param string $pagelayout the the page layout name.
2091      * @return string Full path to the lyout file to use
2092      */
2093     public function layout_file($pagelayout) {
2094         global $CFG;
2096         $layoutinfo = $this->layout_info_for_page($pagelayout);
2097         $layoutfile = $layoutinfo['file'];
2099         if (array_key_exists('theme', $layoutinfo)) {
2100             $themes = array($layoutinfo['theme']);
2101         } else {
2102             $themes = array_merge(array($this->name),$this->parents);
2103         }
2105         foreach ($themes as $theme) {
2106             if ($dir = $this->find_theme_location($theme)) {
2107                 $path = "$dir/layout/$layoutfile";
2109                 // Check the template exists, return general base theme template if not.
2110                 if (is_readable($path)) {
2111                     return $path;
2112                 }
2113             }
2114         }
2116         debugging('Can not find layout file for: ' . $pagelayout);
2117         // fallback to standard normal layout
2118         return "$CFG->dirroot/theme/base/layout/general.php";
2119     }
2121     /**
2122      * Returns auxiliary page layout options specified in layout configuration array.
2123      *
2124      * @param string $pagelayout
2125      * @return array
2126      */
2127     public function pagelayout_options($pagelayout) {
2128         $info = $this->layout_info_for_page($pagelayout);
2129         if (!empty($info['options'])) {
2130             return $info['options'];
2131         }
2132         return array();
2133     }
2135     /**
2136      * Inform a block_manager about the block regions this theme wants on this
2137      * page layout.
2138      *
2139      * @param string $pagelayout the general type of the page.
2140      * @param block_manager $blockmanager the block_manger to set up.
2141      */
2142     public function setup_blocks($pagelayout, $blockmanager) {
2143         $layoutinfo = $this->layout_info_for_page($pagelayout);
2144         if (!empty($layoutinfo['regions'])) {
2145             $blockmanager->add_regions($layoutinfo['regions'], false);
2146             $blockmanager->set_default_region($layoutinfo['defaultregion']);
2147         }
2148     }
2150     /**
2151      * Gets the visible name for the requested block region.
2152      *
2153      * @param string $region The region name to get
2154      * @param string $theme The theme the region belongs to (may come from the parent theme)
2155      * @return string
2156      */
2157     protected function get_region_name($region, $theme) {
2158         $regionstring = get_string('region-' . $region, 'theme_' . $theme);
2159         // A name exists in this theme, so use it
2160         if (substr($regionstring, 0, 1) != '[') {
2161             return $regionstring;
2162         }
2164         // Otherwise, try to find one elsewhere
2165         // Check parents, if any
2166         foreach ($this->parents as $parentthemename) {
2167             $regionstring = get_string('region-' . $region, 'theme_' . $parentthemename);
2168             if (substr($regionstring, 0, 1) != '[') {
2169                 return $regionstring;
2170             }
2171         }
2173         // Last resort, try the bootstrapbase theme for names
2174         return get_string('region-' . $region, 'theme_bootstrapbase');
2175     }
2177     /**
2178      * Get the list of all block regions known to this theme in all templates.
2179      *
2180      * @return array internal region name => human readable name.
2181      */
2182     public function get_all_block_regions() {
2183         $regions = array();
2184         foreach ($this->layouts as $layoutinfo) {
2185             foreach ($layoutinfo['regions'] as $region) {
2186                 $regions[$region] = $this->get_region_name($region, $this->name);
2187             }
2188         }
2189         return $regions;
2190     }
2192     /**
2193      * Returns the human readable name of the theme
2194      *
2195      * @return string
2196      */
2197     public function get_theme_name() {
2198         return get_string('pluginname', 'theme_'.$this->name);
2199     }
2201     /**
2202      * Returns the block render method.
2203      *
2204      * It is set by the theme via:
2205      *     $THEME->blockrendermethod = '...';
2206      *
2207      * It can be one of two values, blocks or blocks_for_region.
2208      * It should be set to the method being used by the theme layouts.
2209      *
2210      * @return string
2211      */
2212     public function get_block_render_method() {
2213         if ($this->blockrendermethod) {
2214             // Return the specified block render method.
2215             return $this->blockrendermethod;
2216         }
2217         // Its not explicitly set, check the parent theme configs.
2218         foreach ($this->parent_configs as $config) {
2219             if (isset($config->blockrendermethod)) {
2220                 return $config->blockrendermethod;
2221             }
2222         }
2223         // Default it to blocks.
2224         return 'blocks';
2225     }
2227     /**
2228      * Get the callable for CSS tree post processing.
2229      *
2230      * @return string|null
2231      */
2232     public function get_css_tree_post_processor() {
2233         $configs = [$this] + $this->parent_configs;
2234         foreach ($configs as $config) {
2235             if ($config->csstreepostprocessor && is_callable($config->csstreepostprocessor)) {
2236                 return $config->csstreepostprocessor;
2237             }
2238         }
2239         return null;
2240     }
2243 /**
2244  * This class keeps track of which HTML tags are currently open.
2245  *
2246  * This makes it much easier to always generate well formed XHTML output, even
2247  * if execution terminates abruptly. Any time you output some opening HTML
2248  * without the matching closing HTML, you should push the necessary close tags
2249  * onto the stack.
2250  *
2251  * @copyright 2009 Tim Hunt
2252  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2253  * @since Moodle 2.0
2254  * @package core
2255  * @category output
2256  */
2257 class xhtml_container_stack {
2259     /**
2260      * @var array Stores the list of open containers.
2261      */
2262     protected $opencontainers = array();
2264     /**
2265      * @var array In developer debug mode, stores a stack trace of all opens and
2266      * closes, so we can output helpful error messages when there is a mismatch.
2267      */
2268     protected $log = array();
2270     /**
2271      * @var boolean Store whether we are developer debug mode. We need this in
2272      * several places including in the destructor where we may not have access to $CFG.
2273      */
2274     protected $isdebugging;
2276     /**
2277      * Constructor
2278      */
2279     public function __construct() {
2280         global $CFG;
2281         $this->isdebugging = $CFG->debugdeveloper;
2282     }
2284     /**
2285      * Push the close HTML for a recently opened container onto the stack.
2286      *
2287      * @param string $type The type of container. This is checked when {@link pop()}
2288      *      is called and must match, otherwise a developer debug warning is output.
2289      * @param string $closehtml The HTML required to close the container.
2290      */
2291     public function push($type, $closehtml) {
2292         $container = new stdClass;
2293         $container->type = $type;
2294         $container->closehtml = $closehtml;
2295         if ($this->isdebugging) {
2296             $this->log('Open', $type);
2297         }
2298         array_push($this->opencontainers, $container);
2299     }
2301     /**
2302      * Pop the HTML for the next closing container from the stack. The $type
2303      * must match the type passed when the container was opened, otherwise a
2304      * warning will be output.
2305      *
2306      * @param string $type The type of container.
2307      * @return string the HTML required to close the container.
2308      */
2309     public function pop($type) {
2310         if (empty($this->opencontainers)) {
2311             debugging('<p>There are no more open containers. This suggests there is a nesting problem.</p>' .
2312                     $this->output_log(), DEBUG_DEVELOPER);
2313             return;
2314         }
2316         $container = array_pop($this->opencontainers);
2317         if ($container->type != $type) {
2318             debugging('<p>The type of container to be closed (' . $container->type .
2319                     ') does not match the type of the next open container (' . $type .
2320                     '). This suggests there is a nesting problem.</p>' .
2321                     $this->output_log(), DEBUG_DEVELOPER);
2322         }
2323         if ($this->isdebugging) {
2324             $this->log('Close', $type);
2325         }
2326         return $container->closehtml;
2327     }
2329     /**
2330      * Close all but the last open container. This is useful in places like error
2331      * handling, where you want to close all the open containers (apart from <body>)
2332      * before outputting the error message.
2333      *
2334      * @param bool $shouldbenone assert that the stack should be empty now - causes a
2335      *      developer debug warning if it isn't.
2336      * @return string the HTML required to close any open containers inside <body>.
2337      */
2338     public function pop_all_but_last($shouldbenone = false) {
2339         if ($shouldbenone && count($this->opencontainers) != 1) {
2340             debugging('<p>Some HTML tags were opened in the body of the page but not closed.</p>' .
2341                     $this->output_log(), DEBUG_DEVELOPER);
2342         }
2343         $output = '';
2344         while (count($this->opencontainers) > 1) {
2345             $container = array_pop($this->opencontainers);
2346             $output .= $container->closehtml;
2347         }
2348         return $output;
2349     }
2351     /**
2352      * You can call this function if you want to throw away an instance of this
2353      * class without properly emptying the stack (for example, in a unit test).
2354      * Calling this method stops the destruct method from outputting a developer
2355      * debug warning. After calling this method, the instance can no longer be used.
2356      */
2357     public function discard() {
2358         $this->opencontainers = null;
2359     }
2361     /**
2362      * Adds an entry to the log.
2363      *
2364      * @param string $action The name of the action
2365      * @param string $type The type of action
2366      */
2367     protected function log($action, $type) {
2368         $this->log[] = '<li>' . $action . ' ' . $type . ' at:' .
2369                 format_backtrace(debug_backtrace()) . '</li>';
2370     }
2372     /**
2373      * Outputs the log's contents as a HTML list.
2374      *
2375      * @return string HTML list of the log
2376      */
2377     protected function output_log() {
2378         return '<ul>' . implode("\n", $this->log) . '</ul>';
2379     }