MDL-31984 Fixed whitespace during integration
[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 phsyical directory that is used to cache the theme
41  * files used for serving.
42  * Because it deletes the main theme cache directoy all themes are reset by
43  * this function.
44  */
45 function theme_reset_all_caches() {
46     global $CFG;
47     require_once("$CFG->libdir/filelib.php");
49     set_config('themerev', empty($CFG->themerev) ? 1 : $CFG->themerev+1);
50     fulldelete("$CFG->cachedir/theme");
51 }
53 /**
54  * Enable or disable theme designer mode.
55  *
56  * @param bool $state
57  */
58 function theme_set_designer_mod($state) {
59     theme_reset_all_caches();
60     set_config('themedesignermode', (int)!empty($state));
61 }
63 /**
64  * Returns current theme revision number.
65  *
66  * @return int
67  */
68 function theme_get_revision() {
69     global $CFG;
71     if (empty($CFG->themedesignermode)) {
72         if (empty($CFG->themerev)) {
73             return -1;
74         } else {
75             return $CFG->themerev;
76         }
78     } else {
79         return -1;
80     }
81 }
84 /**
85  * This class represents the configuration variables of a Moodle theme.
86  *
87  * All the variables with access: public below (with a few exceptions that are marked)
88  * are the properties you can set in your themes config.php file.
89  *
90  * There are also some methods and protected variables that are part of the inner
91  * workings of Moodle's themes system. If you are just editing a themes config.php
92  * file, you can just ignore those, and the following information for developers.
93  *
94  * Normally, to create an instance of this class, you should use the
95  * {@link theme_config::load()} factory method to load a themes config.php file.
96  * However, normally you don't need to bother, because moodle_page (that is, $PAGE)
97  * will create one for you, accessible as $PAGE->theme.
98  *
99  * @copyright 2009 Tim Hunt
100  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
101  * @since Moodle 2.0
102  * @package core
103  * @category output
104  */
105 class theme_config {
107     /**
108      * @var string Default theme, used when requested theme not found.
109      */
110     const DEFAULT_THEME = 'standard';
112     /**
113      * @var array You can base your theme on other themes by linking to the other theme as
114      * parents. This lets you use the CSS and layouts from the other themes
115      * (see {@link theme_config::$layouts}).
116      * That makes it easy to create a new theme that is similar to another one
117      * but with a few changes. In this themes CSS you only need to override
118      * those rules you want to change.
119      */
120     public $parents;
122     /**
123      * @var array The names of all the stylesheets from this theme that you would
124      * like included, in order. Give the names of the files without .css.
125      */
126     public $sheets = array();
128     /**
129      * @var array The names of all the stylesheets from parents that should be excluded.
130      * true value may be used to specify all parents or all themes from one parent.
131      * If no value specified value from parent theme used.
132      */
133     public $parents_exclude_sheets = null;
135     /**
136      * @var array List of plugin sheets to be excluded.
137      * If no value specified value from parent theme used.
138      */
139     public $plugins_exclude_sheets = null;
141     /**
142      * @var array List of style sheets that are included in the text editor bodies.
143      * Sheets from parent themes are used automatically and can not be excluded.
144      */
145     public $editor_sheets = array();
147     /**
148      * @var array The names of all the javascript files this theme that you would
149      * like included from head, in order. Give the names of the files without .js.
150      */
151     public $javascripts = array();
153     /**
154      * @var array The names of all the javascript files this theme that you would
155      * like included from footer, in order. Give the names of the files without .js.
156      */
157     public $javascripts_footer = array();
159     /**
160      * @var array The names of all the javascript files from parents that should
161      * be excluded. true value may be used to specify all parents or all themes
162      * from one parent.
163      * If no value specified value from parent theme used.
164      */
165     public $parents_exclude_javascripts = null;
167     /**
168      * @var array Which file to use for each page layout.
169      *
170      * This is an array of arrays. The keys of the outer array are the different layouts.
171      * Pages in Moodle are using several different layouts like 'normal', 'course', 'home',
172      * 'popup', 'form', .... The most reliable way to get a complete list is to look at
173      * {@link http://cvs.moodle.org/moodle/theme/base/config.php?view=markup the base theme config.php file}.
174      * That file also has a good example of how to set this setting.
175      *
176      * For each layout, the value in the outer array is an array that describes
177      * how you want that type of page to look. For example
178      * <pre>
179      *   $THEME->layouts = array(
180      *       // Most pages - if we encounter an unknown or a missing page type, this one is used.
181      *       'standard' => array(
182      *           'theme' = 'mytheme',
183      *           'file' => 'normal.php',
184      *           'regions' => array('side-pre', 'side-post'),
185      *           'defaultregion' => 'side-post'
186      *       ),
187      *       // The site home page.
188      *       'home' => array(
189      *           'theme' = 'mytheme',
190      *           'file' => 'home.php',
191      *           'regions' => array('side-pre', 'side-post'),
192      *           'defaultregion' => 'side-post'
193      *       ),
194      *       // ...
195      *   );
196      * </pre>
197      *
198      * 'theme' name of the theme where is the layout located
199      * 'file' is the layout file to use for this type of page.
200      * layout files are stored in layout subfolder
201      * 'regions' This lists the regions on the page where blocks may appear. For
202      * each region you list here, your layout file must include a call to
203      * <pre>
204      *   echo $OUTPUT->blocks_for_region($regionname);
205      * </pre>
206      * or equivalent so that the blocks are actually visible.
207      *
208      * 'defaultregion' If the list of regions is non-empty, then you must pick
209      * one of the one of them as 'default'. This has two meanings. First, this is
210      * where new blocks are added. Second, if there are any blocks associated with
211      * the page, but in non-existent regions, they appear here. (Imaging, for example,
212      * that someone added blocks using a different theme that used different region
213      * names, and then switched to this theme.)
214      */
215     public $layouts = array();
217     /**
218      * @var string Name of the renderer factory class to use. Must implement the
219      * {@link renderer_factory} interface.
220      *
221      * This is an advanced feature. Moodle output is generated by 'renderers',
222      * you can customise the HTML that is output by writing custom renderers,
223      * and then you need to specify 'renderer factory' so that Moodle can find
224      * your renderers.
225      *
226      * There are some renderer factories supplied with Moodle. Please follow these
227      * links to see what they do.
228      * <ul>
229      * <li>{@link standard_renderer_factory} - the default.</li>
230      * <li>{@link theme_overridden_renderer_factory} - use this if you want to write
231      *      your own custom renderers in a lib.php file in this theme (or the parent theme).</li>
232      * </ul>
233      */
234     public $rendererfactory = 'standard_renderer_factory';
236     /**
237      * @var string Function to do custom CSS post-processing.
238      *
239      * This is an advanced feature. If you want to do custom post-processing on the
240      * CSS before it is output (for example, to replace certain variable names
241      * with particular values) you can give the name of a function here.
242      */
243     public $csspostprocess = null;
245     /**
246      * @var string Accessibility: Right arrow-like character is
247      * used in the breadcrumb trail, course navigation menu
248      * (previous/next activity), calendar, and search forum block.
249      * If the theme does not set characters, appropriate defaults
250      * are set automatically. Please DO NOT
251      * use &lt; &gt; &raquo; - these are confusing for blind users.
252      */
253     public $rarrow = null;
255     /**
256      * @var string Accessibility: Right arrow-like character is
257      * used in the breadcrumb trail, course navigation menu
258      * (previous/next activity), calendar, and search forum block.
259      * If the theme does not set characters, appropriate defaults
260      * are set automatically. Please DO NOT
261      * use &lt; &gt; &raquo; - these are confusing for blind users.
262      */
263     public $larrow = null;
265     /**
266      * @var bool Some themes may want to disable ajax course editing.
267      */
268     public $enablecourseajax = true;
270     //==Following properties are not configurable from theme config.php==
272     /**
273      * @var string The name of this theme. Set automatically when this theme is
274      * loaded. This can not be set in theme config.php
275      */
276     public $name;
278     /**
279      * @var string The folder where this themes files are stored. This is set
280      * automatically. This can not be set in theme config.php
281      */
282     public $dir;
284     /**
285      * @var stdClass Theme settings stored in config_plugins table.
286      * This can not be set in theme config.php
287      */
288     public $setting = null;
290     /**
291      * @var bool If set to true and the theme enables the dock then  blocks will be able
292      * to be moved to the special dock
293      */
294     public $enable_dock = false;
296     /**
297      * @var bool If set to true then this theme will not be shown in the theme selector unless
298      * theme designer mode is turned on.
299      */
300     public $hidefromselector = false;
302     /**
303      * @var renderer_factory Instance of the renderer_factory implementation
304      * we are using. Implementation detail.
305      */
306     protected $rf = null;
308     /**
309      * @var array List of parent config objects.
310      **/
311     protected $parent_configs = array();
313     /**
314      * Load the config.php file for a particular theme, and return an instance
315      * of this class. (That is, this is a factory method.)
316      *
317      * @param string $themename the name of the theme.
318      * @return theme_config an instance of this class.
319      */
320     public static function load($themename) {
321         global $CFG;
323         // load theme settings from db
324         try {
325             $settings = get_config('theme_'.$themename);
326         } catch (dml_exception $e) {
327             // most probably moodle tables not created yet
328             $settings = new stdClass();
329         }
331         if ($config = theme_config::find_theme_config($themename, $settings)) {
332             return new theme_config($config);
334         } else if ($themename == theme_config::DEFAULT_THEME) {
335             throw new coding_exception('Default theme '.theme_config::DEFAULT_THEME.' not available or broken!');
337         } else {
338             // bad luck, the requested theme has some problems - admin see details in theme config
339             return new theme_config(theme_config::find_theme_config(theme_config::DEFAULT_THEME, $settings));
340         }
341     }
343     /**
344      * Theme diagnostic code. It is very problematic to send debug output
345      * to the actual CSS file, instead this functions is supposed to
346      * diagnose given theme and highlights all potential problems.
347      * This information should be available from the theme selection page
348      * or some other debug page for theme designers.
349      *
350      * @param string $themename
351      * @return array description of problems
352      */
353     public static function diagnose($themename) {
354         //TODO: MDL-21108
355         return array();
356     }
358     /**
359      * Private constructor, can be called only from the factory method.
360      * @param stdClass $config
361      */
362     private function __construct($config) {
363         global $CFG; //needed for included lib.php files
365         $this->settings = $config->settings;
366         $this->name     = $config->name;
367         $this->dir      = $config->dir;
369         if ($this->name != 'base') {
370             $baseconfig = theme_config::find_theme_config('base', $this->settings);
371         } else {
372             $baseconfig = $config;
373         }
375         $configurable = array('parents', 'sheets', 'parents_exclude_sheets', 'plugins_exclude_sheets', 'javascripts', 'javascripts_footer',
376                               'parents_exclude_javascripts', 'layouts', 'enable_dock', 'enablecourseajax',
377                               'rendererfactory', 'csspostprocess', 'editor_sheets', 'rarrow', 'larrow', 'hidefromselector');
379         foreach ($config as $key=>$value) {
380             if (in_array($key, $configurable)) {
381                 $this->$key = $value;
382             }
383         }
385         // verify all parents and load configs and renderers
386         foreach ($this->parents as $parent) {
387             if ($parent == 'base') {
388                 $parent_config = $baseconfig;
389             } else if (!$parent_config = theme_config::find_theme_config($parent, $this->settings)) {
390                 // this is not good - better exclude faulty parents
391                 continue;
392             }
393             $libfile = $parent_config->dir.'/lib.php';
394             if (is_readable($libfile)) {
395                 // theme may store various function here
396                 include_once($libfile);
397             }
398             $renderersfile = $parent_config->dir.'/renderers.php';
399             if (is_readable($renderersfile)) {
400                 // may contain core and plugin renderers and renderer factory
401                 include_once($renderersfile);
402             }
403             $this->parent_configs[$parent] = $parent_config;
404             $rendererfile = $parent_config->dir.'/renderers.php';
405             if (is_readable($rendererfile)) {
406                  // may contain core and plugin renderers and renderer factory
407                 include_once($rendererfile);
408             }
409         }
410         $libfile = $this->dir.'/lib.php';
411         if (is_readable($libfile)) {
412             // theme may store various function here
413             include_once($libfile);
414         }
415         $rendererfile = $this->dir.'/renderers.php';
416         if (is_readable($rendererfile)) {
417             // may contain core and plugin renderers and renderer factory
418             include_once($rendererfile);
419         } else {
420             // check if renderers.php file is missnamed renderer.php
421             if (is_readable($this->dir.'/renderer.php')) {
422                 debugging('Developer hint: '.$this->dir.'/renderer.php should be renamed to ' . $this->dir."/renderers.php.
423                     See: http://docs.moodle.org/dev/Output_renderers#Theme_renderers.", DEBUG_DEVELOPER);
424             }
425         }
427         // cascade all layouts properly
428         foreach ($baseconfig->layouts as $layout=>$value) {
429             if (!isset($this->layouts[$layout])) {
430                 foreach ($this->parent_configs as $parent_config) {
431                     if (isset($parent_config->layouts[$layout])) {
432                         $this->layouts[$layout] = $parent_config->layouts[$layout];
433                         continue 2;
434                     }
435                 }
436                 $this->layouts[$layout] = $value;
437             }
438         }
440         //fix arrows if needed
441         $this->check_theme_arrows();
442     }
444     /**
445      * Checks if arrows $THEME->rarrow, $THEME->larrow have been set (theme/-/config.php).
446      * If not it applies sensible defaults.
447      *
448      * Accessibility: right and left arrow Unicode characters for breadcrumb, calendar,
449      * search forum block, etc. Important: these are 'silent' in a screen-reader
450      * (unlike &gt; &raquo;), and must be accompanied by text.
451      */
452     private function check_theme_arrows() {
453         if (!isset($this->rarrow) and !isset($this->larrow)) {
454             // Default, looks good in Win XP/IE 6, Win/Firefox 1.5, Win/Netscape 8...
455             // Also OK in Win 9x/2K/IE 5.x
456             $this->rarrow = '&#x25BA;';
457             $this->larrow = '&#x25C4;';
458             if (empty($_SERVER['HTTP_USER_AGENT'])) {
459                 $uagent = '';
460             } else {
461                 $uagent = $_SERVER['HTTP_USER_AGENT'];
462             }
463             if (false !== strpos($uagent, 'Opera')
464                 || false !== strpos($uagent, 'Mac')) {
465                 // Looks good in Win XP/Mac/Opera 8/9, Mac/Firefox 2, Camino, Safari.
466                 // Not broken in Mac/IE 5, Mac/Netscape 7 (?).
467                 $this->rarrow = '&#x25B6;';
468                 $this->larrow = '&#x25C0;';
469             }
470             elseif (false !== strpos($uagent, 'Konqueror')) {
471                 $this->rarrow = '&rarr;';
472                 $this->larrow = '&larr;';
473             }
474             elseif (isset($_SERVER['HTTP_ACCEPT_CHARSET'])
475                 && false === stripos($_SERVER['HTTP_ACCEPT_CHARSET'], 'utf-8')) {
476                 // (Win/IE 5 doesn't set ACCEPT_CHARSET, but handles Unicode.)
477                 // To be safe, non-Unicode browsers!
478                 $this->rarrow = '&gt;';
479                 $this->larrow = '&lt;';
480             }
482             // RTL support - in RTL languages, swap r and l arrows
483             if (right_to_left()) {
484                 $t = $this->rarrow;
485                 $this->rarrow = $this->larrow;
486                 $this->larrow = $t;
487             }
488         }
489     }
491     /**
492      * Returns output renderer prefixes, these are used when looking
493      * for the overridden renderers in themes.
494      *
495      * @return array
496      */
497     public function renderer_prefixes() {
498         global $CFG; // just in case the included files need it
500         $prefixes = array('theme_'.$this->name);
502         foreach ($this->parent_configs as $parent) {
503             $prefixes[] = 'theme_'.$parent->name;
504         }
506         return $prefixes;
507     }
509     /**
510      * Returns the stylesheet URL of this editor content
511      *
512      * @param bool $encoded false means use & and true use &amp; in URLs
513      * @return string
514      */
515     public function editor_css_url($encoded=true) {
516         global $CFG;
518         $rev = theme_get_revision();
520         if ($rev > -1) {
521             $params = array('theme'=>$this->name,'rev'=>$rev, 'type'=>'editor');
522             return new moodle_url($CFG->httpswwwroot.'/theme/styles.php', $params);
523         } else {
524             $params = array('theme'=>$this->name, 'type'=>'editor');
525             return new moodle_url($CFG->httpswwwroot.'/theme/styles_debug.php', $params);
526         }
527     }
529     /**
530      * Returns the content of the CSS to be used in editor content
531      *
532      * @return string
533      */
534     public function editor_css_files() {
535         global $CFG;
537         $files = array();
539         // first editor plugins
540         $plugins = get_plugin_list('editor');
541         foreach ($plugins as $plugin=>$fulldir) {
542             $sheetfile = "$fulldir/editor_styles.css";
543             if (is_readable($sheetfile)) {
544                 $files['plugin_'.$plugin] = $sheetfile;
545             }
546         }
547         // then parent themes
548         foreach (array_reverse($this->parent_configs) as $parent_config) { // base first, the immediate parent last
549             if (empty($parent_config->editor_sheets)) {
550                 continue;
551             }
552             foreach ($parent_config->editor_sheets as $sheet) {
553                 $sheetfile = "$parent_config->dir/style/$sheet.css";
554                 if (is_readable($sheetfile)) {
555                     $files['parent_'.$parent_config->name.'_'.$sheet] = $sheetfile;
556                 }
557             }
558         }
559         // finally this theme
560         if (!empty($this->editor_sheets)) {
561             foreach ($this->editor_sheets as $sheet) {
562                 $sheetfile = "$this->dir/style/$sheet.css";
563                 if (is_readable($sheetfile)) {
564                     $files['theme_'.$sheet] = $sheetfile;
565                 }
566             }
567         }
569         return $files;
570     }
572     /**
573      * Get the stylesheet URL of this theme
574      *
575      * @param moodle_page $page Not used... deprecated?
576      * @return array of moodle_url
577      */
578     public function css_urls(moodle_page $page) {
579         global $CFG;
581         $rev = theme_get_revision();
583         $urls = array();
585         if ($rev > -1) {
586             if (check_browser_version('MSIE', 5)) {
587                 // We need to split the CSS files for IE
588                 $urls[] = new moodle_url($CFG->httpswwwroot.'/theme/styles.php', array('theme'=>$this->name,'rev'=>$rev, 'type'=>'plugins'));
589                 $urls[] = new moodle_url($CFG->httpswwwroot.'/theme/styles.php', array('theme'=>$this->name,'rev'=>$rev, 'type'=>'parents'));
590                 $urls[] = new moodle_url($CFG->httpswwwroot.'/theme/styles.php', array('theme'=>$this->name,'rev'=>$rev, 'type'=>'theme'));
591             } else {
592                 $urls[] = new moodle_url($CFG->httpswwwroot.'/theme/styles.php', array('theme'=>$this->name,'rev'=>$rev));
593             }
594         } else {
595             // find out the current CSS and cache it now for 5 seconds
596             // the point is to construct the CSS only once and pass it through the
597             // dataroot to the script that actually serves the sheets
598             if (!defined('THEME_DESIGNER_CACHE_LIFETIME')) {
599                 define('THEME_DESIGNER_CACHE_LIFETIME', 4); // this can be also set in config.php
600             }
601             $candidatesheet = "$CFG->cachedir/theme/$this->name/designer.ser";
602             if (!file_exists($candidatesheet)) {
603                 $css = $this->css_content();
604                 check_dir_exists(dirname($candidatesheet));
605                 file_put_contents($candidatesheet, serialize($css));
607             } else if (filemtime($candidatesheet) > time() - THEME_DESIGNER_CACHE_LIFETIME) {
608                 if ($css = file_get_contents($candidatesheet)) {
609                     $css = unserialize($css);
610                 } else {
611                     unlink($candidatesheet);
612                     $css = $this->css_content();
613                 }
615             } else {
616                 unlink($candidatesheet);
617                 $css = $this->css_content();
618                 file_put_contents($candidatesheet, serialize($css));
619             }
621             $baseurl = $CFG->httpswwwroot.'/theme/styles_debug.php';
623             if (check_browser_version('MSIE', 5)) {
624                 // lalala, IE does not allow more than 31 linked CSS files from main document
625                 $urls[] = new moodle_url($baseurl, array('theme'=>$this->name, 'type'=>'ie', 'subtype'=>'plugins'));
626                 foreach ($css['parents'] as $parent=>$sheets) {
627                     // We need to serve parents individually otherwise we may easily exceed the style limit IE imposes (4096)
628                     $urls[] = new moodle_url($baseurl, array('theme'=>$this->name,'type'=>'ie', 'subtype'=>'parents', 'sheet'=>$parent));
629                 }
630                 $urls[] = new moodle_url($baseurl, array('theme'=>$this->name, 'type'=>'ie', 'subtype'=>'theme'));
632             } else {
633                 foreach ($css['plugins'] as $plugin=>$unused) {
634                     $urls[] = new moodle_url($baseurl, array('theme'=>$this->name,'type'=>'plugin', 'subtype'=>$plugin));
635                 }
636                 foreach ($css['parents'] as $parent=>$sheets) {
637                     foreach ($sheets as $sheet=>$unused2) {
638                         $urls[] = new moodle_url($baseurl, array('theme'=>$this->name,'type'=>'parent', 'subtype'=>$parent, 'sheet'=>$sheet));
639                     }
640                 }
641                 foreach ($css['theme'] as $sheet=>$unused) {
642                     $urls[] = new moodle_url($baseurl, array('sheet'=>$sheet, 'theme'=>$this->name, 'type'=>'theme')); // sheet first in order to make long urls easier to read
643                 }
644             }
645         }
647         return $urls;
648     }
650     /**
651      * Returns an array of organised CSS files required for this output
652      *
653      * @return array
654      */
655     public function css_files() {
656         $cssfiles = array('plugins'=>array(), 'parents'=>array(), 'theme'=>array());
658         // get all plugin sheets
659         $excludes = $this->resolve_excludes('plugins_exclude_sheets');
660         if ($excludes !== true) {
661             foreach (get_plugin_types() as $type=>$unused) {
662                 if ($type === 'theme' || (!empty($excludes[$type]) and $excludes[$type] === true)) {
663                     continue;
664                 }
665                 $plugins = get_plugin_list($type);
666                 foreach ($plugins as $plugin=>$fulldir) {
667                     if (!empty($excludes[$type]) and is_array($excludes[$type])
668                         and in_array($plugin, $excludes[$type])) {
669                         continue;
670                     }
672                     $plugincontent = '';
673                     $sheetfile = "$fulldir/styles.css";
674                     if (is_readable($sheetfile)) {
675                         $cssfiles['plugins'][$type.'_'.$plugin] = $sheetfile;
676                     }
677                     $sheetthemefile = "$fulldir/styles_{$this->name}.css";
678                     if (is_readable($sheetthemefile)) {
679                         $cssfiles['plugins'][$type.'_'.$plugin.'_'.$this->name] = $sheetthemefile;
680                     }
681                     }
682                 }
683             }
685         // find out wanted parent sheets
686         $excludes = $this->resolve_excludes('parents_exclude_sheets');
687         if ($excludes !== true) {
688             foreach (array_reverse($this->parent_configs) as $parent_config) { // base first, the immediate parent last
689                 $parent = $parent_config->name;
690                 if (empty($parent_config->sheets) || (!empty($excludes[$parent]) and $excludes[$parent] === true)) {
691                     continue;
692                 }
693                 foreach ($parent_config->sheets as $sheet) {
694                     if (!empty($excludes[$parent]) and is_array($excludes[$parent])
695                         and in_array($sheet, $excludes[$parent])) {
696                         continue;
697                     }
698                     $sheetfile = "$parent_config->dir/style/$sheet.css";
699                     if (is_readable($sheetfile)) {
700                         $cssfiles['parents'][$parent][$sheet] = $sheetfile;
701                     }
702                 }
703             }
704         }
706         // current theme sheets
707         if (is_array($this->sheets)) {
708             foreach ($this->sheets as $sheet) {
709                 $sheetfile = "$this->dir/style/$sheet.css";
710                 if (is_readable($sheetfile)) {
711                     $cssfiles['theme'][$sheet] = $sheetfile;
712                 }
713             }
714         }
716         return $cssfiles;
717     }
719     /**
720      * Returns the content of the one huge CSS merged from all style sheets.
721      *
722      * @return string
723      */
724     public function css_content() {
725         $files = array_merge($this->css_files(), array('editor'=>$this->editor_css_files()));
726         $css = $this->css_files_get_contents($files, array());
727         return $css;
728     }
730     /**
731      * Given an array of file paths or a single file path loads the contents of
732      * the CSS file, processes it then returns it in the same structure it was given.
733      *
734      * Can be used recursively on the results of {@link css_files}
735      *
736      * @param array|string $file An array of file paths or a single file path
737      * @param array $keys An array of previous array keys [recursive addition]
738      * @return The converted array or the contents of the single file ($file type)
739      */
740     protected function css_files_get_contents($file, array $keys) {
741         if (is_array($file)) {
742             foreach ($file as $key=>$f) {
743                 $file[$key] = $this->css_files_get_contents($f, array_merge($keys, array($key)));
744             }
745             return $file;
746         } else {
747             $comment = '/** Path: '.implode(' ', $keys).' **/'."\n";
748             return $comment.$this->post_process(file_get_contents($file));
749         }
750     }
753     /**
754      * Generate a URL to the file that serves theme JavaScript files.
755      *
756      * @param bool $inhead true means head url, false means footer
757      * @return moodle_url
758      */
759     public function javascript_url($inhead) {
760         global $CFG;
762         $rev = theme_get_revision();
763         $params = array('theme'=>$this->name,'rev'=>$rev);
764         $params['type'] = $inhead ? 'head' : 'footer';
766         return new moodle_url($CFG->httpswwwroot.'/theme/javascript.php', $params);
767     }
769     /**
770      * Get the URL's for the JavaScript files used by this theme.
771      * They won't be served directly, instead they'll be mediated through
772      * theme/javascript.php.
773      *
774      * @param string $type Either javascripts_footer, or javascripts
775      * @return array
776      */
777     public function javascript_files($type) {
778         if ($type === 'footer') {
779             $type = 'javascripts_footer';
780         } else {
781             $type = 'javascripts';
782         }
784         $js = array();
785         // find out wanted parent javascripts
786         $excludes = $this->resolve_excludes('parents_exclude_javascripts');
787         if ($excludes !== true) {
788             foreach (array_reverse($this->parent_configs) as $parent_config) { // base first, the immediate parent last
789                 $parent = $parent_config->name;
790                 if (empty($parent_config->$type)) {
791                     continue;
792                 }
793                 if (!empty($excludes[$parent]) and $excludes[$parent] === true) {
794                     continue;
795                 }
796                 foreach ($parent_config->$type as $javascript) {
797                     if (!empty($excludes[$parent]) and is_array($excludes[$parent])
798                         and in_array($javascript, $excludes[$parent])) {
799                         continue;
800                     }
801                     $javascriptfile = "$parent_config->dir/javascript/$javascript.js";
802                     if (is_readable($javascriptfile)) {
803                         $js[] = $javascriptfile;
804                     }
805                 }
806             }
807         }
809         // current theme javascripts
810         if (is_array($this->$type)) {
811             foreach ($this->$type as $javascript) {
812                 $javascriptfile = "$this->dir/javascript/$javascript.js";
813                 if (is_readable($javascriptfile)) {
814                     $js[] = $javascriptfile;
815                 }
816             }
817         }
819         return $js;
820     }
822     /**
823      * Resolves an exclude setting to the themes setting is applicable or the
824      * setting of its closest parent.
825      *
826      * @param string $variable The name of the setting the exclude setting to resolve
827      * @param string $default
828      * @return mixed
829      */
830     protected function resolve_excludes($variable, $default = null) {
831         $setting = $default;
832         if (is_array($this->{$variable}) or $this->{$variable} === true) {
833             $setting = $this->{$variable};
834         } else {
835             foreach ($this->parent_configs as $parent_config) { // the immediate parent first, base last
836                 if (!isset($parent_config->{$variable})) {
837                     continue;
838                 }
839                 if (is_array($parent_config->{$variable}) or $parent_config->{$variable} === true) {
840                     $setting = $parent_config->{$variable};
841                     break;
842                 }
843             }
844         }
845         return $setting;
846     }
848     /**
849      * Returns the content of the one huge javascript file merged from all theme javascript files.
850      *
851      * @param bool $type
852      * @return string
853      */
854     public function javascript_content($type) {
855         $jsfiles = $this->javascript_files($type);
856         $js = '';
857         foreach ($jsfiles as $jsfile) {
858             $js .= file_get_contents($jsfile)."\n";
859         }
860         return $js;
861     }
863     /**
864      * Post processes CSS.
865      *
866      * This method post processes all of the CSS before it is served for this theme.
867      * This is done so that things such as image URL's can be swapped in and to
868      * run any specific CSS post process method the theme has requested.
869      * This allows themes to use CSS settings.
870      *
871      * @param string $css The CSS to process.
872      * @return string The processed CSS.
873      */
874     public function post_process($css) {
875         global $CFG;
877         // now resolve all image locations
878         if (preg_match_all('/\[\[pix:([a-z_]+\|)?([^\]]+)\]\]/', $css, $matches, PREG_SET_ORDER)) {
879             $replaced = array();
880             foreach ($matches as $match) {
881                 if (isset($replaced[$match[0]])) {
882                     continue;
883                 }
884                 $replaced[$match[0]] = true;
885                 $imagename = $match[2];
886                 $component = rtrim($match[1], '|');
887                 $imageurl = $this->pix_url($imagename, $component)->out(false);
888                  // we do not need full url because the image.php is always in the same dir
889                 $imageurl = str_replace("$CFG->httpswwwroot/theme/", '', $imageurl);
890                 $css = str_replace($match[0], $imageurl, $css);
891             }
892         }
894         // now resolve all theme settings or do any other postprocessing
895         $csspostprocess = $this->csspostprocess;
896         if (function_exists($csspostprocess)) {
897             $css = $csspostprocess($css, $this);
898         }
900         return $css;
901     }
903     /**
904      * Return the URL for an image
905      *
906      * @param string $imagename the name of the icon.
907      * @param string $component specification of one plugin like in get_string()
908      * @return moodle_url
909      */
910     public function pix_url($imagename, $component) {
911         global $CFG;
913         $params = array('theme'=>$this->name, 'image'=>$imagename);
915         $rev = theme_get_revision();
916         if ($rev != -1) {
917             $params['rev'] = $rev;
918         }
919         if (!empty($component) and $component !== 'moodle'and $component !== 'core') {
920             $params['component'] = $component;
921         }
923         return new moodle_url("$CFG->httpswwwroot/theme/image.php", $params);
924     }
926     /**
927      * Resolves the real image location.
928      * @param string $image name of image, may contain relative path
929      * @param string $component
930      * @return string full file path
931      */
932     public function resolve_image_location($image, $component) {
933         global $CFG;
935         if ($component === 'moodle' or $component === 'core' or empty($component)) {
936             if ($imagefile = $this->image_exists("$this->dir/pix_core/$image")) {
937                 return $imagefile;
938             }
939             foreach (array_reverse($this->parent_configs) as $parent_config) { // base first, the immediate parent last
940                 if ($imagefile = $this->image_exists("$parent_config->dir/pix_core/$image")) {
941                     return $imagefile;
942                 }
943             }
944             if ($imagefile = $this->image_exists("$CFG->dirroot/pix/$image")) {
945                 return $imagefile;
946             }
947             return null;
949         } else if ($component === 'theme') { //exception
950             if ($image === 'favicon') {
951                 return "$this->dir/pix/favicon.ico";
952             }
953             if ($imagefile = $this->image_exists("$this->dir/pix/$image")) {
954                 return $imagefile;
955             }
956             foreach (array_reverse($this->parent_configs) as $parent_config) { // base first, the immediate parent last
957                 if ($imagefile = $this->image_exists("$parent_config->dir/pix/$image")) {
958                     return $imagefile;
959                 }
960             }
961             return null;
963         } else {
964             if (strpos($component, '_') === false) {
965                 $component = 'mod_'.$component;
966             }
967             list($type, $plugin) = explode('_', $component, 2);
969             if ($imagefile = $this->image_exists("$this->dir/pix_plugins/$type/$plugin/$image")) {
970                 return $imagefile;
971             }
972             foreach (array_reverse($this->parent_configs) as $parent_config) { // base first, the immediate parent last
973                 if ($imagefile = $this->image_exists("$parent_config->dir/pix_plugins/$type/$plugin/$image")) {
974                     return $imagefile;
975                 }
976             }
977             $dir = get_plugin_directory($type, $plugin);
978             if ($imagefile = $this->image_exists("$dir/pix/$image")) {
979                 return $imagefile;
980             }
981             return null;
982         }
983     }
985     /**
986      * Checks if file with any image extension exists.
987      *
988      * @param string $filepath
989      * @return string image name with extension
990      */
991     private static function image_exists($filepath) {
992         if (file_exists("$filepath.gif")) {
993             return "$filepath.gif";
994         } else  if (file_exists("$filepath.png")) {
995             return "$filepath.png";
996         } else  if (file_exists("$filepath.jpg")) {
997             return "$filepath.jpg";
998         } else  if (file_exists("$filepath.jpeg")) {
999             return "$filepath.jpeg";
1000         } else {
1001             return false;
1002         }
1003     }
1005     /**
1006      * Loads the theme config from config.php file.
1007      *
1008      * @param string $themename
1009      * @param stdClass $settings from config_plugins table
1010      * @return stdClass The theme configuration
1011      */
1012     private static function find_theme_config($themename, $settings) {
1013         // We have to use the variable name $THEME (upper case) because that
1014         // is what is used in theme config.php files.
1016         if (!$dir = theme_config::find_theme_location($themename)) {
1017             return null;
1018         }
1020         $THEME = new stdClass();
1021         $THEME->name     = $themename;
1022         $THEME->dir      = $dir;
1023         $THEME->settings = $settings;
1025         global $CFG; // just in case somebody tries to use $CFG in theme config
1026         include("$THEME->dir/config.php");
1028         // verify the theme configuration is OK
1029         if (!is_array($THEME->parents)) {
1030             // parents option is mandatory now
1031             return null;
1032         }
1034         return $THEME;
1035     }
1037     /**
1038      * Finds the theme location and verifies the theme has all needed files
1039      * and is not obsoleted.
1040      *
1041      * @param string $themename
1042      * @return string full dir path or null if not found
1043      */
1044     private static function find_theme_location($themename) {
1045         global $CFG;
1047         if (file_exists("$CFG->dirroot/theme/$themename/config.php")) {
1048             $dir = "$CFG->dirroot/theme/$themename";
1050         } else if (!empty($CFG->themedir) and file_exists("$CFG->themedir/$themename/config.php")) {
1051             $dir = "$CFG->themedir/$themename";
1053         } else {
1054             return null;
1055         }
1057         if (file_exists("$dir/styles.php")) {
1058             //legacy theme - needs to be upgraded - upgrade info is displayed on the admin settings page
1059             return null;
1060         }
1062         return $dir;
1063     }
1065     /**
1066      * Get the renderer for a part of Moodle for this theme.
1067      *
1068      * @param moodle_page $page the page we are rendering
1069      * @param string $component the name of part of moodle. E.g. 'core', 'quiz', 'qtype_multichoice'.
1070      * @param string $subtype optional subtype such as 'news' resulting to 'mod_forum_news'
1071      * @param string $target one of rendering target constants
1072      * @return renderer_base the requested renderer.
1073      */
1074     public function get_renderer(moodle_page $page, $component, $subtype = null, $target = null) {
1075         if (is_null($this->rf)) {
1076             $classname = $this->rendererfactory;
1077             $this->rf = new $classname($this);
1078         }
1080         return $this->rf->get_renderer($page, $component, $subtype, $target);
1081     }
1083     /**
1084      * Get the information from {@link $layouts} for this type of page.
1085      *
1086      * @param string $pagelayout the the page layout name.
1087      * @return array the appropriate part of {@link $layouts}.
1088      */
1089     protected function layout_info_for_page($pagelayout) {
1090         if (array_key_exists($pagelayout, $this->layouts)) {
1091             return $this->layouts[$pagelayout];
1092         } else {
1093             debugging('Invalid page layout specified: ' . $pagelayout);
1094             return $this->layouts['standard'];
1095         }
1096     }
1098     /**
1099      * Given the settings of this theme, and the page pagelayout, return the
1100      * full path of the page layout file to use.
1101      *
1102      * Used by {@link core_renderer::header()}.
1103      *
1104      * @param string $pagelayout the the page layout name.
1105      * @return string Full path to the lyout file to use
1106      */
1107     public function layout_file($pagelayout) {
1108         global $CFG;
1110         $layoutinfo = $this->layout_info_for_page($pagelayout);
1111         $layoutfile = $layoutinfo['file'];
1113         if (array_key_exists('theme', $layoutinfo)) {
1114             $themes = array($layoutinfo['theme']);
1115         } else {
1116             $themes = array_merge(array($this->name),$this->parents);
1117         }
1119         foreach ($themes as $theme) {
1120             if ($dir = $this->find_theme_location($theme)) {
1121                 $path = "$dir/layout/$layoutfile";
1123                 // Check the template exists, return general base theme template if not.
1124                 if (is_readable($path)) {
1125                     return $path;
1126                 }
1127             }
1128         }
1130         debugging('Can not find layout file for: ' . $pagelayout);
1131         // fallback to standard normal layout
1132         return "$CFG->dirroot/theme/base/layout/general.php";
1133     }
1135     /**
1136      * Returns auxiliary page layout options specified in layout configuration array.
1137      *
1138      * @param string $pagelayout
1139      * @return array
1140      */
1141     public function pagelayout_options($pagelayout) {
1142         $info = $this->layout_info_for_page($pagelayout);
1143         if (!empty($info['options'])) {
1144             return $info['options'];
1145         }
1146         return array();
1147     }
1149     /**
1150      * Inform a block_manager about the block regions this theme wants on this
1151      * page layout.
1152      *
1153      * @param string $pagelayout the general type of the page.
1154      * @param block_manager $blockmanager the block_manger to set up.
1155      */
1156     public function setup_blocks($pagelayout, $blockmanager) {
1157         $layoutinfo = $this->layout_info_for_page($pagelayout);
1158         if (!empty($layoutinfo['regions'])) {
1159             $blockmanager->add_regions($layoutinfo['regions']);
1160             $blockmanager->set_default_region($layoutinfo['defaultregion']);
1161         }
1162     }
1164     /**
1165      * Gets the visible name for the requested block region.
1166      *
1167      * @param string $region The region name to get
1168      * @param string $theme The theme the region belongs to (may come from the parent theme)
1169      * @return string
1170      */
1171     protected function get_region_name($region, $theme) {
1172         $regionstring = get_string('region-' . $region, 'theme_' . $theme);
1173         // A name exists in this theme, so use it
1174         if (substr($regionstring, 0, 1) != '[') {
1175             return $regionstring;
1176         }
1178         // Otherwise, try to find one elsewhere
1179         // Check parents, if any
1180         foreach ($this->parents as $parentthemename) {
1181             $regionstring = get_string('region-' . $region, 'theme_' . $parentthemename);
1182             if (substr($regionstring, 0, 1) != '[') {
1183                 return $regionstring;
1184             }
1185         }
1187         // Last resort, try the base theme for names
1188         return get_string('region-' . $region, 'theme_base');
1189     }
1191     /**
1192      * Get the list of all block regions known to this theme in all templates.
1193      *
1194      * @return array internal region name => human readable name.
1195      */
1196     public function get_all_block_regions() {
1197         $regions = array();
1198         foreach ($this->layouts as $layoutinfo) {
1199             foreach ($layoutinfo['regions'] as $region) {
1200                 $regions[$region] = $this->get_region_name($region, $this->name);
1201             }
1202         }
1203         return $regions;
1204     }
1206     /**
1207      * Returns the human readable name of the theme
1208      *
1209      * @return string
1210      */
1211     public function get_theme_name() {
1212         return get_string('pluginname', 'theme_'.$this->name);
1213     }
1216 /**
1217  * This class keeps track of which HTML tags are currently open.
1218  *
1219  * This makes it much easier to always generate well formed XHTML output, even
1220  * if execution terminates abruptly. Any time you output some opening HTML
1221  * without the matching closing HTML, you should push the necessary close tags
1222  * onto the stack.
1223  *
1224  * @copyright 2009 Tim Hunt
1225  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1226  * @since Moodle 2.0
1227  * @package core
1228  * @category output
1229  */
1230 class xhtml_container_stack {
1232     /**
1233      * @var array Stores the list of open containers.
1234      */
1235     protected $opencontainers = array();
1237     /**
1238      * @var array In developer debug mode, stores a stack trace of all opens and
1239      * closes, so we can output helpful error messages when there is a mismatch.
1240      */
1241     protected $log = array();
1243     /**
1244      * @var boolean Store whether we are developer debug mode. We need this in
1245      * several places including in the destructor where we may not have access to $CFG.
1246      */
1247     protected $isdebugging;
1249     /**
1250      * Constructor
1251      */
1252     public function __construct() {
1253         $this->isdebugging = debugging('', DEBUG_DEVELOPER);
1254     }
1256     /**
1257      * Push the close HTML for a recently opened container onto the stack.
1258      *
1259      * @param string $type The type of container. This is checked when {@link pop()}
1260      *      is called and must match, otherwise a developer debug warning is output.
1261      * @param string $closehtml The HTML required to close the container.
1262      */
1263     public function push($type, $closehtml) {
1264         $container = new stdClass;
1265         $container->type = $type;
1266         $container->closehtml = $closehtml;
1267         if ($this->isdebugging) {
1268             $this->log('Open', $type);
1269         }
1270         array_push($this->opencontainers, $container);
1271     }
1273     /**
1274      * Pop the HTML for the next closing container from the stack. The $type
1275      * must match the type passed when the container was opened, otherwise a
1276      * warning will be output.
1277      *
1278      * @param string $type The type of container.
1279      * @return string the HTML required to close the container.
1280      */
1281     public function pop($type) {
1282         if (empty($this->opencontainers)) {
1283             debugging('<p>There are no more open containers. This suggests there is a nesting problem.</p>' .
1284                     $this->output_log(), DEBUG_DEVELOPER);
1285             return;
1286         }
1288         $container = array_pop($this->opencontainers);
1289         if ($container->type != $type) {
1290             debugging('<p>The type of container to be closed (' . $container->type .
1291                     ') does not match the type of the next open container (' . $type .
1292                     '). This suggests there is a nesting problem.</p>' .
1293                     $this->output_log(), DEBUG_DEVELOPER);
1294         }
1295         if ($this->isdebugging) {
1296             $this->log('Close', $type);
1297         }
1298         return $container->closehtml;
1299     }
1301     /**
1302      * Close all but the last open container. This is useful in places like error
1303      * handling, where you want to close all the open containers (apart from <body>)
1304      * before outputting the error message.
1305      *
1306      * @param bool $shouldbenone assert that the stack should be empty now - causes a
1307      *      developer debug warning if it isn't.
1308      * @return string the HTML required to close any open containers inside <body>.
1309      */
1310     public function pop_all_but_last($shouldbenone = false) {
1311         if ($shouldbenone && count($this->opencontainers) != 1) {
1312             debugging('<p>Some HTML tags were opened in the body of the page but not closed.</p>' .
1313                     $this->output_log(), DEBUG_DEVELOPER);
1314         }
1315         $output = '';
1316         while (count($this->opencontainers) > 1) {
1317             $container = array_pop($this->opencontainers);
1318             $output .= $container->closehtml;
1319         }
1320         return $output;
1321     }
1323     /**
1324      * You can call this function if you want to throw away an instance of this
1325      * class without properly emptying the stack (for example, in a unit test).
1326      * Calling this method stops the destruct method from outputting a developer
1327      * debug warning. After calling this method, the instance can no longer be used.
1328      */
1329     public function discard() {
1330         $this->opencontainers = null;
1331     }
1333     /**
1334      * Adds an entry to the log.
1335      *
1336      * @param string $action The name of the action
1337      * @param string $type The type of action
1338      */
1339     protected function log($action, $type) {
1340         $this->log[] = '<li>' . $action . ' ' . $type . ' at:' .
1341                 format_backtrace(debug_backtrace()) . '</li>';
1342     }
1344     /**
1345      * Outputs the log's contents as a HTML list.
1346      *
1347      * @return string HTML list of the log
1348      */
1349     protected function output_log() {
1350         return '<ul>' . implode("\n", $this->log) . '</ul>';
1351     }