7a1a2ec1c5100cc80f5e1f7767cf802fd786f01f
[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             return -1;
90         } else {
91             return $CFG->themerev;
92         }
94     } else {
95         return -1;
96     }
97 }
100 /**
101  * This class represents the configuration variables of a Moodle theme.
102  *
103  * All the variables with access: public below (with a few exceptions that are marked)
104  * are the properties you can set in your themes config.php file.
105  *
106  * There are also some methods and protected variables that are part of the inner
107  * workings of Moodle's themes system. If you are just editing a themes config.php
108  * file, you can just ignore those, and the following information for developers.
109  *
110  * Normally, to create an instance of this class, you should use the
111  * {@link theme_config::load()} factory method to load a themes config.php file.
112  * However, normally you don't need to bother, because moodle_page (that is, $PAGE)
113  * will create one for you, accessible as $PAGE->theme.
114  *
115  * @copyright 2009 Tim Hunt
116  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
117  * @since Moodle 2.0
118  * @package core
119  * @category output
120  */
121 class theme_config {
123     /**
124      * @var string Default theme, used when requested theme not found.
125      */
126     const DEFAULT_THEME = 'clean';
128     /**
129      * @var array You can base your theme on other themes by linking to the other theme as
130      * parents. This lets you use the CSS and layouts from the other themes
131      * (see {@link theme_config::$layouts}).
132      * That makes it easy to create a new theme that is similar to another one
133      * but with a few changes. In this themes CSS you only need to override
134      * those rules you want to change.
135      */
136     public $parents;
138     /**
139      * @var array The names of all the stylesheets from this theme that you would
140      * like included, in order. Give the names of the files without .css.
141      */
142     public $sheets = array();
144     /**
145      * @var array The names of all the stylesheets from parents that should be excluded.
146      * true value may be used to specify all parents or all themes from one parent.
147      * If no value specified value from parent theme used.
148      */
149     public $parents_exclude_sheets = null;
151     /**
152      * @var array List of plugin sheets to be excluded.
153      * If no value specified value from parent theme used.
154      */
155     public $plugins_exclude_sheets = null;
157     /**
158      * @var array List of style sheets that are included in the text editor bodies.
159      * Sheets from parent themes are used automatically and can not be excluded.
160      */
161     public $editor_sheets = array();
163     /**
164      * @var array The names of all the javascript files this theme that you would
165      * like included from head, in order. Give the names of the files without .js.
166      */
167     public $javascripts = array();
169     /**
170      * @var array The names of all the javascript files this theme that you would
171      * like included from footer, in order. Give the names of the files without .js.
172      */
173     public $javascripts_footer = array();
175     /**
176      * @var array The names of all the javascript files from parents that should
177      * be excluded. true value may be used to specify all parents or all themes
178      * from one parent.
179      * If no value specified value from parent theme used.
180      */
181     public $parents_exclude_javascripts = null;
183     /**
184      * @var array Which file to use for each page layout.
185      *
186      * This is an array of arrays. The keys of the outer array are the different layouts.
187      * Pages in Moodle are using several different layouts like 'normal', 'course', 'home',
188      * 'popup', 'form', .... The most reliable way to get a complete list is to look at
189      * {@link http://cvs.moodle.org/moodle/theme/base/config.php?view=markup the base theme config.php file}.
190      * That file also has a good example of how to set this setting.
191      *
192      * For each layout, the value in the outer array is an array that describes
193      * how you want that type of page to look. For example
194      * <pre>
195      *   $THEME->layouts = array(
196      *       // Most pages - if we encounter an unknown or a missing page type, this one is used.
197      *       'standard' => array(
198      *           'theme' = 'mytheme',
199      *           'file' => 'normal.php',
200      *           'regions' => array('side-pre', 'side-post'),
201      *           'defaultregion' => 'side-post'
202      *       ),
203      *       // The site home page.
204      *       'home' => array(
205      *           'theme' = 'mytheme',
206      *           'file' => 'home.php',
207      *           'regions' => array('side-pre', 'side-post'),
208      *           'defaultregion' => 'side-post'
209      *       ),
210      *       // ...
211      *   );
212      * </pre>
213      *
214      * 'theme' name of the theme where is the layout located
215      * 'file' is the layout file to use for this type of page.
216      * layout files are stored in layout subfolder
217      * 'regions' This lists the regions on the page where blocks may appear. For
218      * each region you list here, your layout file must include a call to
219      * <pre>
220      *   echo $OUTPUT->blocks_for_region($regionname);
221      * </pre>
222      * or equivalent so that the blocks are actually visible.
223      *
224      * 'defaultregion' If the list of regions is non-empty, then you must pick
225      * one of the one of them as 'default'. This has two meanings. First, this is
226      * where new blocks are added. Second, if there are any blocks associated with
227      * the page, but in non-existent regions, they appear here. (Imaging, for example,
228      * that someone added blocks using a different theme that used different region
229      * names, and then switched to this theme.)
230      */
231     public $layouts = array();
233     /**
234      * @var string Name of the renderer factory class to use. Must implement the
235      * {@link renderer_factory} interface.
236      *
237      * This is an advanced feature. Moodle output is generated by 'renderers',
238      * you can customise the HTML that is output by writing custom renderers,
239      * and then you need to specify 'renderer factory' so that Moodle can find
240      * your renderers.
241      *
242      * There are some renderer factories supplied with Moodle. Please follow these
243      * links to see what they do.
244      * <ul>
245      * <li>{@link standard_renderer_factory} - the default.</li>
246      * <li>{@link theme_overridden_renderer_factory} - use this if you want to write
247      *      your own custom renderers in a lib.php file in this theme (or the parent theme).</li>
248      * </ul>
249      */
250     public $rendererfactory = 'standard_renderer_factory';
252     /**
253      * @var string Function to do custom CSS post-processing.
254      *
255      * This is an advanced feature. If you want to do custom post-processing on the
256      * CSS before it is output (for example, to replace certain variable names
257      * with particular values) you can give the name of a function here.
258      */
259     public $csspostprocess = null;
261     /**
262      * @var string Accessibility: Right arrow-like character is
263      * used in the breadcrumb trail, course navigation menu
264      * (previous/next activity), calendar, and search forum block.
265      * If the theme does not set characters, appropriate defaults
266      * are set automatically. Please DO NOT
267      * use &lt; &gt; &raquo; - these are confusing for blind users.
268      */
269     public $rarrow = null;
271     /**
272      * @var string Accessibility: Right arrow-like character is
273      * used in the breadcrumb trail, course navigation menu
274      * (previous/next activity), calendar, and search forum block.
275      * If the theme does not set characters, appropriate defaults
276      * are set automatically. Please DO NOT
277      * use &lt; &gt; &raquo; - these are confusing for blind users.
278      */
279     public $larrow = null;
281     /**
282      * @var bool Some themes may want to disable ajax course editing.
283      */
284     public $enablecourseajax = true;
286     /**
287      * @var string Determines served document types
288      *  - 'html5' the only officially supported doctype in Moodle
289      *  - 'xhtml5' may be used in development for validation (not intended for production servers!)
290      *  - 'xhtml' XHTML 1.0 Strict for legacy themes only
291      */
292     public $doctype = 'html5';
294     //==Following properties are not configurable from theme config.php==
296     /**
297      * @var string The name of this theme. Set automatically when this theme is
298      * loaded. This can not be set in theme config.php
299      */
300     public $name;
302     /**
303      * @var string The folder where this themes files are stored. This is set
304      * automatically. This can not be set in theme config.php
305      */
306     public $dir;
308     /**
309      * @var stdClass Theme settings stored in config_plugins table.
310      * This can not be set in theme config.php
311      */
312     public $setting = null;
314     /**
315      * @var bool If set to true and the theme enables the dock then  blocks will be able
316      * to be moved to the special dock
317      */
318     public $enable_dock = false;
320     /**
321      * @var bool If set to true then this theme will not be shown in the theme selector unless
322      * theme designer mode is turned on.
323      */
324     public $hidefromselector = false;
326     /**
327      * @var array list of YUI CSS modules to be included on each page. This may be used
328      * to remove cssreset and use cssnormalise module instead.
329      */
330     public $yuicssmodules = array('cssreset', 'cssfonts', 'cssgrids', 'cssbase');
332     /**
333      * An associative array of block manipulations that should be made if the user is using an rtl language.
334      * The key is the original block region, and the value is the block region to change to.
335      * This is used when displaying blocks for regions only.
336      * @var array
337      */
338     public $blockrtlmanipulations = array();
340     /**
341      * @var renderer_factory Instance of the renderer_factory implementation
342      * we are using. Implementation detail.
343      */
344     protected $rf = null;
346     /**
347      * @var array List of parent config objects.
348      **/
349     protected $parent_configs = array();
351     /**
352      * @var bool If set to true then the theme is safe to run through the optimiser (if it is enabled)
353      * If set to false then we know either the theme has already been optimised and the CSS optimiser is not needed
354      * or the theme is not compatible with the CSS optimiser. In both cases even if enabled the CSS optimiser will not
355      * be used with this theme if set to false.
356      */
357     public $supportscssoptimisation = true;
359     /**
360      * Used to determine whether we can serve SVG images or not.
361      * @var bool
362      */
363     private $usesvg = null;
365     /**
366      * The LESS file to compile. When set, the theme will attempt to compile the file itself.
367      * @var bool
368      */
369     public $lessfile = false;
371     /**
372      * The name of the function to call to get the LESS code to inject.
373      * @var string
374      */
375     public $extralesscallback = null;
377     /**
378      * The name of the function to call to get extra LESS variables.
379      * @var string
380      */
381     public $lessvariablescallback = null;
383     /**
384      * Sets the render method that should be used for rendering custom block regions by scripts such as my/index.php
385      * Defaults to {@link core_renderer::blocks_for_region()}
386      * @var string
387      */
388     public $blockrendermethod = null;
390     /**
391      * Load the config.php file for a particular theme, and return an instance
392      * of this class. (That is, this is a factory method.)
393      *
394      * @param string $themename the name of the theme.
395      * @return theme_config an instance of this class.
396      */
397     public static function load($themename) {
398         global $CFG;
400         // load theme settings from db
401         try {
402             $settings = get_config('theme_'.$themename);
403         } catch (dml_exception $e) {
404             // most probably moodle tables not created yet
405             $settings = new stdClass();
406         }
408         if ($config = theme_config::find_theme_config($themename, $settings)) {
409             return new theme_config($config);
411         } else if ($themename == theme_config::DEFAULT_THEME) {
412             throw new coding_exception('Default theme '.theme_config::DEFAULT_THEME.' not available or broken!');
414         } else if ($config = theme_config::find_theme_config($CFG->theme, $settings)) {
415             return new theme_config($config);
417         } else {
418             // bad luck, the requested theme has some problems - admin see details in theme config
419             return new theme_config(theme_config::find_theme_config(theme_config::DEFAULT_THEME, $settings));
420         }
421     }
423     /**
424      * Theme diagnostic code. It is very problematic to send debug output
425      * to the actual CSS file, instead this functions is supposed to
426      * diagnose given theme and highlights all potential problems.
427      * This information should be available from the theme selection page
428      * or some other debug page for theme designers.
429      *
430      * @param string $themename
431      * @return array description of problems
432      */
433     public static function diagnose($themename) {
434         //TODO: MDL-21108
435         return array();
436     }
438     /**
439      * Private constructor, can be called only from the factory method.
440      * @param stdClass $config
441      */
442     private function __construct($config) {
443         global $CFG; //needed for included lib.php files
445         $this->settings = $config->settings;
446         $this->name     = $config->name;
447         $this->dir      = $config->dir;
449         if ($this->name != 'base') {
450             $baseconfig = theme_config::find_theme_config('base', $this->settings);
451         } else {
452             $baseconfig = $config;
453         }
455         $configurable = array('parents', 'sheets', 'parents_exclude_sheets', 'plugins_exclude_sheets', 'javascripts', 'javascripts_footer',
456                               'parents_exclude_javascripts', 'layouts', 'enable_dock', 'enablecourseajax', 'supportscssoptimisation',
457                               'rendererfactory', 'csspostprocess', 'editor_sheets', 'rarrow', 'larrow', 'hidefromselector', 'doctype',
458                               'yuicssmodules', 'blockrtlmanipulations', 'lessfile', 'extralesscallback', 'lessvariablescallback',
459                               'blockrendermethod');
461         foreach ($config as $key=>$value) {
462             if (in_array($key, $configurable)) {
463                 $this->$key = $value;
464             }
465         }
467         // verify all parents and load configs and renderers
468         foreach ($this->parents as $parent) {
469             if ($parent == 'base') {
470                 $parent_config = $baseconfig;
471             } else if (!$parent_config = theme_config::find_theme_config($parent, $this->settings)) {
472                 // this is not good - better exclude faulty parents
473                 continue;
474             }
475             $libfile = $parent_config->dir.'/lib.php';
476             if (is_readable($libfile)) {
477                 // theme may store various function here
478                 include_once($libfile);
479             }
480             $renderersfile = $parent_config->dir.'/renderers.php';
481             if (is_readable($renderersfile)) {
482                 // may contain core and plugin renderers and renderer factory
483                 include_once($renderersfile);
484             }
485             $this->parent_configs[$parent] = $parent_config;
486         }
487         $libfile = $this->dir.'/lib.php';
488         if (is_readable($libfile)) {
489             // theme may store various function here
490             include_once($libfile);
491         }
492         $rendererfile = $this->dir.'/renderers.php';
493         if (is_readable($rendererfile)) {
494             // may contain core and plugin renderers and renderer factory
495             include_once($rendererfile);
496         } else {
497             // check if renderers.php file is missnamed renderer.php
498             if (is_readable($this->dir.'/renderer.php')) {
499                 debugging('Developer hint: '.$this->dir.'/renderer.php should be renamed to ' . $this->dir."/renderers.php.
500                     See: http://docs.moodle.org/dev/Output_renderers#Theme_renderers.", DEBUG_DEVELOPER);
501             }
502         }
504         // cascade all layouts properly
505         foreach ($baseconfig->layouts as $layout=>$value) {
506             if (!isset($this->layouts[$layout])) {
507                 foreach ($this->parent_configs as $parent_config) {
508                     if (isset($parent_config->layouts[$layout])) {
509                         $this->layouts[$layout] = $parent_config->layouts[$layout];
510                         continue 2;
511                     }
512                 }
513                 $this->layouts[$layout] = $value;
514             }
515         }
517         //fix arrows if needed
518         $this->check_theme_arrows();
519     }
521     /**
522      * Let the theme initialise the page object (usually $PAGE).
523      *
524      * This may be used for example to request jQuery in add-ons.
525      *
526      * @param moodle_page $page
527      */
528     public function init_page(moodle_page $page) {
529         $themeinitfunction = 'theme_'.$this->name.'_page_init';
530         if (function_exists($themeinitfunction)) {
531             $themeinitfunction($page);
532         }
533     }
535     /**
536      * Checks if arrows $THEME->rarrow, $THEME->larrow have been set (theme/-/config.php).
537      * If not it applies sensible defaults.
538      *
539      * Accessibility: right and left arrow Unicode characters for breadcrumb, calendar,
540      * search forum block, etc. Important: these are 'silent' in a screen-reader
541      * (unlike &gt; &raquo;), and must be accompanied by text.
542      */
543     private function check_theme_arrows() {
544         if (!isset($this->rarrow) and !isset($this->larrow)) {
545             // Default, looks good in Win XP/IE 6, Win/Firefox 1.5, Win/Netscape 8...
546             // Also OK in Win 9x/2K/IE 5.x
547             $this->rarrow = '&#x25BA;';
548             $this->larrow = '&#x25C4;';
549             if (empty($_SERVER['HTTP_USER_AGENT'])) {
550                 $uagent = '';
551             } else {
552                 $uagent = $_SERVER['HTTP_USER_AGENT'];
553             }
554             if (false !== strpos($uagent, 'Opera')
555                 || false !== strpos($uagent, 'Mac')) {
556                 // Looks good in Win XP/Mac/Opera 8/9, Mac/Firefox 2, Camino, Safari.
557                 // Not broken in Mac/IE 5, Mac/Netscape 7 (?).
558                 $this->rarrow = '&#x25B6;';
559                 $this->larrow = '&#x25C0;';
560             }
561             elseif ((false !== strpos($uagent, 'Konqueror'))
562                 || (false !== strpos($uagent, 'Android')))  {
563                 // The fonts on Android don't include the characters required for this to work as expected.
564                 // So we use the same ones Konqueror uses.
565                 $this->rarrow = '&rarr;';
566                 $this->larrow = '&larr;';
567             }
568             elseif (isset($_SERVER['HTTP_ACCEPT_CHARSET'])
569                 && false === stripos($_SERVER['HTTP_ACCEPT_CHARSET'], 'utf-8')) {
570                 // (Win/IE 5 doesn't set ACCEPT_CHARSET, but handles Unicode.)
571                 // To be safe, non-Unicode browsers!
572                 $this->rarrow = '&gt;';
573                 $this->larrow = '&lt;';
574             }
576             // RTL support - in RTL languages, swap r and l arrows
577             if (right_to_left()) {
578                 $t = $this->rarrow;
579                 $this->rarrow = $this->larrow;
580                 $this->larrow = $t;
581             }
582         }
583     }
585     /**
586      * Returns output renderer prefixes, these are used when looking
587      * for the overridden renderers in themes.
588      *
589      * @return array
590      */
591     public function renderer_prefixes() {
592         global $CFG; // just in case the included files need it
594         $prefixes = array('theme_'.$this->name);
596         foreach ($this->parent_configs as $parent) {
597             $prefixes[] = 'theme_'.$parent->name;
598         }
600         return $prefixes;
601     }
603     /**
604      * Returns the stylesheet URL of this editor content
605      *
606      * @param bool $encoded false means use & and true use &amp; in URLs
607      * @return moodle_url
608      */
609     public function editor_css_url($encoded=true) {
610         global $CFG;
611         $rev = theme_get_revision();
612         if ($rev > -1) {
613             $url = new moodle_url("$CFG->httpswwwroot/theme/styles.php");
614             if (!empty($CFG->slasharguments)) {
615                 $url->set_slashargument('/'.$this->name.'/'.$rev.'/editor', 'noparam', true);
616             } else {
617                 $url->params(array('theme'=>$this->name,'rev'=>$rev, 'type'=>'editor'));
618             }
619         } else {
620             $params = array('theme'=>$this->name, 'type'=>'editor');
621             $url = new moodle_url($CFG->httpswwwroot.'/theme/styles_debug.php', $params);
622         }
623         return $url;
624     }
626     /**
627      * Returns the content of the CSS to be used in editor content
628      *
629      * @return array
630      */
631     public function editor_css_files() {
632         $files = array();
634         // First editor plugins.
635         $plugins = core_component::get_plugin_list('editor');
636         foreach ($plugins as $plugin=>$fulldir) {
637             $sheetfile = "$fulldir/editor_styles.css";
638             if (is_readable($sheetfile)) {
639                 $files['plugin_'.$plugin] = $sheetfile;
640             }
641         }
642         // Then parent themes - base first, the immediate parent last.
643         foreach (array_reverse($this->parent_configs) as $parent_config) {
644             if (empty($parent_config->editor_sheets)) {
645                 continue;
646             }
647             foreach ($parent_config->editor_sheets as $sheet) {
648                 $sheetfile = "$parent_config->dir/style/$sheet.css";
649                 if (is_readable($sheetfile)) {
650                     $files['parent_'.$parent_config->name.'_'.$sheet] = $sheetfile;
651                 }
652             }
653         }
654         // Finally this theme.
655         if (!empty($this->editor_sheets)) {
656             foreach ($this->editor_sheets as $sheet) {
657                 $sheetfile = "$this->dir/style/$sheet.css";
658                 if (is_readable($sheetfile)) {
659                     $files['theme_'.$sheet] = $sheetfile;
660                 }
661             }
662         }
664         return $files;
665     }
667     /**
668      * Get the stylesheet URL of this theme.
669      *
670      * @param moodle_page $page Not used... deprecated?
671      * @return moodle_url[]
672      */
673     public function css_urls(moodle_page $page) {
674         global $CFG;
676         $rev = theme_get_revision();
678         $urls = array();
680         $svg = $this->use_svg_icons();
682         if ($rev > -1) {
683             $url = new moodle_url("$CFG->httpswwwroot/theme/styles.php");
684             $separate = (core_useragent::is_ie() && !core_useragent::check_ie_version('10'));
685             if (!empty($CFG->slasharguments)) {
686                 $slashargs = '';
687                 if (!$svg) {
688                     // We add a simple /_s to the start of the path.
689                     // The underscore is used to ensure that it isn't a valid theme name.
690                     $slashargs .= '/_s'.$slashargs;
691                 }
692                 $slashargs .= '/'.$this->name.'/'.$rev.'/all';
693                 if ($separate) {
694                     $slashargs .= '/chunk0';
695                 }
696                 $url->set_slashargument($slashargs, 'noparam', true);
697             } else {
698                 $params = array('theme' => $this->name,'rev' => $rev, 'type' => 'all');
699                 if (!$svg) {
700                     // We add an SVG param so that we know not to serve SVG images.
701                     // We do this because all modern browsers support SVG and this param will one day be removed.
702                     $params['svg'] = '0';
703                 }
704                 if ($separate) {
705                     $params['chunk'] = '0';
706                 }
707                 $url->params($params);
708             }
709             $urls[] = $url;
711         } else {
712             $baseurl = new moodle_url($CFG->httpswwwroot.'/theme/styles_debug.php');
714             $css = $this->get_css_files(true);
715             if (!$svg) {
716                 // We add an SVG param so that we know not to serve SVG images.
717                 // We do this because all modern browsers support SVG and this param will one day be removed.
718                 $baseurl->param('svg', '0');
719             }
720             if (core_useragent::is_ie()) {
721                 // Lalala, IE does not allow more than 31 linked CSS files from main document.
722                 $urls[] = new moodle_url($baseurl, array('theme'=>$this->name, 'type'=>'ie', 'subtype'=>'plugins'));
723                 foreach ($css['parents'] as $parent=>$sheets) {
724                     // We need to serve parents individually otherwise we may easily exceed the style limit IE imposes (4096).
725                     $urls[] = new moodle_url($baseurl, array('theme'=>$this->name,'type'=>'ie', 'subtype'=>'parents', 'sheet'=>$parent));
726                 }
727                 if (!empty($this->lessfile)) {
728                     // No need to define the type as IE here.
729                     $urls[] = new moodle_url($baseurl, array('theme' => $this->name, 'type' => 'less', 'chunk' => 0));
730                 }
731                 $urls[] = new moodle_url($baseurl, array('theme'=>$this->name, 'type'=>'ie', 'subtype'=>'theme'));
733             } else {
734                 foreach ($css['plugins'] as $plugin=>$unused) {
735                     $urls[] = new moodle_url($baseurl, array('theme'=>$this->name,'type'=>'plugin', 'subtype'=>$plugin));
736                 }
737                 foreach ($css['parents'] as $parent=>$sheets) {
738                     foreach ($sheets as $sheet=>$unused2) {
739                         $urls[] = new moodle_url($baseurl, array('theme'=>$this->name,'type'=>'parent', 'subtype'=>$parent, 'sheet'=>$sheet));
740                     }
741                 }
742                 foreach ($css['theme'] as $sheet => $filename) {
743                     if ($sheet === $this->lessfile) {
744                         // This is the theme LESS file.
745                         $urls[] = new moodle_url($baseurl, array('theme' => $this->name, 'type' => 'less'));
746                     } else {
747                         // Sheet first in order to make long urls easier to read.
748                         $urls[] = new moodle_url($baseurl, array('sheet'=>$sheet, 'theme'=>$this->name, 'type'=>'theme'));
749                     }
750                 }
751             }
752         }
754         return $urls;
755     }
757     /**
758      * Get the whole css stylesheet for production mode.
759      *
760      * NOTE: this method is not expected to be used from any addons.
761      *
762      * @return string CSS markup, already optimised and compressed
763      */
764     public function get_css_content() {
765         global $CFG;
766         require_once($CFG->dirroot.'/lib/csslib.php');
768         $csscontent = '';
769         foreach ($this->get_css_files(false) as $type => $value) {
770             foreach ($value as $identifier => $val) {
771                 if (is_array($val)) {
772                     foreach ($val as $v) {
773                         $csscontent .= file_get_contents($v) . "\n";
774                     }
775                 } else {
776                     if ($type === 'theme' && $identifier === $this->lessfile) {
777                         // We need the content from LESS because this is the LESS file from the theme.
778                         $csscontent .= $this->get_css_content_from_less(false);
779                     } else {
780                         $csscontent .= file_get_contents($val) . "\n";
781                     }
782                 }
783             }
784         }
785         $csscontent = $this->post_process($csscontent);
787         if (!empty($CFG->enablecssoptimiser) && $this->supportscssoptimisation) {
788             // This is an experimental feature introduced in Moodle 2.3
789             // The CSS optimiser organises the CSS in order to reduce the overall number
790             // of rules and styles being sent to the client. It does this by collating
791             // the CSS before it is cached removing excess styles and rules and stripping
792             // out any extraneous content such as comments and empty rules.
793             $optimiser = new css_optimiser();
794             $csscontent = $optimiser->process($csscontent);
796         } else {
797             $csscontent = core_minify::css($csscontent);
798         }
800         return $csscontent;
801     }
803     /**
804      * Get the theme designer css markup,
805      * the parameters are coming from css_urls().
806      *
807      * NOTE: this method is not expected to be used from any addons.
808      *
809      * @param string $type
810      * @param string $subtype
811      * @param string $sheet
812      * @return string CSS markup
813      */
814     public function get_css_content_debug($type, $subtype, $sheet) {
815         global $CFG;
816         require_once($CFG->dirroot.'/lib/csslib.php');
818         // The LESS file of the theme is requested.
819         if ($type === 'less') {
820             $csscontent = $this->get_css_content_from_less(true);
821             if ($csscontent !== false) {
822                 return $csscontent;
823             }
824             return '';
825         }
827         $optimiser = null;
828         if (!empty($CFG->enablecssoptimiser) && $this->supportscssoptimisation) {
829             // This is an experimental feature introduced in Moodle 2.3
830             // The CSS optimiser organises the CSS in order to reduce the overall number
831             // of rules and styles being sent to the client. It does this by collating
832             // the CSS before it is cached removing excess styles and rules and stripping
833             // out any extraneous content such as comments and empty rules.
834             $optimiser = new css_optimiser();
835         }
837         $cssfiles = array();
838         $css = $this->get_css_files(true);
840         if ($type === 'ie') {
841             // IE is a sloppy browser with weird limits, sorry.
842             if ($subtype === 'plugins') {
843                 $cssfiles = $css['plugins'];
845             } else if ($subtype === 'parents') {
846                 if (empty($sheet)) {
847                     // Do not bother with the empty parent here.
848                 } else {
849                     // Build up the CSS for that parent so we can serve it as one file.
850                     foreach ($css[$subtype][$sheet] as $parent => $css) {
851                         $cssfiles[] = $css;
852                     }
853                 }
854             } else if ($subtype === 'theme') {
855                 $cssfiles = $css['theme'];
856             }
858         } else if ($type === 'plugin') {
859             if (isset($css['plugins'][$subtype])) {
860                 $cssfiles[] = $css['plugins'][$subtype];
861             }
863         } else if ($type === 'parent') {
864             if (isset($css['parents'][$subtype][$sheet])) {
865                 $cssfiles[] = $css['parents'][$subtype][$sheet];
866             }
868         } else if ($type === 'theme') {
869             if (isset($css['theme'][$sheet])) {
870                 $cssfiles[] = $css['theme'][$sheet];
871             }
872         }
874         $csscontent = '';
875         foreach ($cssfiles as $file) {
876             $contents = file_get_contents($file);
877             $contents = $this->post_process($contents);
878             $comment = "/** Path: $type $subtype $sheet.' **/\n";
879             $stats = '';
880             if ($optimiser) {
881                 $contents = $optimiser->process($contents);
882                 if (!empty($CFG->cssoptimiserstats)) {
883                     $stats = $optimiser->output_stats_css();
884                 }
885             }
886             $csscontent .= $comment.$stats.$contents."\n\n";
887         }
889         return $csscontent;
890     }
892     /**
893      * Get the whole css stylesheet for editor iframe.
894      *
895      * NOTE: this method is not expected to be used from any addons.
896      *
897      * @return string CSS markup
898      */
899     public function get_css_content_editor() {
900         // Do not bother to optimise anything here, just very basic stuff.
901         $cssfiles = $this->editor_css_files();
902         $css = '';
903         foreach ($cssfiles as $file) {
904             $css .= file_get_contents($file)."\n";
905         }
906         return $this->post_process($css);
907     }
909     /**
910      * Returns an array of organised CSS files required for this output.
911      *
912      * @param bool $themedesigner
913      * @return array nested array of file paths
914      */
915     protected function get_css_files($themedesigner) {
916         global $CFG;
918         $cache = null;
919         $cachekey = 'cssfiles';
920         if ($themedesigner) {
921             require_once($CFG->dirroot.'/lib/csslib.php');
922             // We need some kind of caching here because otherwise the page navigation becomes
923             // way too slow in theme designer mode. Feel free to create full cache definition later...
924             $cache = cache::make_from_params(cache_store::MODE_APPLICATION, 'core', 'themedesigner', array('theme' => $this->name));
925             if ($files = $cache->get($cachekey)) {
926                 if ($files['created'] > time() - THEME_DESIGNER_CACHE_LIFETIME) {
927                     unset($files['created']);
928                     return $files;
929                 }
930             }
931         }
933         $cssfiles = array('plugins'=>array(), 'parents'=>array(), 'theme'=>array());
935         // Get all plugin sheets.
936         $excludes = $this->resolve_excludes('plugins_exclude_sheets');
937         if ($excludes !== true) {
938             foreach (core_component::get_plugin_types() as $type=>$unused) {
939                 if ($type === 'theme' || (!empty($excludes[$type]) and $excludes[$type] === true)) {
940                     continue;
941                 }
942                 $plugins = core_component::get_plugin_list($type);
943                 foreach ($plugins as $plugin=>$fulldir) {
944                     if (!empty($excludes[$type]) and is_array($excludes[$type])
945                             and in_array($plugin, $excludes[$type])) {
946                         continue;
947                     }
949                     // Get the CSS from the plugin.
950                     $sheetfile = "$fulldir/styles.css";
951                     if (is_readable($sheetfile)) {
952                         $cssfiles['plugins'][$type.'_'.$plugin] = $sheetfile;
953                     }
955                     // Create a list of candidate sheets from parents (direct parent last) and current theme.
956                     $candidates = array();
957                     foreach (array_reverse($this->parent_configs) as $parent_config) {
958                         $candidates[] = $parent_config->name;
959                     }
960                     $candidates[] = $this->name;
962                     // Add the sheets found.
963                     foreach ($candidates as $candidate) {
964                         $sheetthemefile = "$fulldir/styles_{$candidate}.css";
965                         if (is_readable($sheetthemefile)) {
966                             $cssfiles['plugins'][$type.'_'.$plugin.'_'.$candidate] = $sheetthemefile;
967                         }
968                     }
969                 }
970             }
971         }
973         // Find out wanted parent sheets.
974         $excludes = $this->resolve_excludes('parents_exclude_sheets');
975         if ($excludes !== true) {
976             foreach (array_reverse($this->parent_configs) as $parent_config) { // Base first, the immediate parent last.
977                 $parent = $parent_config->name;
978                 if (empty($parent_config->sheets) || (!empty($excludes[$parent]) and $excludes[$parent] === true)) {
979                     continue;
980                 }
981                 foreach ($parent_config->sheets as $sheet) {
982                     if (!empty($excludes[$parent]) && is_array($excludes[$parent])
983                             && in_array($sheet, $excludes[$parent])) {
984                         continue;
985                     }
987                     // We never refer to the parent LESS files.
988                     $sheetfile = "$parent_config->dir/style/$sheet.css";
989                     if (is_readable($sheetfile)) {
990                         $cssfiles['parents'][$parent][$sheet] = $sheetfile;
991                     }
992                 }
993             }
994         }
996         // Current theme sheets and less file.
997         // We first add the LESS files because we want the CSS ones to be included after the
998         // LESS code. However, if both the LESS file and the CSS file share the same name,
999         // the CSS file is ignored.
1000         if (!empty($this->lessfile)) {
1001             $sheetfile = "{$this->dir}/less/{$this->lessfile}.less";
1002             if (is_readable($sheetfile)) {
1003                 $cssfiles['theme'][$this->lessfile] = $sheetfile;
1004             }
1005         }
1006         if (is_array($this->sheets)) {
1007             foreach ($this->sheets as $sheet) {
1008                 $sheetfile = "$this->dir/style/$sheet.css";
1009                 if (is_readable($sheetfile) && !isset($cssfiles['theme'][$sheet])) {
1010                     $cssfiles['theme'][$sheet] = $sheetfile;
1011                 }
1012             }
1013         }
1015         if ($cache) {
1016             $files = $cssfiles;
1017             $files['created'] = time();
1018             $cache->set($cachekey, $files);
1019         }
1020         return $cssfiles;
1021     }
1023     /**
1024      * Return the CSS content generated from LESS the file.
1025      *
1026      * @param bool $themedesigner True if theme designer is enabled.
1027      * @return bool|string Return false when the compilation failed. Else the compiled string.
1028      */
1029     protected function get_css_content_from_less($themedesigner) {
1031         $lessfile = $this->lessfile;
1032         if (!$lessfile || !is_readable($this->dir . '/less/' . $lessfile . '.less')) {
1033             throw new coding_exception('The theme did not define a LESS file, or it is not readable.');
1034         }
1036         // We might need more memory to do this, so let's play safe.
1037         raise_memory_limit(MEMORY_EXTRA);
1039         // Files list.
1040         $files = $this->get_css_files($themedesigner);
1042         // Get the LESS file path.
1043         $themelessfile = $files['theme'][$lessfile];
1045         // Setup compiler options.
1046         $options = array(
1047             // We need to set the import directory to where $lessfile is.
1048             'import_dirs' => array(dirname($themelessfile) => '/'),
1049             // Always disable default caching.
1050             'cache_method' => false,
1051             // Disable the relative URLs, we have post_process() to handle that.
1052             'relativeUrls' => false,
1053         );
1055         if ($themedesigner) {
1056             // Add the sourceMap inline to ensure that it is atomically generated.
1057             $options['sourceMap'] = true;
1058             $options['sourceRoot'] = 'theme';
1059         }
1061         // Instantiate the compiler.
1062         $compiler = new core_lessc($options);
1064         try {
1065             $compiler->parse_file_content($themelessfile);
1067             // Get the callbacks.
1068             $compiler->parse($this->get_extra_less_code());
1069             $compiler->ModifyVars($this->get_less_variables());
1071             // Compile the CSS.
1072             $compiled = $compiler->getCss();
1074             // Post process the entire thing.
1075             $compiled = $this->post_process($compiled);
1076         } catch (Less_Exception_Parser $e) {
1077             $compiled = false;
1078             debugging('Error while compiling LESS ' . $lessfile . ' file: ' . $e->getMessage(), DEBUG_DEVELOPER);
1079         }
1081         // Try to save memory.
1082         $compiler = null;
1083         unset($compiler);
1085         return $compiled;
1086     }
1088     /**
1089      * Return extra LESS variables to use when compiling.
1090      *
1091      * @return array Where keys are the variable names (omitting the @), and the values are the value.
1092      */
1093     protected function get_less_variables() {
1094         $variables = array();
1096         // Getting all the candidate functions.
1097         $candidates = array();
1098         foreach ($this->parent_configs as $parent_config) {
1099             if (!isset($parent_config->lessvariablescallback)) {
1100                 continue;
1101             }
1102             $candidates[] = $parent_config->lessvariablescallback;
1103         }
1104         $candidates[] = $this->lessvariablescallback;
1106         // Calling the functions.
1107         foreach ($candidates as $function) {
1108             if (function_exists($function)) {
1109                 $vars = $function($this);
1110                 if (!is_array($vars)) {
1111                     debugging('Callback ' . $function . ' did not return an array() as expected', DEBUG_DEVELOPER);
1112                     continue;
1113                 }
1114                 $variables = array_merge($variables, $vars);
1115             }
1116         }
1118         return $variables;
1119     }
1121     /**
1122      * Return extra LESS code to add when compiling.
1123      *
1124      * This is intended to be used by themes to inject some LESS code
1125      * before it gets compiled. If you want to inject variables you
1126      * should use {@link self::get_less_variables()}.
1127      *
1128      * @return string The LESS code to inject.
1129      */
1130     protected function get_extra_less_code() {
1131         $content = '';
1133         // Getting all the candidate functions.
1134         $candidates = array();
1135         foreach ($this->parent_configs as $parent_config) {
1136             if (!isset($parent_config->extralesscallback)) {
1137                 continue;
1138             }
1139             $candidates[] = $parent_config->extralesscallback;
1140         }
1141         $candidates[] = $this->extralesscallback;
1143         // Calling the functions.
1144         foreach ($candidates as $function) {
1145             if (function_exists($function)) {
1146                 $content .= "\n/** Extra LESS from $function **/\n" . $function($this) . "\n";
1147             }
1148         }
1150         return $content;
1151     }
1153     /**
1154      * Generate a URL to the file that serves theme JavaScript files.
1155      *
1156      * If we determine that the theme has no relevant files, then we return
1157      * early with a null value.
1158      *
1159      * @param bool $inhead true means head url, false means footer
1160      * @return moodle_url|null
1161      */
1162     public function javascript_url($inhead) {
1163         global $CFG;
1165         $rev = theme_get_revision();
1166         $params = array('theme'=>$this->name,'rev'=>$rev);
1167         $params['type'] = $inhead ? 'head' : 'footer';
1169         // Return early if there are no files to serve
1170         if (count($this->javascript_files($params['type'])) === 0) {
1171             return null;
1172         }
1174         if (!empty($CFG->slasharguments) and $rev > 0) {
1175             $url = new moodle_url("$CFG->httpswwwroot/theme/javascript.php");
1176             $url->set_slashargument('/'.$this->name.'/'.$rev.'/'.$params['type'], 'noparam', true);
1177             return $url;
1178         } else {
1179             return new moodle_url($CFG->httpswwwroot.'/theme/javascript.php', $params);
1180         }
1181     }
1183     /**
1184      * Get the URL's for the JavaScript files used by this theme.
1185      * They won't be served directly, instead they'll be mediated through
1186      * theme/javascript.php.
1187      *
1188      * @param string $type Either javascripts_footer, or javascripts
1189      * @return array
1190      */
1191     public function javascript_files($type) {
1192         if ($type === 'footer') {
1193             $type = 'javascripts_footer';
1194         } else {
1195             $type = 'javascripts';
1196         }
1198         $js = array();
1199         // find out wanted parent javascripts
1200         $excludes = $this->resolve_excludes('parents_exclude_javascripts');
1201         if ($excludes !== true) {
1202             foreach (array_reverse($this->parent_configs) as $parent_config) { // base first, the immediate parent last
1203                 $parent = $parent_config->name;
1204                 if (empty($parent_config->$type)) {
1205                     continue;
1206                 }
1207                 if (!empty($excludes[$parent]) and $excludes[$parent] === true) {
1208                     continue;
1209                 }
1210                 foreach ($parent_config->$type as $javascript) {
1211                     if (!empty($excludes[$parent]) and is_array($excludes[$parent])
1212                         and in_array($javascript, $excludes[$parent])) {
1213                         continue;
1214                     }
1215                     $javascriptfile = "$parent_config->dir/javascript/$javascript.js";
1216                     if (is_readable($javascriptfile)) {
1217                         $js[] = $javascriptfile;
1218                     }
1219                 }
1220             }
1221         }
1223         // current theme javascripts
1224         if (is_array($this->$type)) {
1225             foreach ($this->$type as $javascript) {
1226                 $javascriptfile = "$this->dir/javascript/$javascript.js";
1227                 if (is_readable($javascriptfile)) {
1228                     $js[] = $javascriptfile;
1229                 }
1230             }
1231         }
1232         return $js;
1233     }
1235     /**
1236      * Resolves an exclude setting to the themes setting is applicable or the
1237      * setting of its closest parent.
1238      *
1239      * @param string $variable The name of the setting the exclude setting to resolve
1240      * @param string $default
1241      * @return mixed
1242      */
1243     protected function resolve_excludes($variable, $default = null) {
1244         $setting = $default;
1245         if (is_array($this->{$variable}) or $this->{$variable} === true) {
1246             $setting = $this->{$variable};
1247         } else {
1248             foreach ($this->parent_configs as $parent_config) { // the immediate parent first, base last
1249                 if (!isset($parent_config->{$variable})) {
1250                     continue;
1251                 }
1252                 if (is_array($parent_config->{$variable}) or $parent_config->{$variable} === true) {
1253                     $setting = $parent_config->{$variable};
1254                     break;
1255                 }
1256             }
1257         }
1258         return $setting;
1259     }
1261     /**
1262      * Returns the content of the one huge javascript file merged from all theme javascript files.
1263      *
1264      * @param bool $type
1265      * @return string
1266      */
1267     public function javascript_content($type) {
1268         $jsfiles = $this->javascript_files($type);
1269         $js = '';
1270         foreach ($jsfiles as $jsfile) {
1271             $js .= file_get_contents($jsfile)."\n";
1272         }
1273         return $js;
1274     }
1276     /**
1277      * Post processes CSS.
1278      *
1279      * This method post processes all of the CSS before it is served for this theme.
1280      * This is done so that things such as image URL's can be swapped in and to
1281      * run any specific CSS post process method the theme has requested.
1282      * This allows themes to use CSS settings.
1283      *
1284      * @param string $css The CSS to process.
1285      * @return string The processed CSS.
1286      */
1287     public function post_process($css) {
1288         // now resolve all image locations
1289         if (preg_match_all('/\[\[pix:([a-z0-9_]+\|)?([^\]]+)\]\]/', $css, $matches, PREG_SET_ORDER)) {
1290             $replaced = array();
1291             foreach ($matches as $match) {
1292                 if (isset($replaced[$match[0]])) {
1293                     continue;
1294                 }
1295                 $replaced[$match[0]] = true;
1296                 $imagename = $match[2];
1297                 $component = rtrim($match[1], '|');
1298                 $imageurl = $this->pix_url($imagename, $component)->out(false);
1299                  // we do not need full url because the image.php is always in the same dir
1300                 $imageurl = preg_replace('|^http.?://[^/]+|', '', $imageurl);
1301                 $css = str_replace($match[0], $imageurl, $css);
1302             }
1303         }
1305         // Now resolve all font locations.
1306         if (preg_match_all('/\[\[font:([a-z0-9_]+\|)?([^\]]+)\]\]/', $css, $matches, PREG_SET_ORDER)) {
1307             $replaced = array();
1308             foreach ($matches as $match) {
1309                 if (isset($replaced[$match[0]])) {
1310                     continue;
1311                 }
1312                 $replaced[$match[0]] = true;
1313                 $fontname = $match[2];
1314                 $component = rtrim($match[1], '|');
1315                 $fonturl = $this->font_url($fontname, $component)->out(false);
1316                 // We do not need full url because the font.php is always in the same dir.
1317                 $fonturl = preg_replace('|^http.?://[^/]+|', '', $fonturl);
1318                 $css = str_replace($match[0], $fonturl, $css);
1319             }
1320         }
1322         // now resolve all theme settings or do any other postprocessing
1323         $csspostprocess = $this->csspostprocess;
1324         if (function_exists($csspostprocess)) {
1325             $css = $csspostprocess($css, $this);
1326         }
1328         return $css;
1329     }
1331     /**
1332      * Return the URL for an image
1333      *
1334      * @param string $imagename the name of the icon.
1335      * @param string $component specification of one plugin like in get_string()
1336      * @return moodle_url
1337      */
1338     public function pix_url($imagename, $component) {
1339         global $CFG;
1341         $params = array('theme'=>$this->name);
1342         $svg = $this->use_svg_icons();
1344         if (empty($component) or $component === 'moodle' or $component === 'core') {
1345             $params['component'] = 'core';
1346         } else {
1347             $params['component'] = $component;
1348         }
1350         $rev = theme_get_revision();
1351         if ($rev != -1) {
1352             $params['rev'] = $rev;
1353         }
1355         $params['image'] = $imagename;
1357         $url = new moodle_url("$CFG->httpswwwroot/theme/image.php");
1358         if (!empty($CFG->slasharguments) and $rev > 0) {
1359             $path = '/'.$params['theme'].'/'.$params['component'].'/'.$params['rev'].'/'.$params['image'];
1360             if (!$svg) {
1361                 // We add a simple /_s to the start of the path.
1362                 // The underscore is used to ensure that it isn't a valid theme name.
1363                 $path = '/_s'.$path;
1364             }
1365             $url->set_slashargument($path, 'noparam', true);
1366         } else {
1367             if (!$svg) {
1368                 // We add an SVG param so that we know not to serve SVG images.
1369                 // We do this because all modern browsers support SVG and this param will one day be removed.
1370                 $params['svg'] = '0';
1371             }
1372             $url->params($params);
1373         }
1375         return $url;
1376     }
1378     /**
1379      * Return the URL for a font
1380      *
1381      * @param string $font the name of the font (including extension).
1382      * @param string $component specification of one plugin like in get_string()
1383      * @return moodle_url
1384      */
1385     public function font_url($font, $component) {
1386         global $CFG;
1388         $params = array('theme'=>$this->name);
1390         if (empty($component) or $component === 'moodle' or $component === 'core') {
1391             $params['component'] = 'core';
1392         } else {
1393             $params['component'] = $component;
1394         }
1396         $rev = theme_get_revision();
1397         if ($rev != -1) {
1398             $params['rev'] = $rev;
1399         }
1401         $params['font'] = $font;
1403         $url = new moodle_url("$CFG->httpswwwroot/theme/font.php");
1404         if (!empty($CFG->slasharguments) and $rev > 0) {
1405             $path = '/'.$params['theme'].'/'.$params['component'].'/'.$params['rev'].'/'.$params['font'];
1406             $url->set_slashargument($path, 'noparam', true);
1407         } else {
1408             $url->params($params);
1409         }
1411         return $url;
1412     }
1414     /**
1415      * Returns URL to the stored file via pluginfile.php.
1416      *
1417      * Note the theme must also implement pluginfile.php handler,
1418      * theme revision is used instead of the itemid.
1419      *
1420      * @param string $setting
1421      * @param string $filearea
1422      * @return string protocol relative URL or null if not present
1423      */
1424     public function setting_file_url($setting, $filearea) {
1425         global $CFG;
1427         if (empty($this->settings->$setting)) {
1428             return null;
1429         }
1431         $component = 'theme_'.$this->name;
1432         $itemid = theme_get_revision();
1433         $filepath = $this->settings->$setting;
1434         $syscontext = context_system::instance();
1436         $url = moodle_url::make_file_url("$CFG->wwwroot/pluginfile.php", "/$syscontext->id/$component/$filearea/$itemid".$filepath);
1438         // Now this is tricky because the we can not hardcode http or https here, lets use the relative link.
1439         // Note: unfortunately moodle_url does not support //urls yet.
1441         $url = preg_replace('|^https?://|i', '//', $url->out(false));
1443         return $url;
1444     }
1446     /**
1447      * Serve the theme setting file.
1448      *
1449      * @param string $filearea
1450      * @param array $args
1451      * @param bool $forcedownload
1452      * @param array $options
1453      * @return bool may terminate if file not found or donotdie not specified
1454      */
1455     public function setting_file_serve($filearea, $args, $forcedownload, $options) {
1456         global $CFG;
1457         require_once("$CFG->libdir/filelib.php");
1459         $syscontext = context_system::instance();
1460         $component = 'theme_'.$this->name;
1462         $revision = array_shift($args);
1463         if ($revision < 0) {
1464             $lifetime = 0;
1465         } else {
1466             $lifetime = 60*60*24*60;
1467         }
1469         $fs = get_file_storage();
1470         $relativepath = implode('/', $args);
1472         $fullpath = "/{$syscontext->id}/{$component}/{$filearea}/0/{$relativepath}";
1473         $fullpath = rtrim($fullpath, '/');
1474         if ($file = $fs->get_file_by_hash(sha1($fullpath))) {
1475             send_stored_file($file, $lifetime, 0, $forcedownload, $options);
1476             return true;
1477         } else {
1478             send_file_not_found();
1479         }
1480     }
1482     /**
1483      * Resolves the real image location.
1484      *
1485      * $svg was introduced as an arg in 2.4. It is important because not all supported browsers support the use of SVG
1486      * and we need a way in which to turn it off.
1487      * By default SVG won't be used unless asked for. This is done for two reasons:
1488      *   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
1489      *      browser supports SVG.
1490      *   2. We only serve SVG images from locations we trust. This must NOT include any areas where the image may have been uploaded
1491      *      by the user due to security concerns.
1492      *
1493      * @param string $image name of image, may contain relative path
1494      * @param string $component
1495      * @param bool $svg If set to true SVG images will also be looked for.
1496      * @return string full file path
1497      */
1498     public function resolve_image_location($image, $component, $svg = false) {
1499         global $CFG;
1501         if (!is_bool($svg)) {
1502             // If $svg isn't a bool then we need to decide for ourselves.
1503             $svg = $this->use_svg_icons();
1504         }
1506         if ($component === 'moodle' or $component === 'core' or empty($component)) {
1507             if ($imagefile = $this->image_exists("$this->dir/pix_core/$image", $svg)) {
1508                 return $imagefile;
1509             }
1510             foreach (array_reverse($this->parent_configs) as $parent_config) { // base first, the immediate parent last
1511                 if ($imagefile = $this->image_exists("$parent_config->dir/pix_core/$image", $svg)) {
1512                     return $imagefile;
1513                 }
1514             }
1515             if ($imagefile = $this->image_exists("$CFG->dataroot/pix/$image", $svg)) {
1516                 return $imagefile;
1517             }
1518             if ($imagefile = $this->image_exists("$CFG->dirroot/pix/$image", $svg)) {
1519                 return $imagefile;
1520             }
1521             return null;
1523         } else if ($component === 'theme') { //exception
1524             if ($image === 'favicon') {
1525                 return "$this->dir/pix/favicon.ico";
1526             }
1527             if ($imagefile = $this->image_exists("$this->dir/pix/$image", $svg)) {
1528                 return $imagefile;
1529             }
1530             foreach (array_reverse($this->parent_configs) as $parent_config) { // base first, the immediate parent last
1531                 if ($imagefile = $this->image_exists("$parent_config->dir/pix/$image", $svg)) {
1532                     return $imagefile;
1533                 }
1534             }
1535             return null;
1537         } else {
1538             if (strpos($component, '_') === false) {
1539                 $component = 'mod_'.$component;
1540             }
1541             list($type, $plugin) = explode('_', $component, 2);
1543             if ($imagefile = $this->image_exists("$this->dir/pix_plugins/$type/$plugin/$image", $svg)) {
1544                 return $imagefile;
1545             }
1546             foreach (array_reverse($this->parent_configs) as $parent_config) { // base first, the immediate parent last
1547                 if ($imagefile = $this->image_exists("$parent_config->dir/pix_plugins/$type/$plugin/$image", $svg)) {
1548                     return $imagefile;
1549                 }
1550             }
1551             if ($imagefile = $this->image_exists("$CFG->dataroot/pix_plugins/$type/$plugin/$image", $svg)) {
1552                 return $imagefile;
1553             }
1554             $dir = core_component::get_plugin_directory($type, $plugin);
1555             if ($imagefile = $this->image_exists("$dir/pix/$image", $svg)) {
1556                 return $imagefile;
1557             }
1558             return null;
1559         }
1560     }
1562     /**
1563      * Resolves the real font location.
1564      *
1565      * @param string $font name of font file
1566      * @param string $component
1567      * @return string full file path
1568      */
1569     public function resolve_font_location($font, $component) {
1570         global $CFG;
1572         if ($component === 'moodle' or $component === 'core' or empty($component)) {
1573             if (file_exists("$this->dir/fonts_core/$font")) {
1574                 return "$this->dir/fonts_core/$font";
1575             }
1576             foreach (array_reverse($this->parent_configs) as $parent_config) { // Base first, the immediate parent last.
1577                 if (file_exists("$parent_config->dir/fonts_core/$font")) {
1578                     return "$parent_config->dir/fonts_core/$font";
1579                 }
1580             }
1581             if (file_exists("$CFG->dataroot/fonts/$font")) {
1582                 return "$CFG->dataroot/fonts/$font";
1583             }
1584             if (file_exists("$CFG->dirroot/lib/fonts/$font")) {
1585                 return "$CFG->dirroot/lib/fonts/$font";
1586             }
1587             return null;
1589         } else if ($component === 'theme') { // Exception.
1590             if (file_exists("$this->dir/fonts/$font")) {
1591                 return "$this->dir/fonts/$font";
1592             }
1593             foreach (array_reverse($this->parent_configs) as $parent_config) { // Base first, the immediate parent last.
1594                 if (file_exists("$parent_config->dir/fonts/$font")) {
1595                     return "$parent_config->dir/fonts/$font";
1596                 }
1597             }
1598             return null;
1600         } else {
1601             if (strpos($component, '_') === false) {
1602                 $component = 'mod_'.$component;
1603             }
1604             list($type, $plugin) = explode('_', $component, 2);
1606             if (file_exists("$this->dir/fonts_plugins/$type/$plugin/$font")) {
1607                 return "$this->dir/fonts_plugins/$type/$plugin/$font";
1608             }
1609             foreach (array_reverse($this->parent_configs) as $parent_config) { // Base first, the immediate parent last.
1610                 if (file_exists("$parent_config->dir/fonts_plugins/$type/$plugin/$font")) {
1611                     return "$parent_config->dir/fonts_plugins/$type/$plugin/$font";
1612                 }
1613             }
1614             if (file_exists("$CFG->dataroot/fonts_plugins/$type/$plugin/$font")) {
1615                 return "$CFG->dataroot/fonts_plugins/$type/$plugin/$font";
1616             }
1617             $dir = core_component::get_plugin_directory($type, $plugin);
1618             if (file_exists("$dir/fonts/$font")) {
1619                 return "$dir/fonts/$font";
1620             }
1621             return null;
1622         }
1623     }
1625     /**
1626      * Return true if we should look for SVG images as well.
1627      *
1628      * @return bool
1629      */
1630     public function use_svg_icons() {
1631         global $CFG;
1632         if ($this->usesvg === null) {
1634             if (!isset($CFG->svgicons) || !is_bool($CFG->svgicons)) {
1635                 $this->usesvg = core_useragent::supports_svg();
1636             } else {
1637                 // Force them on/off depending upon the setting.
1638                 $this->usesvg = $CFG->svgicons;
1639             }
1640         }
1641         return $this->usesvg;
1642     }
1644     /**
1645      * Forces the usesvg setting to either true or false, avoiding any decision making.
1646      *
1647      * This function should only ever be used when absolutely required, and before any generation of image URL's has occurred.
1648      * DO NOT ABUSE THIS FUNCTION... not that you'd want to right ;)
1649      *
1650      * @param bool $setting True to force the use of svg when available, null otherwise.
1651      */
1652     public function force_svg_use($setting) {
1653         $this->usesvg = (bool)$setting;
1654     }
1656     /**
1657      * Checks if file with any image extension exists.
1658      *
1659      * The order to these images was adjusted prior to the release of 2.4
1660      * At that point the were the following image counts in Moodle core:
1661      *
1662      *     - png = 667 in pix dirs (1499 total)
1663      *     - gif = 385 in pix dirs (606 total)
1664      *     - jpg = 62  in pix dirs (74 total)
1665      *     - jpeg = 0  in pix dirs (1 total)
1666      *
1667      * There is work in progress to move towards SVG presently hence that has been prioritiesed.
1668      *
1669      * @param string $filepath
1670      * @param bool $svg If set to true SVG images will also be looked for.
1671      * @return string image name with extension
1672      */
1673     private static function image_exists($filepath, $svg = false) {
1674         if ($svg && file_exists("$filepath.svg")) {
1675             return "$filepath.svg";
1676         } else  if (file_exists("$filepath.png")) {
1677             return "$filepath.png";
1678         } else if (file_exists("$filepath.gif")) {
1679             return "$filepath.gif";
1680         } else  if (file_exists("$filepath.jpg")) {
1681             return "$filepath.jpg";
1682         } else  if (file_exists("$filepath.jpeg")) {
1683             return "$filepath.jpeg";
1684         } else {
1685             return false;
1686         }
1687     }
1689     /**
1690      * Loads the theme config from config.php file.
1691      *
1692      * @param string $themename
1693      * @param stdClass $settings from config_plugins table
1694      * @param boolean $parentscheck true to also check the parents.    .
1695      * @return stdClass The theme configuration
1696      */
1697     private static function find_theme_config($themename, $settings, $parentscheck = true) {
1698         // We have to use the variable name $THEME (upper case) because that
1699         // is what is used in theme config.php files.
1701         if (!$dir = theme_config::find_theme_location($themename)) {
1702             return null;
1703         }
1705         $THEME = new stdClass();
1706         $THEME->name     = $themename;
1707         $THEME->dir      = $dir;
1708         $THEME->settings = $settings;
1710         global $CFG; // just in case somebody tries to use $CFG in theme config
1711         include("$THEME->dir/config.php");
1713         // verify the theme configuration is OK
1714         if (!is_array($THEME->parents)) {
1715             // parents option is mandatory now
1716             return null;
1717         } else {
1718             // We use $parentscheck to only check the direct parents (avoid infinite loop).
1719             if ($parentscheck) {
1720                 // Find all parent theme configs.
1721                 foreach ($THEME->parents as $parent) {
1722                     $parentconfig = theme_config::find_theme_config($parent, $settings, false);
1723                     if (empty($parentconfig)) {
1724                         return null;
1725                     }
1726                 }
1727             }
1728         }
1730         return $THEME;
1731     }
1733     /**
1734      * Finds the theme location and verifies the theme has all needed files
1735      * and is not obsoleted.
1736      *
1737      * @param string $themename
1738      * @return string full dir path or null if not found
1739      */
1740     private static function find_theme_location($themename) {
1741         global $CFG;
1743         if (file_exists("$CFG->dirroot/theme/$themename/config.php")) {
1744             $dir = "$CFG->dirroot/theme/$themename";
1746         } else if (!empty($CFG->themedir) and file_exists("$CFG->themedir/$themename/config.php")) {
1747             $dir = "$CFG->themedir/$themename";
1749         } else {
1750             return null;
1751         }
1753         if (file_exists("$dir/styles.php")) {
1754             //legacy theme - needs to be upgraded - upgrade info is displayed on the admin settings page
1755             return null;
1756         }
1758         return $dir;
1759     }
1761     /**
1762      * Get the renderer for a part of Moodle for this theme.
1763      *
1764      * @param moodle_page $page the page we are rendering
1765      * @param string $component the name of part of moodle. E.g. 'core', 'quiz', 'qtype_multichoice'.
1766      * @param string $subtype optional subtype such as 'news' resulting to 'mod_forum_news'
1767      * @param string $target one of rendering target constants
1768      * @return renderer_base the requested renderer.
1769      */
1770     public function get_renderer(moodle_page $page, $component, $subtype = null, $target = null) {
1771         if (is_null($this->rf)) {
1772             $classname = $this->rendererfactory;
1773             $this->rf = new $classname($this);
1774         }
1776         return $this->rf->get_renderer($page, $component, $subtype, $target);
1777     }
1779     /**
1780      * Get the information from {@link $layouts} for this type of page.
1781      *
1782      * @param string $pagelayout the the page layout name.
1783      * @return array the appropriate part of {@link $layouts}.
1784      */
1785     protected function layout_info_for_page($pagelayout) {
1786         if (array_key_exists($pagelayout, $this->layouts)) {
1787             return $this->layouts[$pagelayout];
1788         } else {
1789             debugging('Invalid page layout specified: ' . $pagelayout);
1790             return $this->layouts['standard'];
1791         }
1792     }
1794     /**
1795      * Given the settings of this theme, and the page pagelayout, return the
1796      * full path of the page layout file to use.
1797      *
1798      * Used by {@link core_renderer::header()}.
1799      *
1800      * @param string $pagelayout the the page layout name.
1801      * @return string Full path to the lyout file to use
1802      */
1803     public function layout_file($pagelayout) {
1804         global $CFG;
1806         $layoutinfo = $this->layout_info_for_page($pagelayout);
1807         $layoutfile = $layoutinfo['file'];
1809         if (array_key_exists('theme', $layoutinfo)) {
1810             $themes = array($layoutinfo['theme']);
1811         } else {
1812             $themes = array_merge(array($this->name),$this->parents);
1813         }
1815         foreach ($themes as $theme) {
1816             if ($dir = $this->find_theme_location($theme)) {
1817                 $path = "$dir/layout/$layoutfile";
1819                 // Check the template exists, return general base theme template if not.
1820                 if (is_readable($path)) {
1821                     return $path;
1822                 }
1823             }
1824         }
1826         debugging('Can not find layout file for: ' . $pagelayout);
1827         // fallback to standard normal layout
1828         return "$CFG->dirroot/theme/base/layout/general.php";
1829     }
1831     /**
1832      * Returns auxiliary page layout options specified in layout configuration array.
1833      *
1834      * @param string $pagelayout
1835      * @return array
1836      */
1837     public function pagelayout_options($pagelayout) {
1838         $info = $this->layout_info_for_page($pagelayout);
1839         if (!empty($info['options'])) {
1840             return $info['options'];
1841         }
1842         return array();
1843     }
1845     /**
1846      * Inform a block_manager about the block regions this theme wants on this
1847      * page layout.
1848      *
1849      * @param string $pagelayout the general type of the page.
1850      * @param block_manager $blockmanager the block_manger to set up.
1851      */
1852     public function setup_blocks($pagelayout, $blockmanager) {
1853         $layoutinfo = $this->layout_info_for_page($pagelayout);
1854         if (!empty($layoutinfo['regions'])) {
1855             $blockmanager->add_regions($layoutinfo['regions']);
1856             $blockmanager->set_default_region($layoutinfo['defaultregion']);
1857         }
1858     }
1860     /**
1861      * Gets the visible name for the requested block region.
1862      *
1863      * @param string $region The region name to get
1864      * @param string $theme The theme the region belongs to (may come from the parent theme)
1865      * @return string
1866      */
1867     protected function get_region_name($region, $theme) {
1868         $regionstring = get_string('region-' . $region, 'theme_' . $theme);
1869         // A name exists in this theme, so use it
1870         if (substr($regionstring, 0, 1) != '[') {
1871             return $regionstring;
1872         }
1874         // Otherwise, try to find one elsewhere
1875         // Check parents, if any
1876         foreach ($this->parents as $parentthemename) {
1877             $regionstring = get_string('region-' . $region, 'theme_' . $parentthemename);
1878             if (substr($regionstring, 0, 1) != '[') {
1879                 return $regionstring;
1880             }
1881         }
1883         // Last resort, try the base theme for names
1884         return get_string('region-' . $region, 'theme_base');
1885     }
1887     /**
1888      * Get the list of all block regions known to this theme in all templates.
1889      *
1890      * @return array internal region name => human readable name.
1891      */
1892     public function get_all_block_regions() {
1893         $regions = array();
1894         foreach ($this->layouts as $layoutinfo) {
1895             foreach ($layoutinfo['regions'] as $region) {
1896                 $regions[$region] = $this->get_region_name($region, $this->name);
1897             }
1898         }
1899         return $regions;
1900     }
1902     /**
1903      * Returns the human readable name of the theme
1904      *
1905      * @return string
1906      */
1907     public function get_theme_name() {
1908         return get_string('pluginname', 'theme_'.$this->name);
1909     }
1911     /**
1912      * Returns the block render method.
1913      *
1914      * It is set by the theme via:
1915      *     $THEME->blockrendermethod = '...';
1916      *
1917      * It can be one of two values, blocks or blocks_for_region.
1918      * It should be set to the method being used by the theme layouts.
1919      *
1920      * @return string
1921      */
1922     public function get_block_render_method() {
1923         if ($this->blockrendermethod) {
1924             // Return the specified block render method.
1925             return $this->blockrendermethod;
1926         }
1927         // Its not explicitly set, check the parent theme configs.
1928         foreach ($this->parent_configs as $config) {
1929             if (isset($config->blockrendermethod)) {
1930                 return $config->blockrendermethod;
1931             }
1932         }
1933         // Default it to blocks.
1934         return 'blocks';
1935     }
1938 /**
1939  * This class keeps track of which HTML tags are currently open.
1940  *
1941  * This makes it much easier to always generate well formed XHTML output, even
1942  * if execution terminates abruptly. Any time you output some opening HTML
1943  * without the matching closing HTML, you should push the necessary close tags
1944  * onto the stack.
1945  *
1946  * @copyright 2009 Tim Hunt
1947  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1948  * @since Moodle 2.0
1949  * @package core
1950  * @category output
1951  */
1952 class xhtml_container_stack {
1954     /**
1955      * @var array Stores the list of open containers.
1956      */
1957     protected $opencontainers = array();
1959     /**
1960      * @var array In developer debug mode, stores a stack trace of all opens and
1961      * closes, so we can output helpful error messages when there is a mismatch.
1962      */
1963     protected $log = array();
1965     /**
1966      * @var boolean Store whether we are developer debug mode. We need this in
1967      * several places including in the destructor where we may not have access to $CFG.
1968      */
1969     protected $isdebugging;
1971     /**
1972      * Constructor
1973      */
1974     public function __construct() {
1975         global $CFG;
1976         $this->isdebugging = $CFG->debugdeveloper;
1977     }
1979     /**
1980      * Push the close HTML for a recently opened container onto the stack.
1981      *
1982      * @param string $type The type of container. This is checked when {@link pop()}
1983      *      is called and must match, otherwise a developer debug warning is output.
1984      * @param string $closehtml The HTML required to close the container.
1985      */
1986     public function push($type, $closehtml) {
1987         $container = new stdClass;
1988         $container->type = $type;
1989         $container->closehtml = $closehtml;
1990         if ($this->isdebugging) {
1991             $this->log('Open', $type);
1992         }
1993         array_push($this->opencontainers, $container);
1994     }
1996     /**
1997      * Pop the HTML for the next closing container from the stack. The $type
1998      * must match the type passed when the container was opened, otherwise a
1999      * warning will be output.
2000      *
2001      * @param string $type The type of container.
2002      * @return string the HTML required to close the container.
2003      */
2004     public function pop($type) {
2005         if (empty($this->opencontainers)) {
2006             debugging('<p>There are no more open containers. This suggests there is a nesting problem.</p>' .
2007                     $this->output_log(), DEBUG_DEVELOPER);
2008             return;
2009         }
2011         $container = array_pop($this->opencontainers);
2012         if ($container->type != $type) {
2013             debugging('<p>The type of container to be closed (' . $container->type .
2014                     ') does not match the type of the next open container (' . $type .
2015                     '). This suggests there is a nesting problem.</p>' .
2016                     $this->output_log(), DEBUG_DEVELOPER);
2017         }
2018         if ($this->isdebugging) {
2019             $this->log('Close', $type);
2020         }
2021         return $container->closehtml;
2022     }
2024     /**
2025      * Close all but the last open container. This is useful in places like error
2026      * handling, where you want to close all the open containers (apart from <body>)
2027      * before outputting the error message.
2028      *
2029      * @param bool $shouldbenone assert that the stack should be empty now - causes a
2030      *      developer debug warning if it isn't.
2031      * @return string the HTML required to close any open containers inside <body>.
2032      */
2033     public function pop_all_but_last($shouldbenone = false) {
2034         if ($shouldbenone && count($this->opencontainers) != 1) {
2035             debugging('<p>Some HTML tags were opened in the body of the page but not closed.</p>' .
2036                     $this->output_log(), DEBUG_DEVELOPER);
2037         }
2038         $output = '';
2039         while (count($this->opencontainers) > 1) {
2040             $container = array_pop($this->opencontainers);
2041             $output .= $container->closehtml;
2042         }
2043         return $output;
2044     }
2046     /**
2047      * You can call this function if you want to throw away an instance of this
2048      * class without properly emptying the stack (for example, in a unit test).
2049      * Calling this method stops the destruct method from outputting a developer
2050      * debug warning. After calling this method, the instance can no longer be used.
2051      */
2052     public function discard() {
2053         $this->opencontainers = null;
2054     }
2056     /**
2057      * Adds an entry to the log.
2058      *
2059      * @param string $action The name of the action
2060      * @param string $type The type of action
2061      */
2062     protected function log($action, $type) {
2063         $this->log[] = '<li>' . $action . ' ' . $type . ' at:' .
2064                 format_backtrace(debug_backtrace()) . '</li>';
2065     }
2067     /**
2068      * Outputs the log's contents as a HTML list.
2069      *
2070      * @return string HTML list of the log
2071      */
2072     protected function output_log() {
2073         return '<ul>' . implode("\n", $this->log) . '</ul>';
2074     }