See Release Notes
Long Term Support Release
Differences Between: [Versions 39 and 310] [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]
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 * Returns current theme revision number. 39 * 40 * @return int 41 */ 42 function theme_get_revision() { 43 global $CFG; 44 45 if (empty($CFG->themedesignermode)) { 46 if (empty($CFG->themerev)) { 47 // This only happens during install. It doesn't matter what themerev we use as long as it's positive. 48 return 1; 49 } else { 50 return $CFG->themerev; 51 } 52 53 } else { 54 return -1; 55 } 56 } 57 58 /** 59 * Returns current theme sub revision number. This is the revision for 60 * this theme exclusively, not the global theme revision. 61 * 62 * @param string $themename The non-frankenstyle name of the theme 63 * @return int 64 */ 65 function theme_get_sub_revision_for_theme($themename) { 66 global $CFG; 67 68 if (empty($CFG->themedesignermode)) { 69 $pluginname = "theme_{$themename}"; 70 $revision = during_initial_install() ? null : get_config($pluginname, 'themerev'); 71 72 if (empty($revision)) { 73 // This only happens during install. It doesn't matter what themerev we use as long as it's positive. 74 return 1; 75 } else { 76 return $revision; 77 } 78 } else { 79 return -1; 80 } 81 } 82 83 /** 84 * Calculates and returns the next theme revision number. 85 * 86 * @return int 87 */ 88 function theme_get_next_revision() { 89 global $CFG; 90 91 $next = time(); 92 if (isset($CFG->themerev) and $next <= $CFG->themerev and $CFG->themerev - $next < 60*60) { 93 // This resolves problems when reset is requested repeatedly within 1s, 94 // the < 1h condition prevents accidental switching to future dates 95 // because we might not recover from it. 96 $next = $CFG->themerev+1; 97 } 98 99 return $next; 100 } 101 102 /** 103 * Calculates and returns the next theme revision number. 104 * 105 * @param string $themename The non-frankenstyle name of the theme 106 * @return int 107 */ 108 function theme_get_next_sub_revision_for_theme($themename) { 109 global $CFG; 110 111 $next = time(); 112 $current = theme_get_sub_revision_for_theme($themename); 113 if ($next <= $current and $current - $next < 60 * 60) { 114 // This resolves problems when reset is requested repeatedly within 1s, 115 // the < 1h condition prevents accidental switching to future dates 116 // because we might not recover from it. 117 $next = $current + 1; 118 } 119 120 return $next; 121 } 122 123 /** 124 * Sets the current theme revision number. 125 * 126 * @param int $revision The new theme revision number 127 */ 128 function theme_set_revision($revision) { 129 set_config('themerev', $revision); 130 } 131 132 /** 133 * Sets the current theme revision number for a specific theme. 134 * This does not affect the global themerev value. 135 * 136 * @param string $themename The non-frankenstyle name of the theme 137 * @param int $revision The new theme revision number 138 */ 139 function theme_set_sub_revision_for_theme($themename, $revision) { 140 set_config('themerev', $revision, "theme_{$themename}"); 141 } 142 143 /** 144 * Get the path to a theme config.php file. 145 * 146 * @param string $themename The non-frankenstyle name of the theme to check 147 */ 148 function theme_get_config_file_path($themename) { 149 global $CFG; 150 151 if (file_exists("{$CFG->dirroot}/theme/{$themename}/config.php")) { 152 return "{$CFG->dirroot}/theme/{$themename}/config.php"; 153 } else if (!empty($CFG->themedir) and file_exists("{$CFG->themedir}/{$themename}/config.php")) { 154 return "{$CFG->themedir}/{$themename}/config.php"; 155 } else { 156 return null; 157 } 158 } 159 160 /** 161 * Get the path to the local cached CSS file. 162 * 163 * @param string $themename The non-frankenstyle theme name. 164 * @param int $globalrevision The global theme revision. 165 * @param int $themerevision The theme specific revision. 166 * @param string $direction Either 'ltr' or 'rtl' (case sensitive). 167 */ 168 function theme_get_css_filename($themename, $globalrevision, $themerevision, $direction) { 169 global $CFG; 170 171 $path = "{$CFG->localcachedir}/theme/{$globalrevision}/{$themename}/css"; 172 $filename = $direction == 'rtl' ? "all-rtl_{$themerevision}" : "all_{$themerevision}"; 173 return "{$path}/{$filename}.css"; 174 } 175 176 /** 177 * Generates and saves the CSS files for the given theme configs. 178 * 179 * @param theme_config[] $themeconfigs An array of theme_config instances. 180 * @param array $directions Must be a subset of ['rtl', 'ltr']. 181 * @param bool $cache Should the generated files be stored in local cache. 182 * @return array The built theme content in a multi-dimensional array of name => direction => content 183 */ 184 function theme_build_css_for_themes($themeconfigs = [], $directions = ['rtl', 'ltr'], $cache = true): array { 185 global $CFG; 186 187 if (empty($themeconfigs)) { 188 return []; 189 } 190 191 require_once("{$CFG->libdir}/csslib.php"); 192 193 $themescss = []; 194 $themerev = theme_get_revision(); 195 // Make sure the local cache directory exists. 196 make_localcache_directory('theme'); 197 198 foreach ($themeconfigs as $themeconfig) { 199 $themecss = []; 200 $oldrevision = theme_get_sub_revision_for_theme($themeconfig->name); 201 $newrevision = theme_get_next_sub_revision_for_theme($themeconfig->name); 202 203 // First generate all the new css. 204 foreach ($directions as $direction) { 205 // Lock it on. Technically we should build all themes for SVG and no SVG - but ie9 is out of support. 206 $themeconfig->force_svg_use(true); 207 $themeconfig->set_rtl_mode(($direction === 'rtl')); 208 209 $themecss[$direction] = $themeconfig->get_css_content(); 210 if ($cache) { 211 $themeconfig->set_css_content_cache($themecss[$direction]); 212 $filename = theme_get_css_filename($themeconfig->name, $themerev, $newrevision, $direction); 213 css_store_css($themeconfig, $filename, $themecss[$direction]); 214 } 215 } 216 $themescss[$themeconfig->name] = $themecss; 217 218 if ($cache) { 219 // Only update the theme revision after we've successfully created the 220 // new CSS cache. 221 theme_set_sub_revision_for_theme($themeconfig->name, $newrevision); 222 223 // Now purge old files. We must purge all old files in the local cache 224 // because we've incremented the theme sub revision. This will leave any 225 // files with the old revision inaccessbile so we might as well removed 226 // them from disk. 227 foreach (['ltr', 'rtl'] as $direction) { 228 $oldcss = theme_get_css_filename($themeconfig->name, $themerev, $oldrevision, $direction); 229 if (file_exists($oldcss)) { 230 unlink($oldcss); 231 } 232 } 233 } 234 } 235 236 return $themescss; 237 } 238 239 /** 240 * Invalidate all server and client side caches. 241 * 242 * This method deletes the physical directory that is used to cache the theme 243 * files used for serving. 244 * Because it deletes the main theme cache directory all themes are reset by 245 * this function. 246 */ 247 function theme_reset_all_caches() { 248 global $CFG, $PAGE; 249 require_once("{$CFG->libdir}/filelib.php"); 250 251 $next = theme_get_next_revision(); 252 theme_set_revision($next); 253 254 if (!empty($CFG->themedesignermode)) { 255 $cache = cache::make_from_params(cache_store::MODE_APPLICATION, 'core', 'themedesigner'); 256 $cache->purge(); 257 } 258 259 // Purge compiled post processed css. 260 cache::make('core', 'postprocessedcss')->purge(); 261 262 // Delete all old theme localcaches. 263 $themecachedirs = glob("{$CFG->localcachedir}/theme/*", GLOB_ONLYDIR); 264 foreach ($themecachedirs as $localcachedir) { 265 fulldelete($localcachedir); 266 } 267 268 if ($PAGE) { 269 $PAGE->reload_theme(); 270 } 271 } 272 273 /** 274 * Enable or disable theme designer mode. 275 * 276 * @param bool $state 277 */ 278 function theme_set_designer_mod($state) { 279 set_config('themedesignermode', (int)!empty($state)); 280 // Reset caches after switching mode so that any designer mode caches get purged too. 281 theme_reset_all_caches(); 282 } 283 284 /** 285 * Checks if the given device has a theme defined in config.php. 286 * 287 * @return bool 288 */ 289 function theme_is_device_locked($device) { 290 global $CFG; 291 $themeconfigname = core_useragent::get_device_type_cfg_var_name($device); 292 return isset($CFG->config_php_settings[$themeconfigname]); 293 } 294 295 /** 296 * Returns the theme named defined in config.php for the given device. 297 * 298 * @return string or null 299 */ 300 function theme_get_locked_theme_for_device($device) { 301 global $CFG; 302 303 if (!theme_is_device_locked($device)) { 304 return null; 305 } 306 307 $themeconfigname = core_useragent::get_device_type_cfg_var_name($device); 308 return $CFG->config_php_settings[$themeconfigname]; 309 } 310 311 /** 312 * This class represents the configuration variables of a Moodle theme. 313 * 314 * All the variables with access: public below (with a few exceptions that are marked) 315 * are the properties you can set in your themes config.php file. 316 * 317 * There are also some methods and protected variables that are part of the inner 318 * workings of Moodle's themes system. If you are just editing a themes config.php 319 * file, you can just ignore those, and the following information for developers. 320 * 321 * Normally, to create an instance of this class, you should use the 322 * {@link theme_config::load()} factory method to load a themes config.php file. 323 * However, normally you don't need to bother, because moodle_page (that is, $PAGE) 324 * will create one for you, accessible as $PAGE->theme. 325 * 326 * @copyright 2009 Tim Hunt 327 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 328 * @since Moodle 2.0 329 * @package core 330 * @category output 331 */ 332 class theme_config { 333 334 /** 335 * @var string Default theme, used when requested theme not found. 336 */ 337 const DEFAULT_THEME = 'boost'; 338 339 /** The key under which the SCSS file is stored amongst the CSS files. */ 340 const SCSS_KEY = '__SCSS__'; 341 342 /** 343 * @var array You can base your theme on other themes by linking to the other theme as 344 * parents. This lets you use the CSS and layouts from the other themes 345 * (see {@link theme_config::$layouts}). 346 * That makes it easy to create a new theme that is similar to another one 347 * but with a few changes. In this themes CSS you only need to override 348 * those rules you want to change. 349 */ 350 public $parents; 351 352 /** 353 * @var array The names of all the stylesheets from this theme that you would 354 * like included, in order. Give the names of the files without .css. 355 */ 356 public $sheets = array(); 357 358 /** 359 * @var array The names of all the stylesheets from parents that should be excluded. 360 * true value may be used to specify all parents or all themes from one parent. 361 * If no value specified value from parent theme used. 362 */ 363 public $parents_exclude_sheets = null; 364 365 /** 366 * @var array List of plugin sheets to be excluded. 367 * If no value specified value from parent theme used. 368 */ 369 public $plugins_exclude_sheets = null; 370 371 /** 372 * @var array List of style sheets that are included in the text editor bodies. 373 * Sheets from parent themes are used automatically and can not be excluded. 374 */ 375 public $editor_sheets = array(); 376 377 /** 378 * @var bool Whether a fallback version of the stylesheet will be used 379 * whilst the final version is generated. 380 */ 381 public $usefallback = false; 382 383 /** 384 * @var array The names of all the javascript files this theme that you would 385 * like included from head, in order. Give the names of the files without .js. 386 */ 387 public $javascripts = array(); 388 389 /** 390 * @var array The names of all the javascript files this theme that you would 391 * like included from footer, in order. Give the names of the files without .js. 392 */ 393 public $javascripts_footer = array(); 394 395 /** 396 * @var array The names of all the javascript files from parents that should 397 * be excluded. true value may be used to specify all parents or all themes 398 * from one parent. 399 * If no value specified value from parent theme used. 400 */ 401 public $parents_exclude_javascripts = null; 402 403 /** 404 * @var array Which file to use for each page layout. 405 * 406 * This is an array of arrays. The keys of the outer array are the different layouts. 407 * Pages in Moodle are using several different layouts like 'normal', 'course', 'home', 408 * 'popup', 'form', .... The most reliable way to get a complete list is to look at 409 * {@link http://cvs.moodle.org/moodle/theme/base/config.php?view=markup the base theme config.php file}. 410 * That file also has a good example of how to set this setting. 411 * 412 * For each layout, the value in the outer array is an array that describes 413 * how you want that type of page to look. For example 414 * <pre> 415 * $THEME->layouts = array( 416 * // Most pages - if we encounter an unknown or a missing page type, this one is used. 417 * 'standard' => array( 418 * 'theme' = 'mytheme', 419 * 'file' => 'normal.php', 420 * 'regions' => array('side-pre', 'side-post'), 421 * 'defaultregion' => 'side-post' 422 * ), 423 * // The site home page. 424 * 'home' => array( 425 * 'theme' = 'mytheme', 426 * 'file' => 'home.php', 427 * 'regions' => array('side-pre', 'side-post'), 428 * 'defaultregion' => 'side-post' 429 * ), 430 * // ... 431 * ); 432 * </pre> 433 * 434 * 'theme' name of the theme where is the layout located 435 * 'file' is the layout file to use for this type of page. 436 * layout files are stored in layout subfolder 437 * 'regions' This lists the regions on the page where blocks may appear. For 438 * each region you list here, your layout file must include a call to 439 * <pre> 440 * echo $OUTPUT->blocks_for_region($regionname); 441 * </pre> 442 * or equivalent so that the blocks are actually visible. 443 * 444 * 'defaultregion' If the list of regions is non-empty, then you must pick 445 * one of the one of them as 'default'. This has two meanings. First, this is 446 * where new blocks are added. Second, if there are any blocks associated with 447 * the page, but in non-existent regions, they appear here. (Imaging, for example, 448 * that someone added blocks using a different theme that used different region 449 * names, and then switched to this theme.) 450 */ 451 public $layouts = array(); 452 453 /** 454 * @var string Name of the renderer factory class to use. Must implement the 455 * {@link renderer_factory} interface. 456 * 457 * This is an advanced feature. Moodle output is generated by 'renderers', 458 * you can customise the HTML that is output by writing custom renderers, 459 * and then you need to specify 'renderer factory' so that Moodle can find 460 * your renderers. 461 * 462 * There are some renderer factories supplied with Moodle. Please follow these 463 * links to see what they do. 464 * <ul> 465 * <li>{@link standard_renderer_factory} - the default.</li> 466 * <li>{@link theme_overridden_renderer_factory} - use this if you want to write 467 * your own custom renderers in a lib.php file in this theme (or the parent theme).</li> 468 * </ul> 469 */ 470 public $rendererfactory = 'standard_renderer_factory'; 471 472 /** 473 * @var string Function to do custom CSS post-processing. 474 * 475 * This is an advanced feature. If you want to do custom post-processing on the 476 * CSS before it is output (for example, to replace certain variable names 477 * with particular values) you can give the name of a function here. 478 */ 479 public $csspostprocess = null; 480 481 /** 482 * @var string Function to do custom CSS post-processing on a parsed CSS tree. 483 * 484 * This is an advanced feature. If you want to do custom post-processing on the 485 * CSS before it is output, you can provide the name of the function here. The 486 * function will receive a CSS tree document as first parameter, and the theme_config 487 * object as second parameter. A return value is not required, the tree can 488 * be edited in place. 489 */ 490 public $csstreepostprocessor = null; 491 492 /** 493 * @var string Accessibility: Right arrow-like character is 494 * used in the breadcrumb trail, course navigation menu 495 * (previous/next activity), calendar, and search forum block. 496 * If the theme does not set characters, appropriate defaults 497 * are set automatically. Please DO NOT 498 * use < > » - these are confusing for blind users. 499 */ 500 public $rarrow = null; 501 502 /** 503 * @var string Accessibility: Left arrow-like character is 504 * used in the breadcrumb trail, course navigation menu 505 * (previous/next activity), calendar, and search forum block. 506 * If the theme does not set characters, appropriate defaults 507 * are set automatically. Please DO NOT 508 * use < > » - these are confusing for blind users. 509 */ 510 public $larrow = null; 511 512 /** 513 * @var string Accessibility: Up arrow-like character is used in 514 * the book heirarchical navigation. 515 * If the theme does not set characters, appropriate defaults 516 * are set automatically. Please DO NOT 517 * use ^ - this is confusing for blind users. 518 */ 519 public $uarrow = null; 520 521 /** 522 * @var string Accessibility: Down arrow-like character. 523 * If the theme does not set characters, appropriate defaults 524 * are set automatically. 525 */ 526 public $darrow = null; 527 528 /** 529 * @var bool Some themes may want to disable ajax course editing. 530 */ 531 public $enablecourseajax = true; 532 533 /** 534 * @var string Determines served document types 535 * - 'html5' the only officially supported doctype in Moodle 536 * - 'xhtml5' may be used in development for validation (not intended for production servers!) 537 * - 'xhtml' XHTML 1.0 Strict for legacy themes only 538 */ 539 public $doctype = 'html5'; 540 541 /** 542 * @var string requiredblocks If set to a string, will list the block types that cannot be deleted. Defaults to 543 * navigation and settings. 544 */ 545 public $requiredblocks = false; 546 547 //==Following properties are not configurable from theme config.php== 548 549 /** 550 * @var string The name of this theme. Set automatically when this theme is 551 * loaded. This can not be set in theme config.php 552 */ 553 public $name; 554 555 /** 556 * @var string The folder where this themes files are stored. This is set 557 * automatically. This can not be set in theme config.php 558 */ 559 public $dir; 560 561 /** 562 * @var stdClass Theme settings stored in config_plugins table. 563 * This can not be set in theme config.php 564 */ 565 public $settings = null; 566 567 /** 568 * @var bool If set to true and the theme enables the dock then blocks will be able 569 * to be moved to the special dock 570 */ 571 public $enable_dock = false; 572 573 /** 574 * @var bool If set to true then this theme will not be shown in the theme selector unless 575 * theme designer mode is turned on. 576 */ 577 public $hidefromselector = false; 578 579 /** 580 * @var array list of YUI CSS modules to be included on each page. This may be used 581 * to remove cssreset and use cssnormalise module instead. 582 */ 583 public $yuicssmodules = array('cssreset', 'cssfonts', 'cssgrids', 'cssbase'); 584 585 /** 586 * An associative array of block manipulations that should be made if the user is using an rtl language. 587 * The key is the original block region, and the value is the block region to change to. 588 * This is used when displaying blocks for regions only. 589 * @var array 590 */ 591 public $blockrtlmanipulations = array(); 592 593 /** 594 * @var renderer_factory Instance of the renderer_factory implementation 595 * we are using. Implementation detail. 596 */ 597 protected $rf = null; 598 599 /** 600 * @var array List of parent config objects. 601 **/ 602 protected $parent_configs = array(); 603 604 /** 605 * Used to determine whether we can serve SVG images or not. 606 * @var bool 607 */ 608 private $usesvg = null; 609 610 /** 611 * Whether in RTL mode or not. 612 * @var bool 613 */ 614 protected $rtlmode = false; 615 616 /** 617 * The SCSS file to compile (without .scss), located in the scss/ folder of the theme. 618 * Or a Closure, which receives the theme_config as argument and must 619 * return the SCSS content. 620 * @var string|Closure 621 */ 622 public $scss = false; 623 624 /** 625 * Local cache of the SCSS property. 626 * @var false|array 627 */ 628 protected $scsscache = null; 629 630 /** 631 * The name of the function to call to get the SCSS code to inject. 632 * @var string 633 */ 634 public $extrascsscallback = null; 635 636 /** 637 * The name of the function to call to get SCSS to prepend. 638 * @var string 639 */ 640 public $prescsscallback = null; 641 642 /** 643 * Sets the render method that should be used for rendering custom block regions by scripts such as my/index.php 644 * Defaults to {@link core_renderer::blocks_for_region()} 645 * @var string 646 */ 647 public $blockrendermethod = null; 648 649 /** 650 * Remember the results of icon remapping for the current page. 651 * @var array 652 */ 653 public $remapiconcache = []; 654 655 /** 656 * The name of the function to call to get precompiled CSS. 657 * @var string 658 */ 659 public $precompiledcsscallback = null; 660 661 /** 662 * Load the config.php file for a particular theme, and return an instance 663 * of this class. (That is, this is a factory method.) 664 * 665 * @param string $themename the name of the theme. 666 * @return theme_config an instance of this class. 667 */ 668 public static function load($themename) { 669 global $CFG; 670 671 // load theme settings from db 672 try { 673 $settings = get_config('theme_'.$themename); 674 } catch (dml_exception $e) { 675 // most probably moodle tables not created yet 676 $settings = new stdClass(); 677 } 678 679 if ($config = theme_config::find_theme_config($themename, $settings)) { 680 return new theme_config($config); 681 682 } else if ($themename == theme_config::DEFAULT_THEME) { 683 throw new coding_exception('Default theme '.theme_config::DEFAULT_THEME.' not available or broken!'); 684 685 } else if ($config = theme_config::find_theme_config($CFG->theme, $settings)) { 686 debugging('This page should be using theme ' . $themename . 687 ' which cannot be initialised. Falling back to the site theme ' . $CFG->theme, DEBUG_NORMAL); 688 return new theme_config($config); 689 690 } else { 691 // bad luck, the requested theme has some problems - admin see details in theme config 692 debugging('This page should be using theme ' . $themename . 693 ' which cannot be initialised. Nor can the site theme ' . $CFG->theme . 694 '. Falling back to ' . theme_config::DEFAULT_THEME, DEBUG_NORMAL); 695 return new theme_config(theme_config::find_theme_config(theme_config::DEFAULT_THEME, $settings)); 696 } 697 } 698 699 /** 700 * Theme diagnostic code. It is very problematic to send debug output 701 * to the actual CSS file, instead this functions is supposed to 702 * diagnose given theme and highlights all potential problems. 703 * This information should be available from the theme selection page 704 * or some other debug page for theme designers. 705 * 706 * @param string $themename 707 * @return array description of problems 708 */ 709 public static function diagnose($themename) { 710 //TODO: MDL-21108 711 return array(); 712 } 713 714 /** 715 * Private constructor, can be called only from the factory method. 716 * @param stdClass $config 717 */ 718 private function __construct($config) { 719 global $CFG; //needed for included lib.php files 720 721 $this->settings = $config->settings; 722 $this->name = $config->name; 723 $this->dir = $config->dir; 724 725 if ($this->name != self::DEFAULT_THEME) { 726 $baseconfig = self::find_theme_config(self::DEFAULT_THEME, $this->settings); 727 } else { 728 $baseconfig = $config; 729 } 730 731 $configurable = array( 732 'parents', 'sheets', 'parents_exclude_sheets', 'plugins_exclude_sheets', 'usefallback', 733 'javascripts', 'javascripts_footer', 'parents_exclude_javascripts', 734 'layouts', 'enablecourseajax', 'requiredblocks', 735 'rendererfactory', 'csspostprocess', 'editor_sheets', 'editor_scss', 'rarrow', 'larrow', 'uarrow', 'darrow', 736 'hidefromselector', 'doctype', 'yuicssmodules', 'blockrtlmanipulations', 'blockrendermethod', 737 'scss', 'extrascsscallback', 'prescsscallback', 'csstreepostprocessor', 'addblockposition', 738 'iconsystem', 'precompiledcsscallback'); 739 740 foreach ($config as $key=>$value) { 741 if (in_array($key, $configurable)) { 742 $this->$key = $value; 743 } 744 } 745 746 // verify all parents and load configs and renderers 747 foreach ($this->parents as $parent) { 748 if (!$parent_config = theme_config::find_theme_config($parent, $this->settings)) { 749 // this is not good - better exclude faulty parents 750 continue; 751 } 752 $libfile = $parent_config->dir.'/lib.php'; 753 if (is_readable($libfile)) { 754 // theme may store various function here 755 include_once($libfile); 756 } 757 $renderersfile = $parent_config->dir.'/renderers.php'; 758 if (is_readable($renderersfile)) { 759 // may contain core and plugin renderers and renderer factory 760 include_once($renderersfile); 761 } 762 $this->parent_configs[$parent] = $parent_config; 763 } 764 $libfile = $this->dir.'/lib.php'; 765 if (is_readable($libfile)) { 766 // theme may store various function here 767 include_once($libfile); 768 } 769 $rendererfile = $this->dir.'/renderers.php'; 770 if (is_readable($rendererfile)) { 771 // may contain core and plugin renderers and renderer factory 772 include_once($rendererfile); 773 } else { 774 // check if renderers.php file is missnamed renderer.php 775 if (is_readable($this->dir.'/renderer.php')) { 776 debugging('Developer hint: '.$this->dir.'/renderer.php should be renamed to ' . $this->dir."/renderers.php. 777 See: http://docs.moodle.org/dev/Output_renderers#Theme_renderers.", DEBUG_DEVELOPER); 778 } 779 } 780 781 // cascade all layouts properly 782 foreach ($baseconfig->layouts as $layout=>$value) { 783 if (!isset($this->layouts[$layout])) { 784 foreach ($this->parent_configs as $parent_config) { 785 if (isset($parent_config->layouts[$layout])) { 786 $this->layouts[$layout] = $parent_config->layouts[$layout]; 787 continue 2; 788 } 789 } 790 $this->layouts[$layout] = $value; 791 } 792 } 793 794 //fix arrows if needed 795 $this->check_theme_arrows(); 796 } 797 798 /** 799 * Let the theme initialise the page object (usually $PAGE). 800 * 801 * This may be used for example to request jQuery in add-ons. 802 * 803 * @param moodle_page $page 804 */ 805 public function init_page(moodle_page $page) { 806 $themeinitfunction = 'theme_'.$this->name.'_page_init'; 807 if (function_exists($themeinitfunction)) { 808 $themeinitfunction($page); 809 } 810 } 811 812 /** 813 * Checks if arrows $THEME->rarrow, $THEME->larrow, $THEME->uarrow, $THEME->darrow have been set (theme/-/config.php). 814 * If not it applies sensible defaults. 815 * 816 * Accessibility: right and left arrow Unicode characters for breadcrumb, calendar, 817 * search forum block, etc. Important: these are 'silent' in a screen-reader 818 * (unlike > »), and must be accompanied by text. 819 */ 820 private function check_theme_arrows() { 821 if (!isset($this->rarrow) and !isset($this->larrow)) { 822 // Default, looks good in Win XP/IE 6, Win/Firefox 1.5, Win/Netscape 8... 823 // Also OK in Win 9x/2K/IE 5.x 824 $this->rarrow = '►'; 825 $this->larrow = '◄'; 826 $this->uarrow = '▲'; 827 $this->darrow = '▼'; 828 if (empty($_SERVER['HTTP_USER_AGENT'])) { 829 $uagent = ''; 830 } else { 831 $uagent = $_SERVER['HTTP_USER_AGENT']; 832 } 833 if (false !== strpos($uagent, 'Opera') 834 || false !== strpos($uagent, 'Mac')) { 835 // Looks good in Win XP/Mac/Opera 8/9, Mac/Firefox 2, Camino, Safari. 836 // Not broken in Mac/IE 5, Mac/Netscape 7 (?). 837 $this->rarrow = '▶︎'; 838 $this->larrow = '◀︎'; 839 } 840 elseif ((false !== strpos($uagent, 'Konqueror')) 841 || (false !== strpos($uagent, 'Android'))) { 842 // The fonts on Android don't include the characters required for this to work as expected. 843 // So we use the same ones Konqueror uses. 844 $this->rarrow = '→'; 845 $this->larrow = '←'; 846 $this->uarrow = '↑'; 847 $this->darrow = '↓'; 848 } 849 elseif (isset($_SERVER['HTTP_ACCEPT_CHARSET']) 850 && false === stripos($_SERVER['HTTP_ACCEPT_CHARSET'], 'utf-8')) { 851 // (Win/IE 5 doesn't set ACCEPT_CHARSET, but handles Unicode.) 852 // To be safe, non-Unicode browsers! 853 $this->rarrow = '>'; 854 $this->larrow = '<'; 855 $this->uarrow = '^'; 856 $this->darrow = 'v'; 857 } 858 859 // RTL support - in RTL languages, swap r and l arrows 860 if (right_to_left()) { 861 $t = $this->rarrow; 862 $this->rarrow = $this->larrow; 863 $this->larrow = $t; 864 } 865 } 866 } 867 868 /** 869 * Returns output renderer prefixes, these are used when looking 870 * for the overridden renderers in themes. 871 * 872 * @return array 873 */ 874 public function renderer_prefixes() { 875 global $CFG; // just in case the included files need it 876 877 $prefixes = array('theme_'.$this->name); 878 879 foreach ($this->parent_configs as $parent) { 880 $prefixes[] = 'theme_'.$parent->name; 881 } 882 883 return $prefixes; 884 } 885 886 /** 887 * Returns the stylesheet URL of this editor content 888 * 889 * @param bool $encoded false means use & and true use & in URLs 890 * @return moodle_url 891 */ 892 public function editor_css_url($encoded=true) { 893 global $CFG; 894 $rev = theme_get_revision(); 895 if ($rev > -1) { 896 $themesubrevision = theme_get_sub_revision_for_theme($this->name); 897 898 // Provide the sub revision to allow us to invalidate cached theme CSS 899 // on a per theme basis, rather than globally. 900 if ($themesubrevision && $themesubrevision > 0) { 901 $rev .= "_{$themesubrevision}"; 902 } 903 904 $url = new moodle_url("/theme/styles.php"); 905 if (!empty($CFG->slasharguments)) { 906 $url->set_slashargument('/'.$this->name.'/'.$rev.'/editor', 'noparam', true); 907 } else { 908 $url->params(array('theme'=>$this->name,'rev'=>$rev, 'type'=>'editor')); 909 } 910 } else { 911 $params = array('theme'=>$this->name, 'type'=>'editor'); 912 $url = new moodle_url('/theme/styles_debug.php', $params); 913 } 914 return $url; 915 } 916 917 /** 918 * Returns the content of the CSS to be used in editor content 919 * 920 * @return array 921 */ 922 public function editor_css_files() { 923 $files = array(); 924 925 // First editor plugins. 926 $plugins = core_component::get_plugin_list('editor'); 927 foreach ($plugins as $plugin=>$fulldir) { 928 $sheetfile = "$fulldir/editor_styles.css"; 929 if (is_readable($sheetfile)) { 930 $files['plugin_'.$plugin] = $sheetfile; 931 } 932 } 933 // Then parent themes - base first, the immediate parent last. 934 foreach (array_reverse($this->parent_configs) as $parent_config) { 935 if (empty($parent_config->editor_sheets)) { 936 continue; 937 } 938 foreach ($parent_config->editor_sheets as $sheet) { 939 $sheetfile = "$parent_config->dir/style/$sheet.css"; 940 if (is_readable($sheetfile)) { 941 $files['parent_'.$parent_config->name.'_'.$sheet] = $sheetfile; 942 } 943 } 944 } 945 // Finally this theme. 946 if (!empty($this->editor_sheets)) { 947 foreach ($this->editor_sheets as $sheet) { 948 $sheetfile = "$this->dir/style/$sheet.css"; 949 if (is_readable($sheetfile)) { 950 $files['theme_'.$sheet] = $sheetfile; 951 } 952 } 953 } 954 955 return $files; 956 } 957 958 /** 959 * Compiles and returns the content of the SCSS to be used in editor content 960 * 961 * @return string Compiled CSS from the editor SCSS 962 */ 963 public function editor_scss_to_css() { 964 $css = ''; 965 $dir = $this->dir; 966 $filenames = []; 967 968 // Use editor_scss file(s) provided by this theme if set. 969 if (!empty($this->editor_scss)) { 970 $filenames = $this->editor_scss; 971 } else { 972 // If no editor_scss set, move up theme hierarchy until one is found (if at all). 973 // This is so child themes only need to set editor_scss if an override is required. 974 foreach (array_reverse($this->parent_configs) as $parentconfig) { 975 if (!empty($parentconfig->editor_scss)) { 976 $dir = $parentconfig->dir; 977 $filenames = $parentconfig->editor_scss; 978 979 // Config found, stop looking. 980 break; 981 } 982 } 983 } 984 985 if (!empty($filenames)) { 986 $compiler = new core_scss(); 987 988 foreach ($filenames as $filename) { 989 $compiler->set_file("{$dir}/scss/{$filename}.scss"); 990 991 try { 992 $css .= $compiler->to_css(); 993 } catch (\Exception $e) { 994 debugging('Error while compiling editor SCSS: ' . $e->getMessage(), DEBUG_DEVELOPER); 995 } 996 } 997 } 998 999 return $css; 1000 } 1001 1002 /** 1003 * Get the stylesheet URL of this theme. 1004 * 1005 * @param moodle_page $page Not used... deprecated? 1006 * @return moodle_url[] 1007 */ 1008 public function css_urls(moodle_page $page) { 1009 global $CFG; 1010 1011 $rev = theme_get_revision(); 1012 1013 $urls = array(); 1014 1015 $svg = $this->use_svg_icons(); 1016 $separate = (core_useragent::is_ie() && !core_useragent::check_ie_version('10')); 1017 1018 if ($rev > -1) { 1019 $filename = right_to_left() ? 'all-rtl' : 'all'; 1020 $url = new moodle_url("/theme/styles.php"); 1021 $themesubrevision = theme_get_sub_revision_for_theme($this->name); 1022 1023 // Provide the sub revision to allow us to invalidate cached theme CSS 1024 // on a per theme basis, rather than globally. 1025 if ($themesubrevision && $themesubrevision > 0) { 1026 $rev .= "_{$themesubrevision}"; 1027 } 1028 1029 if (!empty($CFG->slasharguments)) { 1030 $slashargs = ''; 1031 if (!$svg) { 1032 // We add a simple /_s to the start of the path. 1033 // The underscore is used to ensure that it isn't a valid theme name. 1034 $slashargs .= '/_s'.$slashargs; 1035 } 1036 $slashargs .= '/'.$this->name.'/'.$rev.'/'.$filename; 1037 if ($separate) { 1038 $slashargs .= '/chunk0'; 1039 } 1040 $url->set_slashargument($slashargs, 'noparam', true); 1041 } else { 1042 $params = array('theme' => $this->name, 'rev' => $rev, 'type' => $filename); 1043 if (!$svg) { 1044 // We add an SVG param so that we know not to serve SVG images. 1045 // We do this because all modern browsers support SVG and this param will one day be removed. 1046 $params['svg'] = '0'; 1047 } 1048 if ($separate) { 1049 $params['chunk'] = '0'; 1050 } 1051 $url->params($params); 1052 } 1053 $urls[] = $url; 1054 1055 } else { 1056 $baseurl = new moodle_url('/theme/styles_debug.php'); 1057 1058 $css = $this->get_css_files(true); 1059 if (!$svg) { 1060 // We add an SVG param so that we know not to serve SVG images. 1061 // We do this because all modern browsers support SVG and this param will one day be removed. 1062 $baseurl->param('svg', '0'); 1063 } 1064 if (right_to_left()) { 1065 $baseurl->param('rtl', 1); 1066 } 1067 if ($separate) { 1068 // We might need to chunk long files. 1069 $baseurl->param('chunk', '0'); 1070 } 1071 if (core_useragent::is_ie()) { 1072 // Lalala, IE does not allow more than 31 linked CSS files from main document. 1073 $urls[] = new moodle_url($baseurl, array('theme'=>$this->name, 'type'=>'ie', 'subtype'=>'plugins')); 1074 foreach ($css['parents'] as $parent=>$sheets) { 1075 // We need to serve parents individually otherwise we may easily exceed the style limit IE imposes (4096). 1076 $urls[] = new moodle_url($baseurl, array('theme'=>$this->name,'type'=>'ie', 'subtype'=>'parents', 'sheet'=>$parent)); 1077 } 1078 if ($this->get_scss_property()) { 1079 // No need to define the type as IE here. 1080 $urls[] = new moodle_url($baseurl, array('theme' => $this->name, 'type' => 'scss')); 1081 } 1082 $urls[] = new moodle_url($baseurl, array('theme'=>$this->name, 'type'=>'ie', 'subtype'=>'theme')); 1083 1084 } else { 1085 foreach ($css['plugins'] as $plugin=>$unused) { 1086 $urls[] = new moodle_url($baseurl, array('theme'=>$this->name,'type'=>'plugin', 'subtype'=>$plugin)); 1087 } 1088 foreach ($css['parents'] as $parent=>$sheets) { 1089 foreach ($sheets as $sheet=>$unused2) { 1090 $urls[] = new moodle_url($baseurl, array('theme'=>$this->name,'type'=>'parent', 'subtype'=>$parent, 'sheet'=>$sheet)); 1091 } 1092 } 1093 foreach ($css['theme'] as $sheet => $filename) { 1094 if ($sheet === self::SCSS_KEY) { 1095 // This is the theme SCSS file. 1096 $urls[] = new moodle_url($baseurl, array('theme' => $this->name, 'type' => 'scss')); 1097 } else { 1098 // Sheet first in order to make long urls easier to read. 1099 $urls[] = new moodle_url($baseurl, array('sheet'=>$sheet, 'theme'=>$this->name, 'type'=>'theme')); 1100 } 1101 } 1102 } 1103 } 1104 1105 // Allow themes to change the css url to something like theme/mytheme/mycss.php. 1106 component_callback('theme_' . $this->name, 'alter_css_urls', [&$urls]); 1107 return $urls; 1108 } 1109 1110 /** 1111 * Get the whole css stylesheet for production mode. 1112 * 1113 * NOTE: this method is not expected to be used from any addons. 1114 * 1115 * @return string CSS markup compressed 1116 */ 1117 public function get_css_content() { 1118 1119 $csscontent = ''; 1120 foreach ($this->get_css_files(false) as $type => $value) { 1121 foreach ($value as $identifier => $val) { 1122 if (is_array($val)) { 1123 foreach ($val as $v) { 1124 $csscontent .= file_get_contents($v) . "\n"; 1125 } 1126 } else { 1127 if ($type === 'theme' && $identifier === self::SCSS_KEY) { 1128 // We need the content from SCSS because this is the SCSS file from the theme. 1129 if ($compiled = $this->get_css_content_from_scss(false)) { 1130 $csscontent .= $compiled; 1131 } else { 1132 // The compiler failed so default back to any precompiled css that might 1133 // exist. 1134 $csscontent .= $this->get_precompiled_css_content(); 1135 } 1136 } else { 1137 $csscontent .= file_get_contents($val) . "\n"; 1138 } 1139 } 1140 } 1141 } 1142 $csscontent = $this->post_process($csscontent); 1143 $csscontent = core_minify::css($csscontent); 1144 1145 return $csscontent; 1146 } 1147 /** 1148 * Set post processed CSS content cache. 1149 * 1150 * @param string $csscontent The post processed CSS content. 1151 * @return bool True if the content was successfully cached. 1152 */ 1153 public function set_css_content_cache($csscontent) { 1154 1155 $cache = cache::make('core', 'postprocessedcss'); 1156 $key = $this->get_css_cache_key(); 1157 1158 return $cache->set($key, $csscontent); 1159 } 1160 1161 /** 1162 * Return whether the post processed CSS content has been cached. 1163 * 1164 * @return bool Whether the post-processed CSS is available in the cache. 1165 */ 1166 public function has_css_cached_content() { 1167 1168 $key = $this->get_css_cache_key(); 1169 $cache = cache::make('core', 'postprocessedcss'); 1170 1171 return $cache->has($key); 1172 } 1173 1174 /** 1175 * Return cached post processed CSS content. 1176 * 1177 * @return bool|string The cached css content or false if not found. 1178 */ 1179 public function get_css_cached_content() { 1180 1181 $key = $this->get_css_cache_key(); 1182 $cache = cache::make('core', 'postprocessedcss'); 1183 1184 return $cache->get($key); 1185 } 1186 1187 /** 1188 * Generate the css content cache key. 1189 * 1190 * @return string The post processed css cache key. 1191 */ 1192 public function get_css_cache_key() { 1193 $nosvg = (!$this->use_svg_icons()) ? 'nosvg_' : ''; 1194 $rtlmode = ($this->rtlmode == true) ? 'rtl' : 'ltr'; 1195 1196 return $nosvg . $this->name . '_' . $rtlmode; 1197 } 1198 1199 /** 1200 * Get the theme designer css markup, 1201 * the parameters are coming from css_urls(). 1202 * 1203 * NOTE: this method is not expected to be used from any addons. 1204 * 1205 * @param string $type 1206 * @param string $subtype 1207 * @param string $sheet 1208 * @return string CSS markup 1209 */ 1210 public function get_css_content_debug($type, $subtype, $sheet) { 1211 if ($type === 'scss') { 1212 // The SCSS file of the theme is requested. 1213 $csscontent = $this->get_css_content_from_scss(true); 1214 if ($csscontent !== false) { 1215 return $this->post_process($csscontent); 1216 } 1217 return ''; 1218 } 1219 1220 $cssfiles = array(); 1221 $css = $this->get_css_files(true); 1222 1223 if ($type === 'ie') { 1224 // IE is a sloppy browser with weird limits, sorry. 1225 if ($subtype === 'plugins') { 1226 $cssfiles = $css['plugins']; 1227 1228 } else if ($subtype === 'parents') { 1229 if (empty($sheet)) { 1230 // Do not bother with the empty parent here. 1231 } else { 1232 // Build up the CSS for that parent so we can serve it as one file. 1233 foreach ($css[$subtype][$sheet] as $parent => $css) { 1234 $cssfiles[] = $css; 1235 } 1236 } 1237 } else if ($subtype === 'theme') { 1238 $cssfiles = $css['theme']; 1239 foreach ($cssfiles as $key => $value) { 1240 if (in_array($key, [self::SCSS_KEY])) { 1241 // Remove the SCSS file from the theme CSS files. 1242 // The SCSS files use the type 'scss', not 'ie'. 1243 unset($cssfiles[$key]); 1244 } 1245 } 1246 } 1247 1248 } else if ($type === 'plugin') { 1249 if (isset($css['plugins'][$subtype])) { 1250 $cssfiles[] = $css['plugins'][$subtype]; 1251 } 1252 1253 } else if ($type === 'parent') { 1254 if (isset($css['parents'][$subtype][$sheet])) { 1255 $cssfiles[] = $css['parents'][$subtype][$sheet]; 1256 } 1257 1258 } else if ($type === 'theme') { 1259 if (isset($css['theme'][$sheet])) { 1260 $cssfiles[] = $css['theme'][$sheet]; 1261 } 1262 } 1263 1264 $csscontent = ''; 1265 foreach ($cssfiles as $file) { 1266 $contents = file_get_contents($file); 1267 $contents = $this->post_process($contents); 1268 $comment = "/** Path: $type $subtype $sheet.' **/\n"; 1269 $stats = ''; 1270 $csscontent .= $comment.$stats.$contents."\n\n"; 1271 } 1272 1273 return $csscontent; 1274 } 1275 1276 /** 1277 * Get the whole css stylesheet for editor iframe. 1278 * 1279 * NOTE: this method is not expected to be used from any addons. 1280 * 1281 * @return string CSS markup 1282 */ 1283 public function get_css_content_editor() { 1284 $css = ''; 1285 $cssfiles = $this->editor_css_files(); 1286 1287 // If editor has static CSS, include it. 1288 foreach ($cssfiles as $file) { 1289 $css .= file_get_contents($file)."\n"; 1290 } 1291 1292 // If editor has SCSS, compile and include it. 1293 if (($convertedscss = $this->editor_scss_to_css())) { 1294 $css .= $convertedscss; 1295 } 1296 1297 $output = $this->post_process($css); 1298 1299 return $output; 1300 } 1301 1302 /** 1303 * Returns an array of organised CSS files required for this output. 1304 * 1305 * @param bool $themedesigner 1306 * @return array nested array of file paths 1307 */ 1308 protected function get_css_files($themedesigner) { 1309 global $CFG; 1310 1311 $cache = null; 1312 $cachekey = 'cssfiles'; 1313 if ($themedesigner) { 1314 require_once($CFG->dirroot.'/lib/csslib.php'); 1315 // We need some kind of caching here because otherwise the page navigation becomes 1316 // way too slow in theme designer mode. Feel free to create full cache definition later... 1317 $cache = cache::make_from_params(cache_store::MODE_APPLICATION, 'core', 'themedesigner', array('theme' => $this->name)); 1318 if ($files = $cache->get($cachekey)) { 1319 if ($files['created'] > time() - THEME_DESIGNER_CACHE_LIFETIME) { 1320 unset($files['created']); 1321 return $files; 1322 } 1323 } 1324 } 1325 1326 $cssfiles = array('plugins'=>array(), 'parents'=>array(), 'theme'=>array()); 1327 1328 // Get all plugin sheets. 1329 $excludes = $this->resolve_excludes('plugins_exclude_sheets'); 1330 if ($excludes !== true) { 1331 foreach (core_component::get_plugin_types() as $type=>$unused) { 1332 if ($type === 'theme' || (!empty($excludes[$type]) and $excludes[$type] === true)) { 1333 continue; 1334 } 1335 $plugins = core_component::get_plugin_list($type); 1336 foreach ($plugins as $plugin=>$fulldir) { 1337 if (!empty($excludes[$type]) and is_array($excludes[$type]) 1338 and in_array($plugin, $excludes[$type])) { 1339 continue; 1340 } 1341 1342 // Get the CSS from the plugin. 1343 $sheetfile = "$fulldir/styles.css"; 1344 if (is_readable($sheetfile)) { 1345 $cssfiles['plugins'][$type.'_'.$plugin] = $sheetfile; 1346 } 1347 1348 // Create a list of candidate sheets from parents (direct parent last) and current theme. 1349 $candidates = array(); 1350 foreach (array_reverse($this->parent_configs) as $parent_config) { 1351 $candidates[] = $parent_config->name; 1352 } 1353 $candidates[] = $this->name; 1354 1355 // Add the sheets found. 1356 foreach ($candidates as $candidate) { 1357 $sheetthemefile = "$fulldir/styles_{$candidate}.css"; 1358 if (is_readable($sheetthemefile)) { 1359 $cssfiles['plugins'][$type.'_'.$plugin.'_'.$candidate] = $sheetthemefile; 1360 } 1361 } 1362 } 1363 } 1364 } 1365 1366 // Find out wanted parent sheets. 1367 $excludes = $this->resolve_excludes('parents_exclude_sheets'); 1368 if ($excludes !== true) { 1369 foreach (array_reverse($this->parent_configs) as $parent_config) { // Base first, the immediate parent last. 1370 $parent = $parent_config->name; 1371 if (empty($parent_config->sheets) || (!empty($excludes[$parent]) and $excludes[$parent] === true)) { 1372 continue; 1373 } 1374 foreach ($parent_config->sheets as $sheet) { 1375 if (!empty($excludes[$parent]) && is_array($excludes[$parent]) 1376 && in_array($sheet, $excludes[$parent])) { 1377 continue; 1378 } 1379 1380 // We never refer to the parent LESS files. 1381 $sheetfile = "$parent_config->dir/style/$sheet.css"; 1382 if (is_readable($sheetfile)) { 1383 $cssfiles['parents'][$parent][$sheet] = $sheetfile; 1384 } 1385 } 1386 } 1387 } 1388 1389 1390 // Current theme sheets. 1391 // We first add the SCSS file because we want the CSS ones to 1392 // be included after the SCSS code. 1393 if ($this->get_scss_property()) { 1394 $cssfiles['theme'][self::SCSS_KEY] = true; 1395 } 1396 if (is_array($this->sheets)) { 1397 foreach ($this->sheets as $sheet) { 1398 $sheetfile = "$this->dir/style/$sheet.css"; 1399 if (is_readable($sheetfile) && !isset($cssfiles['theme'][$sheet])) { 1400 $cssfiles['theme'][$sheet] = $sheetfile; 1401 } 1402 } 1403 } 1404 1405 if ($cache) { 1406 $files = $cssfiles; 1407 $files['created'] = time(); 1408 $cache->set($cachekey, $files); 1409 } 1410 return $cssfiles; 1411 } 1412 1413 /** 1414 * Return the CSS content generated from the SCSS file. 1415 * 1416 * @param bool $themedesigner True if theme designer is enabled. 1417 * @return bool|string Return false when the compilation failed. Else the compiled string. 1418 */ 1419 protected function get_css_content_from_scss($themedesigner) { 1420 global $CFG; 1421 1422 list($paths, $scss) = $this->get_scss_property(); 1423 if (!$scss) { 1424 throw new coding_exception('The theme did not define a SCSS file, or it is not readable.'); 1425 } 1426 1427 // We might need more memory/time to do this, so let's play safe. 1428 raise_memory_limit(MEMORY_EXTRA); 1429 core_php_time_limit::raise(300); 1430 1431 // TODO: MDL-62757 When changing anything in this method please do not forget to check 1432 // if the validate() method in class admin_setting_configthemepreset needs updating too. 1433 $cacheoptions = ''; 1434 if ($themedesigner) { 1435 $scsscachedir = $CFG->localcachedir . '/scsscache/'; 1436 $cacheoptions = array( 1437 'cacheDir' => $scsscachedir, 1438 'prefix' => 'scssphp_', 1439 'forceRefresh' => false, 1440 ); 1441 } 1442 // Set-up the compiler. 1443 $compiler = new core_scss($cacheoptions); 1444 $compiler->prepend_raw_scss($this->get_pre_scss_code()); 1445 if (is_string($scss)) { 1446 $compiler->set_file($scss); 1447 } else { 1448 $compiler->append_raw_scss($scss($this)); 1449 $compiler->setImportPaths($paths); 1450 } 1451 $compiler->append_raw_scss($this->get_extra_scss_code()); 1452 1453 try { 1454 // Compile! 1455 $compiled = $compiler->to_css(); 1456 1457 } catch (\Exception $e) { 1458 $compiled = false; 1459 debugging('Error while compiling SCSS: ' . $e->getMessage(), DEBUG_DEVELOPER); 1460 } 1461 1462 // Try to save memory. 1463 $compiler = null; 1464 unset($compiler); 1465 1466 return $compiled; 1467 } 1468 1469 /** 1470 * Return the precompiled CSS if the precompiledcsscallback exists. 1471 * 1472 * @return string Return compiled css. 1473 */ 1474 public function get_precompiled_css_content() { 1475 $configs = array_reverse($this->parent_configs) + [$this]; 1476 $css = ''; 1477 1478 foreach ($configs as $config) { 1479 if (isset($config->precompiledcsscallback)) { 1480 $function = $config->precompiledcsscallback; 1481 if (function_exists($function)) { 1482 $css .= $function($this); 1483 } 1484 } 1485 } 1486 return $css; 1487 } 1488 1489 /** 1490 * Get the icon system to use. 1491 * 1492 * @return string 1493 */ 1494 public function get_icon_system() { 1495 1496 // Getting all the candidate functions. 1497 $system = false; 1498 if (isset($this->iconsystem) && \core\output\icon_system::is_valid_system($this->iconsystem)) { 1499 return $this->iconsystem; 1500 } 1501 foreach ($this->parent_configs as $parent_config) { 1502 if (isset($parent_config->iconsystem) && \core\output\icon_system::is_valid_system($parent_config->iconsystem)) { 1503 return $parent_config->iconsystem; 1504 } 1505 } 1506 return \core\output\icon_system::STANDARD; 1507 } 1508 1509 /** 1510 * Return extra SCSS code to add when compiling. 1511 * 1512 * This is intended to be used by themes to inject some SCSS code 1513 * before it gets compiled. If you want to inject variables you 1514 * should use {@link self::get_scss_variables()}. 1515 * 1516 * @return string The SCSS code to inject. 1517 */ 1518 public function get_extra_scss_code() { 1519 $content = ''; 1520 1521 // Getting all the candidate functions. 1522 $candidates = array(); 1523 foreach ($this->parent_configs as $parent_config) { 1524 if (!isset($parent_config->extrascsscallback)) { 1525 continue; 1526 } 1527 $candidates[] = $parent_config->extrascsscallback; 1528 } 1529 $candidates[] = $this->extrascsscallback; 1530 1531 // Calling the functions. 1532 foreach ($candidates as $function) { 1533 if (function_exists($function)) { 1534 $content .= "\n/** Extra SCSS from $function **/\n" . $function($this) . "\n"; 1535 } 1536 } 1537 1538 return $content; 1539 } 1540 1541 /** 1542 * SCSS code to prepend when compiling. 1543 * 1544 * This is intended to be used by themes to inject SCSS code before it gets compiled. 1545 * 1546 * @return string The SCSS code to inject. 1547 */ 1548 public function get_pre_scss_code() { 1549 $content = ''; 1550 1551 // Getting all the candidate functions. 1552 $candidates = array(); 1553 foreach ($this->parent_configs as $parent_config) { 1554 if (!isset($parent_config->prescsscallback)) { 1555 continue; 1556 } 1557 $candidates[] = $parent_config->prescsscallback; 1558 } 1559 $candidates[] = $this->prescsscallback; 1560 1561 // Calling the functions. 1562 foreach ($candidates as $function) { 1563 if (function_exists($function)) { 1564 $content .= "\n/** Pre-SCSS from $function **/\n" . $function($this) . "\n"; 1565 } 1566 } 1567 1568 return $content; 1569 } 1570 1571 /** 1572 * Get the SCSS property. 1573 * 1574 * This resolves whether a SCSS file (or content) has to be used when generating 1575 * the stylesheet for the theme. It will look at parents themes and check the 1576 * SCSS properties there. 1577 * 1578 * @return False when SCSS is not used. 1579 * An array with the import paths, and the path to the SCSS file or Closure as second. 1580 */ 1581 public function get_scss_property() { 1582 if ($this->scsscache === null) { 1583 $configs = [$this] + $this->parent_configs; 1584 $scss = null; 1585 1586 foreach ($configs as $config) { 1587 $path = "{$config->dir}/scss"; 1588 1589 // We collect the SCSS property until we've found one. 1590 if (empty($scss) && !empty($config->scss)) { 1591 $candidate = is_string($config->scss) ? "{$path}/{$config->scss}.scss" : $config->scss; 1592 if ($candidate instanceof Closure) { 1593 $scss = $candidate; 1594 } else if (is_string($candidate) && is_readable($candidate)) { 1595 $scss = $candidate; 1596 } 1597 } 1598 1599 // We collect the import paths once we've found a SCSS property. 1600 if ($scss && is_dir($path)) { 1601 $paths[] = $path; 1602 } 1603 1604 } 1605 1606 $this->scsscache = $scss !== null ? [$paths, $scss] : false; 1607 } 1608 1609 return $this->scsscache; 1610 } 1611 1612 /** 1613 * Generate a URL to the file that serves theme JavaScript files. 1614 * 1615 * If we determine that the theme has no relevant files, then we return 1616 * early with a null value. 1617 * 1618 * @param bool $inhead true means head url, false means footer 1619 * @return moodle_url|null 1620 */ 1621 public function javascript_url($inhead) { 1622 global $CFG; 1623 1624 $rev = theme_get_revision(); 1625 $params = array('theme'=>$this->name,'rev'=>$rev); 1626 $params['type'] = $inhead ? 'head' : 'footer'; 1627 1628 // Return early if there are no files to serve 1629 if (count($this->javascript_files($params['type'])) === 0) { 1630 return null; 1631 } 1632 1633 if (!empty($CFG->slasharguments) and $rev > 0) { 1634 $url = new moodle_url("/theme/javascript.php"); 1635 $url->set_slashargument('/'.$this->name.'/'.$rev.'/'.$params['type'], 'noparam', true); 1636 return $url; 1637 } else { 1638 return new moodle_url('/theme/javascript.php', $params); 1639 } 1640 } 1641 1642 /** 1643 * Get the URL's for the JavaScript files used by this theme. 1644 * They won't be served directly, instead they'll be mediated through 1645 * theme/javascript.php. 1646 * 1647 * @param string $type Either javascripts_footer, or javascripts 1648 * @return array 1649 */ 1650 public function javascript_files($type) { 1651 if ($type === 'footer') { 1652 $type = 'javascripts_footer'; 1653 } else { 1654 $type = 'javascripts'; 1655 } 1656 1657 $js = array(); 1658 // find out wanted parent javascripts 1659 $excludes = $this->resolve_excludes('parents_exclude_javascripts'); 1660 if ($excludes !== true) { 1661 foreach (array_reverse($this->parent_configs) as $parent_config) { // base first, the immediate parent last 1662 $parent = $parent_config->name; 1663 if (empty($parent_config->$type)) { 1664 continue; 1665 } 1666 if (!empty($excludes[$parent]) and $excludes[$parent] === true) { 1667 continue; 1668 } 1669 foreach ($parent_config->$type as $javascript) { 1670 if (!empty($excludes[$parent]) and is_array($excludes[$parent]) 1671 and in_array($javascript, $excludes[$parent])) { 1672 continue; 1673 } 1674 $javascriptfile = "$parent_config->dir/javascript/$javascript.js"; 1675 if (is_readable($javascriptfile)) { 1676 $js[] = $javascriptfile; 1677 } 1678 } 1679 } 1680 } 1681 1682 // current theme javascripts 1683 if (is_array($this->$type)) { 1684 foreach ($this->$type as $javascript) { 1685 $javascriptfile = "$this->dir/javascript/$javascript.js"; 1686 if (is_readable($javascriptfile)) { 1687 $js[] = $javascriptfile; 1688 } 1689 } 1690 } 1691 return $js; 1692 } 1693 1694 /** 1695 * Resolves an exclude setting to the themes setting is applicable or the 1696 * setting of its closest parent. 1697 * 1698 * @param string $variable The name of the setting the exclude setting to resolve 1699 * @param string $default 1700 * @return mixed 1701 */ 1702 protected function resolve_excludes($variable, $default = null) { 1703 $setting = $default; 1704 if (is_array($this->{$variable}) or $this->{$variable} === true) { 1705 $setting = $this->{$variable}; 1706 } else { 1707 foreach ($this->parent_configs as $parent_config) { // the immediate parent first, base last 1708 if (!isset($parent_config->{$variable})) { 1709 continue; 1710 } 1711 if (is_array($parent_config->{$variable}) or $parent_config->{$variable} === true) { 1712 $setting = $parent_config->{$variable}; 1713 break; 1714 } 1715 } 1716 } 1717 return $setting; 1718 } 1719 1720 /** 1721 * Returns the content of the one huge javascript file merged from all theme javascript files. 1722 * 1723 * @param bool $type 1724 * @return string 1725 */ 1726 public function javascript_content($type) { 1727 $jsfiles = $this->javascript_files($type); 1728 $js = ''; 1729 foreach ($jsfiles as $jsfile) { 1730 $js .= file_get_contents($jsfile)."\n"; 1731 } 1732 return $js; 1733 } 1734 1735 /** 1736 * Post processes CSS. 1737 * 1738 * This method post processes all of the CSS before it is served for this theme. 1739 * This is done so that things such as image URL's can be swapped in and to 1740 * run any specific CSS post process method the theme has requested. 1741 * This allows themes to use CSS settings. 1742 * 1743 * @param string $css The CSS to process. 1744 * @return string The processed CSS. 1745 */ 1746 public function post_process($css) { 1747 // now resolve all image locations 1748 if (preg_match_all('/\[\[pix:([a-z0-9_]+\|)?([^\]]+)\]\]/', $css, $matches, PREG_SET_ORDER)) { 1749 $replaced = array(); 1750 foreach ($matches as $match) { 1751 if (isset($replaced[$match[0]])) { 1752 continue; 1753 } 1754 $replaced[$match[0]] = true; 1755 $imagename = $match[2]; 1756 $component = rtrim($match[1], '|'); 1757 $imageurl = $this->image_url($imagename, $component)->out(false); 1758 // we do not need full url because the image.php is always in the same dir 1759 $imageurl = preg_replace('|^http.?://[^/]+|', '', $imageurl); 1760 $css = str_replace($match[0], $imageurl, $css); 1761 } 1762 } 1763 1764 // Now resolve all font locations. 1765 if (preg_match_all('/\[\[font:([a-z0-9_]+\|)?([^\]]+)\]\]/', $css, $matches, PREG_SET_ORDER)) { 1766 $replaced = array(); 1767 foreach ($matches as $match) { 1768 if (isset($replaced[$match[0]])) { 1769 continue; 1770 } 1771 $replaced[$match[0]] = true; 1772 $fontname = $match[2]; 1773 $component = rtrim($match[1], '|'); 1774 $fonturl = $this->font_url($fontname, $component)->out(false); 1775 // We do not need full url because the font.php is always in the same dir. 1776 $fonturl = preg_replace('|^http.?://[^/]+|', '', $fonturl); 1777 $css = str_replace($match[0], $fonturl, $css); 1778 } 1779 } 1780 1781 // Now resolve all theme settings or do any other postprocessing. 1782 // This needs to be done before calling core parser, since the parser strips [[settings]] tags. 1783 $csspostprocess = $this->csspostprocess; 1784 if (function_exists($csspostprocess)) { 1785 $css = $csspostprocess($css, $this); 1786 } 1787 1788 // Post processing using an object representation of CSS. 1789 $treeprocessor = $this->get_css_tree_post_processor(); 1790 $needsparsing = !empty($treeprocessor) || !empty($this->rtlmode); 1791 if ($needsparsing) { 1792 1793 // We might need more memory/time to do this, so let's play safe. 1794 raise_memory_limit(MEMORY_EXTRA); 1795 core_php_time_limit::raise(300); 1796 1797 $parser = new core_cssparser($css); 1798 $csstree = $parser->parse(); 1799 unset($parser); 1800 1801 if ($this->rtlmode) { 1802 $this->rtlize($csstree); 1803 } 1804 1805 if ($treeprocessor) { 1806 $treeprocessor($csstree, $this); 1807 } 1808 1809 $css = $csstree->render(); 1810 unset($csstree); 1811 } 1812 1813 return $css; 1814 } 1815 1816 /** 1817 * Flip a stylesheet to RTL. 1818 * 1819 * @param Object $csstree The parsed CSS tree structure to flip. 1820 * @return void 1821 */ 1822 protected function rtlize($csstree) { 1823 $rtlcss = new core_rtlcss($csstree); 1824 $rtlcss->flip(); 1825 } 1826 1827 /** 1828 * Return the direct URL for an image from the pix folder. 1829 * 1830 * Use this function sparingly and never for icons. For icons use pix_icon or the pix helper in a mustache template. 1831 * 1832 * @deprecated since Moodle 3.3 1833 * @param string $imagename the name of the icon. 1834 * @param string $component specification of one plugin like in get_string() 1835 * @return moodle_url 1836 */ 1837 public function pix_url($imagename, $component) { 1838 debugging('pix_url is deprecated. Use image_url for images and pix_icon for icons.', DEBUG_DEVELOPER); 1839 return $this->image_url($imagename, $component); 1840 } 1841 1842 /** 1843 * Return the direct URL for an image from the pix folder. 1844 * 1845 * Use this function sparingly and never for icons. For icons use pix_icon or the pix helper in a mustache template. 1846 * 1847 * @param string $imagename the name of the icon. 1848 * @param string $component specification of one plugin like in get_string() 1849 * @return moodle_url 1850 */ 1851 public function image_url($imagename, $component) { 1852 global $CFG; 1853 1854 $params = array('theme'=>$this->name); 1855 $svg = $this->use_svg_icons(); 1856 1857 if (empty($component) or $component === 'moodle' or $component === 'core') { 1858 $params['component'] = 'core'; 1859 } else { 1860 $params['component'] = $component; 1861 } 1862 1863 $rev = theme_get_revision(); 1864 if ($rev != -1) { 1865 $params['rev'] = $rev; 1866 } 1867 1868 $params['image'] = $imagename; 1869 1870 $url = new moodle_url("/theme/image.php"); 1871 if (!empty($CFG->slasharguments) and $rev > 0) { 1872 $path = '/'.$params['theme'].'/'.$params['component'].'/'.$params['rev'].'/'.$params['image']; 1873 if (!$svg) { 1874 // We add a simple /_s to the start of the path. 1875 // The underscore is used to ensure that it isn't a valid theme name. 1876 $path = '/_s'.$path; 1877 } 1878 $url->set_slashargument($path, 'noparam', true); 1879 } else { 1880 if (!$svg) { 1881 // We add an SVG param so that we know not to serve SVG images. 1882 // We do this because all modern browsers support SVG and this param will one day be removed. 1883 $params['svg'] = '0'; 1884 } 1885 $url->params($params); 1886 } 1887 1888 return $url; 1889 } 1890 1891 /** 1892 * Return the URL for a font 1893 * 1894 * @param string $font the name of the font (including extension). 1895 * @param string $component specification of one plugin like in get_string() 1896 * @return moodle_url 1897 */ 1898 public function font_url($font, $component) { 1899 global $CFG; 1900 1901 $params = array('theme'=>$this->name); 1902 1903 if (empty($component) or $component === 'moodle' or $component === 'core') { 1904 $params['component'] = 'core'; 1905 } else { 1906 $params['component'] = $component; 1907 } 1908 1909 $rev = theme_get_revision(); 1910 if ($rev != -1) { 1911 $params['rev'] = $rev; 1912 } 1913 1914 $params['font'] = $font; 1915 1916 $url = new moodle_url("/theme/font.php"); 1917 if (!empty($CFG->slasharguments) and $rev > 0) { 1918 $path = '/'.$params['theme'].'/'.$params['component'].'/'.$params['rev'].'/'.$params['font']; 1919 $url->set_slashargument($path, 'noparam', true); 1920 } else { 1921 $url->params($params); 1922 } 1923 1924 return $url; 1925 } 1926 1927 /** 1928 * Returns URL to the stored file via pluginfile.php. 1929 * 1930 * Note the theme must also implement pluginfile.php handler, 1931 * theme revision is used instead of the itemid. 1932 * 1933 * @param string $setting 1934 * @param string $filearea 1935 * @return string protocol relative URL or null if not present 1936 */ 1937 public function setting_file_url($setting, $filearea) { 1938 global $CFG; 1939 1940 if (empty($this->settings->$setting)) { 1941 return null; 1942 } 1943 1944 $component = 'theme_'.$this->name; 1945 $itemid = theme_get_revision(); 1946 $filepath = $this->settings->$setting; 1947 $syscontext = context_system::instance(); 1948 1949 $url = moodle_url::make_file_url("$CFG->wwwroot/pluginfile.php", "/$syscontext->id/$component/$filearea/$itemid".$filepath); 1950 1951 // Now this is tricky because the we can not hardcode http or https here, lets use the relative link. 1952 // Note: unfortunately moodle_url does not support //urls yet. 1953 1954 $url = preg_replace('|^https?://|i', '//', $url->out(false)); 1955 1956 return $url; 1957 } 1958 1959 /** 1960 * Serve the theme setting file. 1961 * 1962 * @param string $filearea 1963 * @param array $args 1964 * @param bool $forcedownload 1965 * @param array $options 1966 * @return bool may terminate if file not found or donotdie not specified 1967 */ 1968 public function setting_file_serve($filearea, $args, $forcedownload, $options) { 1969 global $CFG; 1970 require_once("$CFG->libdir/filelib.php"); 1971 1972 $syscontext = context_system::instance(); 1973 $component = 'theme_'.$this->name; 1974 1975 $revision = array_shift($args); 1976 if ($revision < 0) { 1977 $lifetime = 0; 1978 } else { 1979 $lifetime = 60*60*24*60; 1980 // By default, theme files must be cache-able by both browsers and proxies. 1981 if (!array_key_exists('cacheability', $options)) { 1982 $options['cacheability'] = 'public'; 1983 } 1984 } 1985 1986 $fs = get_file_storage(); 1987 $relativepath = implode('/', $args); 1988 1989 $fullpath = "/{$syscontext->id}/{$component}/{$filearea}/0/{$relativepath}"; 1990 $fullpath = rtrim($fullpath, '/'); 1991 if ($file = $fs->get_file_by_hash(sha1($fullpath))) { 1992 send_stored_file($file, $lifetime, 0, $forcedownload, $options); 1993 return true; 1994 } else { 1995 send_file_not_found(); 1996 } 1997 } 1998 1999 /** 2000 * Resolves the real image location. 2001 * 2002 * $svg was introduced as an arg in 2.4. It is important because not all supported browsers support the use of SVG 2003 * and we need a way in which to turn it off. 2004 * By default SVG won't be used unless asked for. This is done for two reasons: 2005 * 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 2006 * browser supports SVG. 2007 * 2. We only serve SVG images from locations we trust. This must NOT include any areas where the image may have been uploaded 2008 * by the user due to security concerns. 2009 * 2010 * @param string $image name of image, may contain relative path 2011 * @param string $component 2012 * @param bool $svg|null Should SVG images also be looked for? If null, resorts to $CFG->svgicons if that is set; falls back to 2013 * auto-detection of browser support otherwise 2014 * @return string full file path 2015 */ 2016 public function resolve_image_location($image, $component, $svg = false) { 2017 global $CFG; 2018 2019 if (!is_bool($svg)) { 2020 // If $svg isn't a bool then we need to decide for ourselves. 2021 $svg = $this->use_svg_icons(); 2022 } 2023 2024 if ($component === 'moodle' or $component === 'core' or empty($component)) { 2025 if ($imagefile = $this->image_exists("$this->dir/pix_core/$image", $svg)) { 2026 return $imagefile; 2027 } 2028 foreach (array_reverse($this->parent_configs) as $parent_config) { // base first, the immediate parent last 2029 if ($imagefile = $this->image_exists("$parent_config->dir/pix_core/$image", $svg)) { 2030 return $imagefile; 2031 } 2032 } 2033 if ($imagefile = $this->image_exists("$CFG->dataroot/pix/$image", $svg)) { 2034 return $imagefile; 2035 } 2036 if ($imagefile = $this->image_exists("$CFG->dirroot/pix/$image", $svg)) { 2037 return $imagefile; 2038 } 2039 return null; 2040 2041 } else if ($component === 'theme') { //exception 2042 if ($image === 'favicon') { 2043 return "$this->dir/pix/favicon.ico"; 2044 } 2045 if ($imagefile = $this->image_exists("$this->dir/pix/$image", $svg)) { 2046 return $imagefile; 2047 } 2048 foreach (array_reverse($this->parent_configs) as $parent_config) { // base first, the immediate parent last 2049 if ($imagefile = $this->image_exists("$parent_config->dir/pix/$image", $svg)) { 2050 return $imagefile; 2051 } 2052 } 2053 return null; 2054 2055 } else { 2056 if (strpos($component, '_') === false) { 2057 $component = 'mod_'.$component; 2058 } 2059 list($type, $plugin) = explode('_', $component, 2); 2060 2061 if ($imagefile = $this->image_exists("$this->dir/pix_plugins/$type/$plugin/$image", $svg)) { 2062 return $imagefile; 2063 } 2064 foreach (array_reverse($this->parent_configs) as $parent_config) { // base first, the immediate parent last 2065 if ($imagefile = $this->image_exists("$parent_config->dir/pix_plugins/$type/$plugin/$image", $svg)) { 2066 return $imagefile; 2067 } 2068 } 2069 if ($imagefile = $this->image_exists("$CFG->dataroot/pix_plugins/$type/$plugin/$image", $svg)) { 2070 return $imagefile; 2071 } 2072 $dir = core_component::get_plugin_directory($type, $plugin); 2073 if ($imagefile = $this->image_exists("$dir/pix/$image", $svg)) { 2074 return $imagefile; 2075 } 2076 return null; 2077 } 2078 } 2079 2080 /** 2081 * Resolves the real font location. 2082 * 2083 * @param string $font name of font file 2084 * @param string $component 2085 * @return string full file path 2086 */ 2087 public function resolve_font_location($font, $component) { 2088 global $CFG; 2089 2090 if ($component === 'moodle' or $component === 'core' or empty($component)) { 2091 if (file_exists("$this->dir/fonts_core/$font")) { 2092 return "$this->dir/fonts_core/$font"; 2093 } 2094 foreach (array_reverse($this->parent_configs) as $parent_config) { // Base first, the immediate parent last. 2095 if (file_exists("$parent_config->dir/fonts_core/$font")) { 2096 return "$parent_config->dir/fonts_core/$font"; 2097 } 2098 } 2099 if (file_exists("$CFG->dataroot/fonts/$font")) { 2100 return "$CFG->dataroot/fonts/$font"; 2101 } 2102 if (file_exists("$CFG->dirroot/lib/fonts/$font")) { 2103 return "$CFG->dirroot/lib/fonts/$font"; 2104 } 2105 return null; 2106 2107 } else if ($component === 'theme') { // Exception. 2108 if (file_exists("$this->dir/fonts/$font")) { 2109 return "$this->dir/fonts/$font"; 2110 } 2111 foreach (array_reverse($this->parent_configs) as $parent_config) { // Base first, the immediate parent last. 2112 if (file_exists("$parent_config->dir/fonts/$font")) { 2113 return "$parent_config->dir/fonts/$font"; 2114 } 2115 } 2116 return null; 2117 2118 } else { 2119 if (strpos($component, '_') === false) { 2120 $component = 'mod_'.$component; 2121 } 2122 list($type, $plugin) = explode('_', $component, 2); 2123 2124 if (file_exists("$this->dir/fonts_plugins/$type/$plugin/$font")) { 2125 return "$this->dir/fonts_plugins/$type/$plugin/$font"; 2126 } 2127 foreach (array_reverse($this->parent_configs) as $parent_config) { // Base first, the immediate parent last. 2128 if (file_exists("$parent_config->dir/fonts_plugins/$type/$plugin/$font")) { 2129 return "$parent_config->dir/fonts_plugins/$type/$plugin/$font"; 2130 } 2131 } 2132 if (file_exists("$CFG->dataroot/fonts_plugins/$type/$plugin/$font")) { 2133 return "$CFG->dataroot/fonts_plugins/$type/$plugin/$font"; 2134 } 2135 $dir = core_component::get_plugin_directory($type, $plugin); 2136 if (file_exists("$dir/fonts/$font")) { 2137 return "$dir/fonts/$font"; 2138 } 2139 return null; 2140 } 2141 } 2142 2143 /** 2144 * Return true if we should look for SVG images as well. 2145 * 2146 * @return bool 2147 */ 2148 public function use_svg_icons() { 2149 global $CFG; 2150 if ($this->usesvg === null) { 2151 2152 if (!isset($CFG->svgicons)) { 2153 $this->usesvg = core_useragent::supports_svg(); 2154 } else { 2155 // Force them on/off depending upon the setting. 2156 $this->usesvg = (bool)$CFG->svgicons; 2157 } 2158 } 2159 return $this->usesvg; 2160 } 2161 2162 /** 2163 * Forces the usesvg setting to either true or false, avoiding any decision making. 2164 * 2165 * This function should only ever be used when absolutely required, and before any generation of image URL's has occurred. 2166 * DO NOT ABUSE THIS FUNCTION... not that you'd want to right ;) 2167 * 2168 * @param bool $setting True to force the use of svg when available, null otherwise. 2169 */ 2170 public function force_svg_use($setting) { 2171 $this->usesvg = (bool)$setting; 2172 } 2173 2174 /** 2175 * Set to be in RTL mode. 2176 * 2177 * This will likely be used when post processing the CSS before serving it. 2178 * 2179 * @param bool $inrtl True when in RTL mode. 2180 */ 2181 public function set_rtl_mode($inrtl = true) { 2182 $this->rtlmode = $inrtl; 2183 } 2184 2185 /** 2186 * Whether the theme is being served in RTL mode. 2187 * 2188 * @return bool True when in RTL mode. 2189 */ 2190 public function get_rtl_mode() { 2191 return $this->rtlmode; 2192 } 2193 2194 /** 2195 * Checks if file with any image extension exists. 2196 * 2197 * The order to these images was adjusted prior to the release of 2.4 2198 * At that point the were the following image counts in Moodle core: 2199 * 2200 * - png = 667 in pix dirs (1499 total) 2201 * - gif = 385 in pix dirs (606 total) 2202 * - jpg = 62 in pix dirs (74 total) 2203 * - jpeg = 0 in pix dirs (1 total) 2204 * 2205 * There is work in progress to move towards SVG presently hence that has been prioritiesed. 2206 * 2207 * @param string $filepath 2208 * @param bool $svg If set to true SVG images will also be looked for. 2209 * @return string image name with extension 2210 */ 2211 private static function image_exists($filepath, $svg = false) { 2212 if ($svg && file_exists("$filepath.svg")) { 2213 return "$filepath.svg"; 2214 } else if (file_exists("$filepath.png")) { 2215 return "$filepath.png"; 2216 } else if (file_exists("$filepath.gif")) { 2217 return "$filepath.gif"; 2218 } else if (file_exists("$filepath.jpg")) { 2219 return "$filepath.jpg"; 2220 } else if (file_exists("$filepath.jpeg")) { 2221 return "$filepath.jpeg"; 2222 } else { 2223 return false; 2224 } 2225 } 2226 2227 /** 2228 * Loads the theme config from config.php file. 2229 * 2230 * @param string $themename 2231 * @param stdClass $settings from config_plugins table 2232 * @param boolean $parentscheck true to also check the parents. . 2233 * @return stdClass The theme configuration 2234 */ 2235 private static function find_theme_config($themename, $settings, $parentscheck = true) { 2236 // We have to use the variable name $THEME (upper case) because that 2237 // is what is used in theme config.php files. 2238 2239 if (!$dir = theme_config::find_theme_location($themename)) { 2240 return null; 2241 } 2242 2243 $THEME = new stdClass(); 2244 $THEME->name = $themename; 2245 $THEME->dir = $dir; 2246 $THEME->settings = $settings; 2247 2248 global $CFG; // just in case somebody tries to use $CFG in theme config 2249 include("$THEME->dir/config.php"); 2250 2251 // verify the theme configuration is OK 2252 if (!is_array($THEME->parents)) { 2253 // parents option is mandatory now 2254 return null; 2255 } else { 2256 // We use $parentscheck to only check the direct parents (avoid infinite loop). 2257 if ($parentscheck) { 2258 // Find all parent theme configs. 2259 foreach ($THEME->parents as $parent) { 2260 $parentconfig = theme_config::find_theme_config($parent, $settings, false); 2261 if (empty($parentconfig)) { 2262 return null; 2263 } 2264 } 2265 } 2266 } 2267 2268 return $THEME; 2269 } 2270 2271 /** 2272 * Finds the theme location and verifies the theme has all needed files 2273 * and is not obsoleted. 2274 * 2275 * @param string $themename 2276 * @return string full dir path or null if not found 2277 */ 2278 private static function find_theme_location($themename) { 2279 global $CFG; 2280 2281 if (file_exists("$CFG->dirroot/theme/$themename/config.php")) { 2282 $dir = "$CFG->dirroot/theme/$themename"; 2283 2284 } else if (!empty($CFG->themedir) and file_exists("$CFG->themedir/$themename/config.php")) { 2285 $dir = "$CFG->themedir/$themename"; 2286 2287 } else { 2288 return null; 2289 } 2290 2291 if (file_exists("$dir/styles.php")) { 2292 //legacy theme - needs to be upgraded - upgrade info is displayed on the admin settings page 2293 return null; 2294 } 2295 2296 return $dir; 2297 } 2298 2299 /** 2300 * Get the renderer for a part of Moodle for this theme. 2301 * 2302 * @param moodle_page $page the page we are rendering 2303 * @param string $component the name of part of moodle. E.g. 'core', 'quiz', 'qtype_multichoice'. 2304 * @param string $subtype optional subtype such as 'news' resulting to 'mod_forum_news' 2305 * @param string $target one of rendering target constants 2306 * @return renderer_base the requested renderer. 2307 */ 2308 public function get_renderer(moodle_page $page, $component, $subtype = null, $target = null) { 2309 if (is_null($this->rf)) { 2310 $classname = $this->rendererfactory; 2311 $this->rf = new $classname($this); 2312 } 2313 2314 return $this->rf->get_renderer($page, $component, $subtype, $target); 2315 } 2316 2317 /** 2318 * Get the information from {@link $layouts} for this type of page. 2319 * 2320 * @param string $pagelayout the the page layout name. 2321 * @return array the appropriate part of {@link $layouts}. 2322 */ 2323 protected function layout_info_for_page($pagelayout) { 2324 if (array_key_exists($pagelayout, $this->layouts)) { 2325 return $this->layouts[$pagelayout]; 2326 } else { 2327 debugging('Invalid page layout specified: ' . $pagelayout); 2328 return $this->layouts['standard']; 2329 } 2330 } 2331 2332 /** 2333 * Given the settings of this theme, and the page pagelayout, return the 2334 * full path of the page layout file to use. 2335 * 2336 * Used by {@link core_renderer::header()}. 2337 * 2338 * @param string $pagelayout the the page layout name. 2339 * @return string Full path to the lyout file to use 2340 */ 2341 public function layout_file($pagelayout) { 2342 global $CFG; 2343 2344 $layoutinfo = $this->layout_info_for_page($pagelayout); 2345 $layoutfile = $layoutinfo['file']; 2346 2347 if (array_key_exists('theme', $layoutinfo)) { 2348 $themes = array($layoutinfo['theme']); 2349 } else { 2350 $themes = array_merge(array($this->name),$this->parents); 2351 } 2352 2353 foreach ($themes as $theme) { 2354 if ($dir = $this->find_theme_location($theme)) { 2355 $path = "$dir/layout/$layoutfile"; 2356 2357 // Check the template exists, return general base theme template if not. 2358 if (is_readable($path)) { 2359 return $path; 2360 } 2361 } 2362 } 2363 2364 debugging('Can not find layout file for: ' . $pagelayout); 2365 // fallback to standard normal layout 2366 return "$CFG->dirroot/theme/base/layout/general.php"; 2367 } 2368 2369 /** 2370 * Returns auxiliary page layout options specified in layout configuration array. 2371 * 2372 * @param string $pagelayout 2373 * @return array 2374 */ 2375 public function pagelayout_options($pagelayout) { 2376 $info = $this->layout_info_for_page($pagelayout); 2377 if (!empty($info['options'])) { 2378 return $info['options']; 2379 } 2380 return array(); 2381 } 2382 2383 /** 2384 * Inform a block_manager about the block regions this theme wants on this 2385 * page layout. 2386 * 2387 * @param string $pagelayout the general type of the page. 2388 * @param block_manager $blockmanager the block_manger to set up. 2389 */ 2390 public function setup_blocks($pagelayout, $blockmanager) { 2391 $layoutinfo = $this->layout_info_for_page($pagelayout); 2392 if (!empty($layoutinfo['regions'])) { 2393 $blockmanager->add_regions($layoutinfo['regions'], false); 2394 $blockmanager->set_default_region($layoutinfo['defaultregion']); 2395 } 2396 } 2397 2398 /** 2399 * Gets the visible name for the requested block region. 2400 * 2401 * @param string $region The region name to get 2402 * @param string $theme The theme the region belongs to (may come from the parent theme) 2403 * @return string 2404 */ 2405 protected function get_region_name($region, $theme) { 2406 $regionstring = get_string('region-' . $region, 'theme_' . $theme); 2407 // A name exists in this theme, so use it 2408 if (substr($regionstring, 0, 1) != '[') { 2409 return $regionstring; 2410 } 2411 2412 // Otherwise, try to find one elsewhere 2413 // Check parents, if any 2414 foreach ($this->parents as $parentthemename) { 2415 $regionstring = get_string('region-' . $region, 'theme_' . $parentthemename); 2416 if (substr($regionstring, 0, 1) != '[') { 2417 return $regionstring; 2418 } 2419 } 2420 2421 // Last resort, try the boost theme for names 2422 return get_string('region-' . $region, 'theme_boost'); 2423 } 2424 2425 /** 2426 * Get the list of all block regions known to this theme in all templates. 2427 * 2428 * @return array internal region name => human readable name. 2429 */ 2430 public function get_all_block_regions() { 2431 $regions = array(); 2432 foreach ($this->layouts as $layoutinfo) { 2433 foreach ($layoutinfo['regions'] as $region) { 2434 $regions[$region] = $this->get_region_name($region, $this->name); 2435 } 2436 } 2437 return $regions; 2438 } 2439 2440 /** 2441 * Returns the human readable name of the theme 2442 * 2443 * @return string 2444 */ 2445 public function get_theme_name() { 2446 return get_string('pluginname', 'theme_'.$this->name); 2447 } 2448 2449 /** 2450 * Returns the block render method. 2451 * 2452 * It is set by the theme via: 2453 * $THEME->blockrendermethod = '...'; 2454 * 2455 * It can be one of two values, blocks or blocks_for_region. 2456 * It should be set to the method being used by the theme layouts. 2457 * 2458 * @return string 2459 */ 2460 public function get_block_render_method() { 2461 if ($this->blockrendermethod) { 2462 // Return the specified block render method. 2463 return $this->blockrendermethod; 2464 } 2465 // Its not explicitly set, check the parent theme configs. 2466 foreach ($this->parent_configs as $config) { 2467 if (isset($config->blockrendermethod)) { 2468 return $config->blockrendermethod; 2469 } 2470 } 2471 // Default it to blocks. 2472 return 'blocks'; 2473 } 2474 2475 /** 2476 * Get the callable for CSS tree post processing. 2477 * 2478 * @return string|null 2479 */ 2480 public function get_css_tree_post_processor() { 2481 $configs = [$this] + $this->parent_configs; 2482 foreach ($configs as $config) { 2483 if (!empty($config->csstreepostprocessor) && is_callable($config->csstreepostprocessor)) { 2484 return $config->csstreepostprocessor; 2485 } 2486 } 2487 return null; 2488 } 2489 2490 } 2491 2492 /** 2493 * This class keeps track of which HTML tags are currently open. 2494 * 2495 * This makes it much easier to always generate well formed XHTML output, even 2496 * if execution terminates abruptly. Any time you output some opening HTML 2497 * without the matching closing HTML, you should push the necessary close tags 2498 * onto the stack. 2499 * 2500 * @copyright 2009 Tim Hunt 2501 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 2502 * @since Moodle 2.0 2503 * @package core 2504 * @category output 2505 */ 2506 class xhtml_container_stack { 2507 2508 /** 2509 * @var array Stores the list of open containers. 2510 */ 2511 protected $opencontainers = array(); 2512 2513 /** 2514 * @var array In developer debug mode, stores a stack trace of all opens and 2515 * closes, so we can output helpful error messages when there is a mismatch. 2516 */ 2517 protected $log = array(); 2518 2519 /** 2520 * @var boolean Store whether we are developer debug mode. We need this in 2521 * several places including in the destructor where we may not have access to $CFG. 2522 */ 2523 protected $isdebugging; 2524 2525 /** 2526 * Constructor 2527 */ 2528 public function __construct() { 2529 global $CFG; 2530 $this->isdebugging = $CFG->debugdeveloper; 2531 } 2532 2533 /** 2534 * Push the close HTML for a recently opened container onto the stack. 2535 * 2536 * @param string $type The type of container. This is checked when {@link pop()} 2537 * is called and must match, otherwise a developer debug warning is output. 2538 * @param string $closehtml The HTML required to close the container. 2539 */ 2540 public function push($type, $closehtml) { 2541 $container = new stdClass; 2542 $container->type = $type; 2543 $container->closehtml = $closehtml; 2544 if ($this->isdebugging) { 2545 $this->log('Open', $type); 2546 } 2547 array_push($this->opencontainers, $container); 2548 } 2549 2550 /** 2551 * Pop the HTML for the next closing container from the stack. The $type 2552 * must match the type passed when the container was opened, otherwise a 2553 * warning will be output. 2554 * 2555 * @param string $type The type of container. 2556 * @return string the HTML required to close the container. 2557 */ 2558 public function pop($type) { 2559 if (empty($this->opencontainers)) { 2560 debugging('<p>There are no more open containers. This suggests there is a nesting problem.</p>' . 2561 $this->output_log(), DEBUG_DEVELOPER); 2562 return; 2563 } 2564 2565 $container = array_pop($this->opencontainers); 2566 if ($container->type != $type) { 2567 debugging('<p>The type of container to be closed (' . $container->type . 2568 ') does not match the type of the next open container (' . $type . 2569 '). This suggests there is a nesting problem.</p>' . 2570 $this->output_log(), DEBUG_DEVELOPER); 2571 } 2572 if ($this->isdebugging) { 2573 $this->log('Close', $type); 2574 } 2575 return $container->closehtml; 2576 } 2577 2578 /** 2579 * Close all but the last open container. This is useful in places like error 2580 * handling, where you want to close all the open containers (apart from <body>) 2581 * before outputting the error message. 2582 * 2583 * @param bool $shouldbenone assert that the stack should be empty now - causes a 2584 * developer debug warning if it isn't. 2585 * @return string the HTML required to close any open containers inside <body>. 2586 */ 2587 public function pop_all_but_last($shouldbenone = false) { 2588 if ($shouldbenone && count($this->opencontainers) != 1) { 2589 debugging('<p>Some HTML tags were opened in the body of the page but not closed.</p>' . 2590 $this->output_log(), DEBUG_DEVELOPER); 2591 } 2592 $output = ''; 2593 while (count($this->opencontainers) > 1) { 2594 $container = array_pop($this->opencontainers); 2595 $output .= $container->closehtml; 2596 } 2597 return $output; 2598 } 2599 2600 /** 2601 * You can call this function if you want to throw away an instance of this 2602 * class without properly emptying the stack (for example, in a unit test). 2603 * Calling this method stops the destruct method from outputting a developer 2604 * debug warning. After calling this method, the instance can no longer be used. 2605 */ 2606 public function discard() { 2607 $this->opencontainers = null; 2608 } 2609 2610 /** 2611 * Adds an entry to the log. 2612 * 2613 * @param string $action The name of the action 2614 * @param string $type The type of action 2615 */ 2616 protected function log($action, $type) { 2617 $this->log[] = '<li>' . $action . ' ' . $type . ' at:' . 2618 format_backtrace(debug_backtrace()) . '</li>'; 2619 } 2620 2621 /** 2622 * Outputs the log's contents as a HTML list. 2623 * 2624 * @return string HTML list of the log 2625 */ 2626 protected function output_log() { 2627 return '<ul>' . implode("\n", $this->log) . '</ul>'; 2628 } 2629 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body