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