[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/lib/ -> outputlib.php (source)

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


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1