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