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