Commit | Line | Data |
---|---|---|
571fa828 | 1 | <?php |
2 | ||
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/>. | |
17 | ||
78946b9b PS |
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 | * | |
78bfb562 PS |
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 | |
78946b9b PS |
28 | */ |
29 | ||
78bfb562 PS |
30 | defined('MOODLE_INTERNAL') || die(); |
31 | ||
d9c8f425 | 32 | require_once($CFG->libdir.'/outputcomponents.php'); |
33 | require_once($CFG->libdir.'/outputactions.php'); | |
34 | require_once($CFG->libdir.'/outputfactories.php'); | |
d9c8f425 | 35 | require_once($CFG->libdir.'/outputrenderers.php'); |
0bb38e8c | 36 | require_once($CFG->libdir.'/outputrequirementslib.php'); |
d9c8f425 | 37 | |
571fa828 | 38 | /** |
78946b9b PS |
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"); | |
45 | ||
46 | set_config('themerev', empty($CFG->themerev) ? 1 : $CFG->themerev+1); | |
365bec4c | 47 | fulldelete("$CFG->cachedir/theme"); |
78946b9b PS |
48 | } |
49 | ||
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 | } | |
59 | ||
60 | /** | |
61 | * Returns current theme revision number. | |
62 | * @return int | |
571fa828 | 63 | */ |
78946b9b PS |
64 | function theme_get_revision() { |
65 | global $CFG; | |
66 | ||
67 | if (empty($CFG->themedesignermode)) { | |
68 | if (empty($CFG->themerev)) { | |
69 | return -1; | |
70 | } else { | |
71 | return $CFG->themerev; | |
72 | } | |
73 | ||
74 | } else { | |
75 | return -1; | |
76 | } | |
77 | } | |
78 | ||
571fa828 | 79 | |
571fa828 | 80 | /** |
fdeb7fa1 | 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 | |
fa1afe32 | 88 | * file, you can just ignore those, and the following information for developers. |
ebebf55c | 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. | |
fa1afe32 | 92 | * However, normally you don't need to bother, because moodle_page (that is, $PAGE) |
fdeb7fa1 | 93 | * will create one for you, accessible as $PAGE->theme. |
571fa828 | 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 | */ | |
ebebf55c | 99 | class theme_config { |
5f0baa43 PS |
100 | /** |
101 | * @var string default theme, used when requested theme not found | |
5f0baa43 | 102 | */ |
90723839 | 103 | const DEFAULT_THEME = 'standard'; |
5f0baa43 | 104 | |
ebebf55c | 105 | /** |
78946b9b PS |
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}). | |
fdeb7fa1 | 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. | |
fdeb7fa1 | 112 | * |
78946b9b | 113 | * @var array |
fdeb7fa1 | 114 | */ |
78946b9b | 115 | public $parents; |
571fa828 | 116 | |
fdeb7fa1 | 117 | /** |
78946b9b PS |
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. | |
fdeb7fa1 | 120 | * |
78946b9b | 121 | * @var array |
fdeb7fa1 | 122 | */ |
78946b9b | 123 | public $sheets = array(); |
571fa828 | 124 | |
fdeb7fa1 | 125 | /** |
2e7dbbd7 | 126 | * The names of all the stylesheets from parents that should be excluded. |
78946b9b PS |
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. | |
fdeb7fa1 | 129 | * |
78946b9b | 130 | * @var array or arrays, true means all, null means use value from parent |
fdeb7fa1 | 131 | */ |
78946b9b | 132 | public $parents_exclude_sheets = null; |
571fa828 | 133 | |
fdeb7fa1 | 134 | /** |
78946b9b PS |
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 | |
fdeb7fa1 | 139 | */ |
78946b9b | 140 | public $plugins_exclude_sheets = null; |
571fa828 | 141 | |
fdeb7fa1 | 142 | /** |
78946b9b PS |
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. | |
fdeb7fa1 | 145 | * |
78946b9b | 146 | * @var array |
fdeb7fa1 | 147 | */ |
78946b9b | 148 | public $editor_sheets = array(); |
571fa828 | 149 | |
38aafea2 PS |
150 | /** |
151 | * The names of all the javascript files this theme that you would | |
04c01408 | 152 | * like included from head, in order. Give the names of the files without .js. |
38aafea2 PS |
153 | * |
154 | * @var array | |
155 | */ | |
156 | public $javascripts = array(); | |
157 | ||
04c01408 PS |
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(); | |
4399b9d5 | 165 | |
38aafea2 | 166 | /** |
b2ae9661 | 167 | * The names of all the javascript files from parents that should be excluded. |
38aafea2 PS |
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; | |
174 | ||
fdeb7fa1 | 175 | /** |
78946b9b | 176 | * Which file to use for each page layout. |
fdeb7fa1 | 177 | * |
78946b9b PS |
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}. | |
d4a03c00 | 182 | * That file also has a good example of how to set this setting. |
fdeb7fa1 | 183 | * |
78946b9b | 184 | * For each layout, the value in the outer array is an array that describes |
d4a03c00 | 185 | * how you want that type of page to look. For example |
186 | * <pre> | |
187 | * $THEME->layouts = array( | |
39df78bf | 188 | * // Most pages - if we encounter an unknown or a missing page type, this one is used. |
191b267b | 189 | * 'standard' => array( |
78946b9b PS |
190 | * 'theme' = 'mytheme', |
191 | * 'file' => 'normal.php', | |
d4a03c00 | 192 | * 'regions' => array('side-pre', 'side-post'), |
193 | * 'defaultregion' => 'side-post' | |
194 | * ), | |
195 | * // The site home page. | |
196 | * 'home' => array( | |
78946b9b PS |
197 | * 'theme' = 'mytheme', |
198 | * 'file' => 'home.php', | |
d4a03c00 | 199 | * 'regions' => array('side-pre', 'side-post'), |
200 | * 'defaultregion' => 'side-post' | |
201 | * ), | |
202 | * // ... | |
203 | * ); | |
204 | * </pre> | |
fdeb7fa1 | 205 | * |
78946b9b PS |
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 | |
d4a03c00 | 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. | |
fdeb7fa1 | 215 | * |
d4a03c00 | 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 | |
fa1afe32 | 219 | * the page, but in non-existent regions, they appear here. (Imaging, for example, |
d4a03c00 | 220 | * that someone added blocks using a different theme that used different region |
221 | * names, and then switched to this theme.) | |
fdeb7fa1 | 222 | * |
223 | * @var array | |
224 | */ | |
d4a03c00 | 225 | public $layouts = array(); |
571fa828 | 226 | |
fdeb7fa1 | 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 | |
78946b9b | 240 | * your own custom renderers in a lib.php file in this theme (or the parent theme).</li> |
fdeb7fa1 | 241 | * </ul> |
242 | * | |
243 | * @var string name of a class implementing the {@link renderer_factory} interface. | |
244 | */ | |
ebebf55c | 245 | public $rendererfactory = 'standard_renderer_factory'; |
ebebf55c | 246 | |
fdeb7fa1 | 247 | /** |
78946b9b | 248 | * Function to do custom CSS post-processing. |
fdeb7fa1 | 249 | * |
78946b9b PS |
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. | |
fdeb7fa1 | 253 | * |
78946b9b | 254 | * @var string the name of a function. |
fdeb7fa1 | 255 | */ |
78946b9b | 256 | public $csspostprocess = null; |
571fa828 | 257 | |
258 | /** | |
78946b9b PS |
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 < > » - these are confusing for blind users. | |
fdeb7fa1 | 265 | * |
78946b9b PS |
266 | * @var string |
267 | */ | |
268 | public $rarrow = null; | |
269 | ||
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 < > » - these are confusing for blind users. | |
ebebf55c | 277 | * |
78946b9b | 278 | * @var string |
571fa828 | 279 | */ |
78946b9b PS |
280 | public $larrow = null; |
281 | ||
03276af6 DM |
282 | /** |
283 | * Some themes may want to disable ajax course editing. | |
284 | * @var bool | |
285 | */ | |
286 | public $enablecourseajax = true; | |
78946b9b PS |
287 | |
288 | //==Following properties are not configurable from theme config.php== | |
571fa828 | 289 | |
fdeb7fa1 | 290 | /** |
78946b9b PS |
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 | |
fdeb7fa1 | 294 | */ |
78946b9b | 295 | public $name; |
fdeb7fa1 | 296 | |
297 | /** | |
78946b9b PS |
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 | |
fdeb7fa1 | 301 | */ |
78946b9b | 302 | public $dir; |
fdeb7fa1 | 303 | |
304 | /** | |
78946b9b PS |
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; | |
310 | ||
d2c394f3 SH |
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; | |
317 | ||
4d56ee95 SH |
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; | |
324 | ||
78946b9b PS |
325 | /** |
326 | * Instance of the renderer_factory implementation | |
fdeb7fa1 | 327 | * we are using. Implementation detail. |
78946b9b | 328 | * @var renderer_factory |
fdeb7fa1 | 329 | */ |
330 | protected $rf = null; | |
331 | ||
332 | /** | |
78946b9b PS |
333 | * List of parent config objects. |
334 | * @var array list of parent configs | |
335 | **/ | |
336 | protected $parent_configs = array(); | |
fdeb7fa1 | 337 | |
571fa828 | 338 | /** |
ebebf55c | 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. | |
571fa828 | 344 | */ |
ebebf55c | 345 | public static function load($themename) { |
346 | global $CFG; | |
571fa828 | 347 | |
78946b9b PS |
348 | // load theme settings from db |
349 | try { | |
350 | $settings = get_config('theme_'.$themename); | |
351 | } catch (dml_exception $e) { | |
da0c0e25 | 352 | // most probably moodle tables not created yet |
365a5941 | 353 | $settings = new stdClass(); |
78946b9b | 354 | } |
ebebf55c | 355 | |
78946b9b PS |
356 | if ($config = theme_config::find_theme_config($themename, $settings)) { |
357 | return new theme_config($config); | |
5f0baa43 PS |
358 | |
359 | } else if ($themename == theme_config::DEFAULT_THEME) { | |
360 | throw new coding_exception('Default theme '.theme_config::DEFAULT_THEME.' not available or broken!'); | |
361 | ||
78946b9b PS |
362 | } else { |
363 | // bad luck, the requested theme has some problems - admin see details in theme config | |
5f0baa43 | 364 | return new theme_config(theme_config::find_theme_config(theme_config::DEFAULT_THEME, $settings)); |
78946b9b PS |
365 | } |
366 | } | |
ebebf55c | 367 | |
78946b9b PS |
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 | |
ae5b6801 | 371 | * diagnose given theme and highlights all potential problems. |
78946b9b PS |
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) { | |
5ef719e7 | 379 | //TODO: MDL-21108 |
78946b9b PS |
380 | return array(); |
381 | } | |
382 | ||
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 | |
389 | ||
390 | $this->settings = $config->settings; | |
391 | $this->name = $config->name; | |
392 | $this->dir = $config->dir; | |
393 | ||
394 | if ($this->name != 'base') { | |
395 | $baseconfig = theme_config::find_theme_config('base', $this->settings); | |
396 | } else { | |
397 | $baseconfig = $config; | |
571fa828 | 398 | } |
ebebf55c | 399 | |
fef27578 | 400 | $configurable = array('parents', 'sheets', 'parents_exclude_sheets', 'plugins_exclude_sheets', 'javascripts', 'javascripts_footer', |
fcd2cbaf PS |
401 | 'parents_exclude_javascripts', 'layouts', 'enable_dock', 'enablecourseajax', |
402 | 'rendererfactory', 'csspostprocess', 'editor_sheets', 'rarrow', 'larrow', 'hidefromselector'); | |
ebebf55c | 403 | |
78946b9b PS |
404 | foreach ($config as $key=>$value) { |
405 | if (in_array($key, $configurable)) { | |
406 | $this->$key = $value; | |
407 | } | |
408 | } | |
409 | ||
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 | } | |
4399b9d5 | 445 | |
78946b9b PS |
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 | } | |
458 | ||
459 | //fix arrows if needed | |
460 | $this->check_theme_arrows(); | |
571fa828 | 461 | } |
462 | ||
78946b9b PS |
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 > »), and must be accompanied by text. | |
34a2777c | 470 | */ |
78946b9b PS |
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 = '►'; | |
476 | $this->larrow = '◄'; | |
477 | if (empty($_SERVER['HTTP_USER_AGENT'])) { | |
478 | $uagent = ''; | |
ebebf55c | 479 | } else { |
78946b9b PS |
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 = '▶'; | |
487 | $this->larrow = '◀'; | |
488 | } | |
489 | elseif (false !== strpos($uagent, 'Konqueror')) { | |
490 | $this->rarrow = '→'; | |
491 | $this->larrow = '←'; | |
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 = '>'; | |
498 | $this->larrow = '<'; | |
499 | } | |
500 | ||
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; | |
ebebf55c | 506 | } |
ebebf55c | 507 | } |
78946b9b | 508 | } |
ebebf55c | 509 | |
78946b9b PS |
510 | /** |
511 | * Returns output renderer prefixes, these are used when looking | |
71c03ac1 | 512 | * for the overridden renderers in themes. |
78946b9b PS |
513 | * @return array |
514 | */ | |
515 | public function renderer_prefixes() { | |
516 | global $CFG; // just in case the included files need it | |
517 | ||
596af93a | 518 | $prefixes = array('theme_'.$this->name); |
78946b9b PS |
519 | |
520 | foreach ($this->parent_configs as $parent) { | |
521 | $prefixes[] = 'theme_'.$parent->name; | |
522 | } | |
523 | ||
524 | return $prefixes; | |
34a2777c | 525 | } |
526 | ||
571fa828 | 527 | /** |
78946b9b PS |
528 | * Returns the stylesheet URL of this editor content |
529 | * @param bool $encoded false means use & and true use & in URLs | |
530 | * @return string | |
571fa828 | 531 | */ |
78946b9b PS |
532 | public function editor_css_url($encoded=true) { |
533 | global $CFG; | |
534 | ||
535 | $rev = theme_get_revision(); | |
536 | ||
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); | |
571fa828 | 543 | } |
571fa828 | 544 | } |
545 | ||
546 | /** | |
78946b9b PS |
547 | * Returns the content of the CSS to be used in editor content |
548 | * @return string | |
571fa828 | 549 | */ |
71be124d | 550 | public function editor_css_files() { |
78946b9b PS |
551 | global $CFG; |
552 | ||
71be124d | 553 | $files = array(); |
78946b9b PS |
554 | |
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)) { | |
71be124d | 560 | $files['plugin_'.$plugin] = $sheetfile; |
78946b9b PS |
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) { | |
ca194849 | 569 | $sheetfile = "$parent_config->dir/style/$sheet.css"; |
78946b9b | 570 | if (is_readable($sheetfile)) { |
71be124d | 571 | $files['parent_'.$parent_config->name.'_'.$sheet] = $sheetfile; |
78946b9b PS |
572 | } |
573 | } | |
574 | } | |
575 | // finally this theme | |
576 | if (!empty($this->editor_sheets)) { | |
577 | foreach ($this->editor_sheets as $sheet) { | |
71be124d | 578 | $sheetfile = "$this->dir/style/$sheet.css"; |
78946b9b | 579 | if (is_readable($sheetfile)) { |
71be124d | 580 | $files['theme_'.$sheet] = $sheetfile; |
78946b9b PS |
581 | } |
582 | } | |
583 | } | |
584 | ||
71be124d | 585 | return $files; |
571fa828 | 586 | } |
587 | ||
588 | /** | |
78946b9b PS |
589 | * Get the stylesheet URL of this theme |
590 | * @param bool $encoded false means use & and true use & in URLs | |
38aafea2 | 591 | * @return array of moodle_url |
571fa828 | 592 | */ |
efaa4c08 | 593 | public function css_urls(moodle_page $page) { |
78946b9b PS |
594 | global $CFG; |
595 | ||
596 | $rev = theme_get_revision(); | |
597 | ||
efaa4c08 | 598 | $urls = array(); |
efaa4c08 | 599 | |
78946b9b | 600 | if ($rev > -1) { |
a6338a13 | 601 | if (check_browser_version('MSIE', 5)) { |
537740cb SH |
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)); | |
78946b9b | 608 | } |
78946b9b | 609 | } else { |
7829cf79 PS |
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 | |
d6b64d15 PS |
613 | if (!defined('THEME_DESIGNER_CACHE_LIFETIME')) { |
614 | define('THEME_DESIGNER_CACHE_LIFETIME', 4); // this can be also set in config.php | |
615 | } | |
365bec4c | 616 | $candidatesheet = "$CFG->cachedir/theme/$this->name/designer.ser"; |
7829cf79 PS |
617 | if (!file_exists($candidatesheet)) { |
618 | $css = $this->css_content(); | |
c426ef3a | 619 | check_dir_exists(dirname($candidatesheet)); |
7829cf79 PS |
620 | file_put_contents($candidatesheet, serialize($css)); |
621 | ||
d6b64d15 | 622 | } else if (filemtime($candidatesheet) > time() - THEME_DESIGNER_CACHE_LIFETIME) { |
7829cf79 PS |
623 | if ($css = file_get_contents($candidatesheet)) { |
624 | $css = unserialize($css); | |
625 | } else { | |
626 | unlink($candidatesheet); | |
627 | $css = $this->css_content(); | |
628 | } | |
629 | ||
630 | } else { | |
631 | unlink($candidatesheet); | |
632 | $css = $this->css_content(); | |
633 | file_put_contents($candidatesheet, serialize($css)); | |
634 | } | |
635 | ||
c724f835 | 636 | $baseurl = $CFG->httpswwwroot.'/theme/styles_debug.php'; |
c09e1a2c PS |
637 | |
638 | if (check_browser_version('MSIE', 5)) { | |
639 | // lalala, IE does not allow more than 31 linked CSS files from main document | |
71be124d | 640 | $urls[] = new moodle_url($baseurl, array('theme'=>$this->name, 'type'=>'ie', 'subtype'=>'plugins')); |
a6338a13 SH |
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) | |
71be124d | 643 | $urls[] = new moodle_url($baseurl, array('theme'=>$this->name,'type'=>'ie', 'subtype'=>'parents', 'sheet'=>$parent)); |
a6338a13 | 644 | } |
71be124d | 645 | $urls[] = new moodle_url($baseurl, array('theme'=>$this->name, 'type'=>'ie', 'subtype'=>'theme')); |
c09e1a2c PS |
646 | |
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 | |
78946b9b | 658 | } |
78946b9b | 659 | } |
78946b9b | 660 | } |
efaa4c08 PS |
661 | |
662 | return $urls; | |
571fa828 | 663 | } |
34a2777c | 664 | |
ebebf55c | 665 | /** |
045f492c SH |
666 | * Returns an array of organised CSS files required for this output |
667 | * @return array | |
ebebf55c | 668 | */ |
045f492c SH |
669 | public function css_files() { |
670 | $cssfiles = array('plugins'=>array(), 'parents'=>array(), 'theme'=>array()); | |
78946b9b | 671 | |
8c96c3cb | 672 | // get all plugin sheets |
045f492c | 673 | $excludes = $this->resolve_excludes('plugins_exclude_sheets'); |
78946b9b PS |
674 | if ($excludes !== true) { |
675 | foreach (get_plugin_types() as $type=>$unused) { | |
045f492c | 676 | if ($type === 'theme' || (!empty($excludes[$type]) and $excludes[$type] === true)) { |
78946b9b PS |
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 | } | |
f8bb9666 SH |
685 | |
686 | $plugincontent = ''; | |
78946b9b PS |
687 | $sheetfile = "$fulldir/styles.css"; |
688 | if (is_readable($sheetfile)) { | |
045f492c | 689 | $cssfiles['plugins'][$type.'_'.$plugin] = $sheetfile; |
f8bb9666 SH |
690 | } |
691 | $sheetthemefile = "$fulldir/styles_{$this->name}.css"; | |
692 | if (is_readable($sheetthemefile)) { | |
045f492c | 693 | $cssfiles['plugins'][$type.'_'.$plugin.'_'.$this->name] = $sheetthemefile; |
f8bb9666 | 694 | } |
78946b9b PS |
695 | } |
696 | } | |
697 | } | |
34a2777c | 698 | |
78946b9b | 699 | // find out wanted parent sheets |
045f492c | 700 | $excludes = $this->resolve_excludes('parents_exclude_sheets'); |
78946b9b PS |
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; | |
045f492c | 704 | if (empty($parent_config->sheets) || (!empty($excludes[$parent]) and $excludes[$parent] === true)) { |
78946b9b PS |
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)) { | |
045f492c | 714 | $cssfiles['parents'][$parent][$sheet] = $sheetfile; |
78946b9b PS |
715 | } |
716 | } | |
717 | } | |
718 | } | |
34a2777c | 719 | |
78946b9b PS |
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)) { | |
045f492c | 725 | $cssfiles['theme'][$sheet] = $sheetfile; |
78946b9b | 726 | } |
ebebf55c | 727 | } |
78946b9b PS |
728 | } |
729 | ||
045f492c SH |
730 | return $cssfiles; |
731 | } | |
732 | ||
733 | /** | |
734 | * Returns the content of the one huge CSS merged from all style sheets. | |
735 | * @return string | |
736 | */ | |
737 | public function css_content() { | |
71be124d SH |
738 | $files = array_merge($this->css_files(), array('editor'=>$this->editor_css_files())); |
739 | $css = $this->css_files_get_contents($files, array()); | |
78946b9b PS |
740 | return $css; |
741 | } | |
34a2777c | 742 | |
045f492c SH |
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 | |
71c03ac1 | 750 | * @param array $keys An array of previous array keys [recursive addition] |
045f492c SH |
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 | } | |
764 | ||
e68c5f36 PS |
765 | |
766 | /** | |
767 | * Get the javascript URL of this theme | |
71c03ac1 | 768 | * @param bool $inhead true means head url, false means footer |
e68c5f36 PS |
769 | * @return moodle_url |
770 | */ | |
baeb20bb | 771 | public function javascript_url($inhead) { |
e68c5f36 PS |
772 | global $CFG; |
773 | ||
774 | $rev = theme_get_revision(); | |
e68c5f36 | 775 | $params = array('theme'=>$this->name,'rev'=>$rev); |
baeb20bb PS |
776 | $params['type'] = $inhead ? 'head' : 'footer'; |
777 | ||
778 | return new moodle_url($CFG->httpswwwroot.'/theme/javascript.php', $params); | |
e68c5f36 PS |
779 | } |
780 | ||
045f492c SH |
781 | public function javascript_files($type) { |
782 | if ($type === 'footer') { | |
783 | $type = 'javascripts_footer'; | |
784 | } else { | |
f856106b | 785 | $type = 'javascripts'; |
045f492c | 786 | } |
04c01408 | 787 | |
358c13cb PS |
788 | $js = array(); |
789 | // find out wanted parent javascripts | |
045f492c | 790 | $excludes = $this->resolve_excludes('parents_exclude_javascripts'); |
358c13cb PS |
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; | |
04c01408 | 794 | if (empty($parent_config->$type)) { |
358c13cb PS |
795 | continue; |
796 | } | |
797 | if (!empty($excludes[$parent]) and $excludes[$parent] === true) { | |
798 | continue; | |
799 | } | |
04c01408 | 800 | foreach ($parent_config->$type as $javascript) { |
358c13cb PS |
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)) { | |
045f492c | 807 | $js[] = $javascriptfile; |
358c13cb PS |
808 | } |
809 | } | |
810 | } | |
811 | } | |
812 | ||
813 | // current theme javascripts | |
04c01408 PS |
814 | if (is_array($this->$type)) { |
815 | foreach ($this->$type as $javascript) { | |
358c13cb PS |
816 | $javascriptfile = "$this->dir/javascript/$javascript.js"; |
817 | if (is_readable($javascriptfile)) { | |
045f492c SH |
818 | $js[] = $javascriptfile; |
819 | } | |
820 | } | |
821 | } | |
822 | ||
823 | return $js; | |
824 | } | |
825 | ||
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; | |
358c13cb PS |
845 | } |
846 | } | |
847 | } | |
045f492c SH |
848 | return $setting; |
849 | } | |
358c13cb | 850 | |
045f492c SH |
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; | |
e68c5f36 PS |
863 | } |
864 | ||
045f492c | 865 | public function post_process($css) { |
df06c1c4 PS |
866 | global $CFG; |
867 | ||
78946b9b PS |
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], '|'); | |
b9bc2019 | 878 | $imageurl = $this->pix_url($imagename, $component)->out(false); |
df06c1c4 PS |
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); | |
ebebf55c | 882 | } |
34a2777c | 883 | } |
17a6649b | 884 | |
78946b9b PS |
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); | |
fdeb7fa1 | 889 | } |
890 | ||
78946b9b | 891 | return $css; |
ebebf55c | 892 | } |
17a6649b | 893 | |
ebebf55c | 894 | /** |
78946b9b PS |
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 | |
fdeb7fa1 | 900 | */ |
c39e5ba2 | 901 | public function pix_url($imagename, $component) { |
fdeb7fa1 | 902 | global $CFG; |
78946b9b PS |
903 | |
904 | $params = array('theme'=>$this->name, 'image'=>$imagename); | |
905 | ||
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; | |
fdeb7fa1 | 912 | } |
78946b9b PS |
913 | |
914 | return new moodle_url("$CFG->httpswwwroot/theme/image.php", $params); | |
fdeb7fa1 | 915 | } |
916 | ||
917 | /** | |
78946b9b PS |
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 | |
fdeb7fa1 | 922 | */ |
78946b9b PS |
923 | public function resolve_image_location($image, $component) { |
924 | global $CFG; | |
925 | ||
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; | |
939 | ||
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; | |
953 | ||
78946b9b PS |
954 | } else { |
955 | if (strpos($component, '_') === false) { | |
956 | $component = 'mod_'.$component; | |
957 | } | |
958 | list($type, $plugin) = explode('_', $component, 2); | |
959 | ||
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; | |
fdeb7fa1 | 973 | } |
fdeb7fa1 | 974 | } |
975 | ||
d4a03c00 | 976 | /** |
78946b9b PS |
977 | * Checks if file with any image extension exists. |
978 | * @param string $filepath | |
979 | * @return string image name with extension | |
d4a03c00 | 980 | */ |
78946b9b PS |
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"; | |
d4a03c00 | 990 | } else { |
78946b9b | 991 | return false; |
d4a03c00 | 992 | } |
993 | } | |
994 | ||
fdeb7fa1 | 995 | /** |
78946b9b PS |
996 | * Loads the theme config from config.php file. |
997 | * @param string $themename | |
998 | * @param object $settings from config_plugins table | |
999 | * @return object | |
fdeb7fa1 | 1000 | */ |
78946b9b PS |
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. | |
fdeb7fa1 | 1004 | |
78946b9b PS |
1005 | if (!$dir = theme_config::find_theme_location($themename)) { |
1006 | return null; | |
fdeb7fa1 | 1007 | } |
2f67a9b3 | 1008 | |
365a5941 | 1009 | $THEME = new stdClass(); |
78946b9b PS |
1010 | $THEME->name = $themename; |
1011 | $THEME->dir = $dir; | |
1012 | $THEME->settings = $settings; | |
1013 | ||
1014 | global $CFG; // just in case somebody tries to use $CFG in theme config | |
1015 | include("$THEME->dir/config.php"); | |
1016 | ||
1017 | // verify the theme configuration is OK | |
1018 | if (!is_array($THEME->parents)) { | |
1019 | // parents option is mandatory now | |
1020 | return null; | |
fdeb7fa1 | 1021 | } |
1022 | ||
78946b9b | 1023 | return $THEME; |
fdeb7fa1 | 1024 | } |
1025 | ||
d4a03c00 | 1026 | /** |
78946b9b PS |
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 | |
d4a03c00 | 1031 | */ |
78946b9b PS |
1032 | private static function find_theme_location($themename) { |
1033 | global $CFG; | |
1034 | ||
1035 | if (file_exists("$CFG->dirroot/theme/$themename/config.php")) { | |
1036 | $dir = "$CFG->dirroot/theme/$themename"; | |
d4a03c00 | 1037 | |
3dd1d7e7 DM |
1038 | } else if (!empty($CFG->themedir) and file_exists("$CFG->themedir/$themename/config.php")) { |
1039 | $dir = "$CFG->themedir/$themename"; | |
1040 | ||
7d875874 | 1041 | } else { |
78946b9b | 1042 | return null; |
d4a03c00 | 1043 | } |
78946b9b PS |
1044 | |
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 | } | |
2f67a9b3 | 1049 | |
78946b9b | 1050 | return $dir; |
d4a03c00 | 1051 | } |
1052 | ||
1d13c75c | 1053 | /** |
78946b9b | 1054 | * Get the renderer for a part of Moodle for this theme. |
78946b9b | 1055 | * @param moodle_page $page the page we are rendering |
56cbc53b | 1056 | * @param string $module the name of part of moodle. E.g. 'core', 'quiz', 'qtype_multichoice'. |
78946b9b | 1057 | * @param string $subtype optional subtype such as 'news' resulting to 'mod_forum_news' |
c927e35c | 1058 | * @param string $target one of rendering target constants |
78946b9b | 1059 | * @return renderer_base the requested renderer. |
1d13c75c | 1060 | */ |
c927e35c | 1061 | public function get_renderer(moodle_page $page, $component, $subtype = null, $target = null) { |
78946b9b | 1062 | if (is_null($this->rf)) { |
c927e35c | 1063 | $classname = $this->rendererfactory; |
78946b9b | 1064 | $this->rf = new $classname($this); |
1d13c75c | 1065 | } |
78946b9b | 1066 | |
c927e35c | 1067 | return $this->rf->get_renderer($page, $component, $subtype, $target); |
1d13c75c | 1068 | } |
1069 | ||
fdeb7fa1 | 1070 | /** |
78946b9b PS |
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}. | |
fdeb7fa1 | 1074 | */ |
78946b9b PS |
1075 | protected function layout_info_for_page($pagelayout) { |
1076 | if (array_key_exists($pagelayout, $this->layouts)) { | |
1077 | return $this->layouts[$pagelayout]; | |
1078 | } else { | |
191b267b PS |
1079 | debugging('Invalid page layout specified: ' . $pagelayout); |
1080 | return $this->layouts['standard']; | |
fdeb7fa1 | 1081 | } |
1082 | } | |
1083 | ||
1084 | /** | |
78946b9b PS |
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 | |
ebebf55c | 1092 | */ |
78946b9b | 1093 | public function layout_file($pagelayout) { |
ebebf55c | 1094 | global $CFG; |
fdeb7fa1 | 1095 | |
78946b9b PS |
1096 | $layoutinfo = $this->layout_info_for_page($pagelayout); |
1097 | $layoutfile = $layoutinfo['file']; | |
fdeb7fa1 | 1098 | |
84db3ea2 SH |
1099 | if (array_key_exists('theme', $layoutinfo)) { |
1100 | $themes = array($layoutinfo['theme']); | |
1101 | } else { | |
1102 | $themes = array_merge(array($this->name),$this->parents); | |
1103 | } | |
4399b9d5 | 1104 | |
84db3ea2 SH |
1105 | foreach ($themes as $theme) { |
1106 | if ($dir = $this->find_theme_location($theme)) { | |
1107 | $path = "$dir/layout/$layoutfile"; | |
1108 | ||
1109 | // Check the template exists, return general base theme template if not. | |
1110 | if (is_readable($path)) { | |
1111 | return $path; | |
1112 | } | |
78946b9b | 1113 | } |
34a2777c | 1114 | } |
1115 | ||
191b267b | 1116 | debugging('Can not find layout file for: ' . $pagelayout); |
78946b9b PS |
1117 | // fallback to standard normal layout |
1118 | return "$CFG->dirroot/theme/base/layout/general.php"; | |
1119 | } | |
ebebf55c | 1120 | |
78946b9b PS |
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']; | |
34a2777c | 1130 | } |
78946b9b PS |
1131 | return array(); |
1132 | } | |
34a2777c | 1133 | |
78946b9b PS |
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']); | |
ebebf55c | 1146 | } |
34a2777c | 1147 | } |
1148 | ||
cadc0d28 TH |
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 | } | |
1155 | ||
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 | } | |
1164 | ||
1165 | // Last resort, try the base theme for names | |
1166 | return get_string('region-' . $region, 'theme_base'); | |
1167 | } | |
1168 | ||
ebebf55c | 1169 | /** |
78946b9b PS |
1170 | * Get the list of all block regions known to this theme in all templates. |
1171 | * @return array internal region name => human readable name. | |
ebebf55c | 1172 | */ |
78946b9b PS |
1173 | public function get_all_block_regions() { |
1174 | $regions = array(); | |
78946b9b PS |
1175 | foreach ($this->layouts as $layoutinfo) { |
1176 | foreach ($layoutinfo['regions'] as $region) { | |
a9535f79 | 1177 | $regions[$region] = $this->get_region_name($region, $this->name); |
ebebf55c | 1178 | } |
34a2777c | 1179 | } |
78946b9b | 1180 | return $regions; |
34a2777c | 1181 | } |
d609d962 SH |
1182 | |
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 | } | |
ebebf55c | 1191 | } |
34a2777c | 1192 | |
78946b9b | 1193 | |
ebebf55c | 1194 | /** |
d9c8f425 | 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. | |
ebebf55c | 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 | */ | |
d9c8f425 | 1206 | class xhtml_container_stack { |
1207 | /** @var array stores the list of open containers. */ | |
1208 | protected $opencontainers = array(); | |
fa1afe32 | 1209 | /** |
d9c8f425 | 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. | |
fa1afe32 | 1212 | */ |
d9c8f425 | 1213 | protected $log = array(); |
fa1afe32 | 1214 | /** |
d9c8f425 | 1215 | * Store whether we are developer debug mode. We need this in several places |
71c03ac1 | 1216 | * including in the destructor where we may not have access to $CFG. |
d9c8f425 | 1217 | * @var boolean |
fa1afe32 | 1218 | */ |
d9c8f425 | 1219 | protected $isdebugging; |
34a2777c | 1220 | |
d9c8f425 | 1221 | public function __construct() { |
1222 | $this->isdebugging = debugging('', DEBUG_DEVELOPER); | |
ebebf55c | 1223 | } |
34a2777c | 1224 | |
fa1afe32 | 1225 | /** |
d9c8f425 | 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 | |
fa1afe32 | 1231 | */ |
d9c8f425 | 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); | |
3aaa27f4 | 1238 | } |
d9c8f425 | 1239 | array_push($this->opencontainers, $container); |
ebebf55c | 1240 | } |
34a2777c | 1241 | |
fa1afe32 | 1242 | /** |
d9c8f425 | 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. | |
fa1afe32 | 1248 | */ |
d9c8f425 | 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; | |
3aaa27f4 | 1254 | } |
ebebf55c | 1255 | |
d9c8f425 | 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); | |
ebebf55c | 1262 | } |
d9c8f425 | 1263 | if ($this->isdebugging) { |
1264 | $this->log('Close', $type); | |
e8775320 | 1265 | } |
d9c8f425 | 1266 | return $container->closehtml; |
ebebf55c | 1267 | } |
e8775320 | 1268 | |
fa1afe32 | 1269 | /** |
d9c8f425 | 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>. | |
fa1afe32 | 1276 | */ |
d9c8f425 | 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; | |
e8775320 | 1286 | } |
d9c8f425 | 1287 | return $output; |
e8775320 | 1288 | } |
34a2777c | 1289 | |
ebebf55c | 1290 | /** |
d9c8f425 | 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 | |
ebebf55c | 1296 | */ |
d9c8f425 | 1297 | public function discard() { |
1298 | $this->opencontainers = null; | |
ebebf55c | 1299 | } |
d9c8f425 | 1300 | |
fa1afe32 | 1301 | /** |
d9c8f425 | 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 | |
fa1afe32 | 1306 | */ |
d9c8f425 | 1307 | protected function log($action, $type) { |
1308 | $this->log[] = '<li>' . $action . ' ' . $type . ' at:' . | |
1309 | format_backtrace(debug_backtrace()) . '</li>'; | |
ebebf55c | 1310 | } |
34a2777c | 1311 | |
fa1afe32 | 1312 | /** |
d9c8f425 | 1313 | * Outputs the log's contents as a HTML list. |
1314 | * @return string HTML list of the log | |
fa1afe32 | 1315 | */ |
d9c8f425 | 1316 | protected function output_log() { |
1317 | return '<ul>' . implode("\n", $this->log) . '</ul>'; | |
34a2777c | 1318 | } |
1319 | } |