Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.
/lib/ -> outputlib.php (source)

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 &lt; &gt; &raquo; - 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 &lt; &gt; &raquo; - 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 &gt; &raquo;), 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 = '&#x25BA;';
 825              $this->larrow = '&#x25C4;';
 826              $this->uarrow = '&#x25B2;';
 827              $this->darrow = '&#x25BC;';
 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 = '&#x25B6;&#xFE0E;';
 838                  $this->larrow = '&#x25C0;&#xFE0E;';
 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 = '&rarr;';
 845                  $this->larrow = '&larr;';
 846                  $this->uarrow = '&uarr;';
 847                  $this->darrow = '&darr;';
 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 = '&gt;';
 854                  $this->larrow = '&lt;';
 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 &amp; 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  }