Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.
   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   * Interface and classes for creating appropriate renderers for various parts of Moodle.
  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  /** General rendering target, usually normal browser page */
  32  define('RENDERER_TARGET_GENERAL', 'general');
  33  
  34  /** General rendering target, usually normal browser page, but with limited capacity to avoid API use */
  35  define('RENDERER_TARGET_MAINTENANCE', 'maintenance');
  36  
  37  /** Plain text rendering for CLI scripts and cron */
  38  define('RENDERER_TARGET_CLI', 'cli');
  39  
  40  /** Plain text rendering for Ajax scripts*/
  41  define('RENDERER_TARGET_AJAX', 'ajax');
  42  
  43  /** Plain text rendering intended for sending via email */
  44  define('RENDERER_TARGET_TEXTEMAIL', 'textemail');
  45  
  46  /** Rich text html rendering intended for sending via email */
  47  define('RENDERER_TARGET_HTMLEMAIL', 'htmlemail');
  48  
  49  // note: maybe we could define portfolio export target too
  50  
  51  
  52  /**
  53   * A renderer factory is just responsible for creating an appropriate renderer
  54   * for any given part of Moodle.
  55   *
  56   * Which renderer factory to use is chose by the current theme, and an instance
  57   * if created automatically when the theme is set up.
  58   *
  59   * A renderer factory must also have a constructor that takes a theme_config object.
  60   * (See {@link renderer_factory_base::__construct} for an example.)
  61   *
  62   * @copyright 2009 Tim Hunt
  63   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  64   * @since Moodle 2.0
  65   * @package core
  66   * @category output
  67   */
  68  interface renderer_factory {
  69  
  70      /**
  71       * Return the renderer for a particular part of Moodle.
  72       *
  73       * The renderer interfaces are defined by classes called {plugin}_renderer
  74       * where {plugin} is the name of the component. The renderers for core Moodle are
  75       * defined in lib/renderer.php. For plugins, they will be defined in a file
  76       * called renderer.php inside the plugin.
  77       *
  78       * Renderers will normally want to subclass the renderer_base class.
  79       * (However, if you really know what you are doing, you don't have to do that.)
  80       *
  81       * There is no separate interface definition for renderers. The default
  82       * {plugin}_renderer implementation also serves to define the API for
  83       * other implementations of the interface, whether or not they subclass it.
  84       *
  85       * A particular plugin can define multiple renderers if it wishes, using the
  86       * $subtype parameter. For example workshop_renderer,
  87       * workshop_allocation_manual_renderer etc.
  88       *
  89       * @param moodle_page $page the page the renderer is outputting content for.
  90       * @param string $component name such as 'core', 'mod_forum' or 'qtype_multichoice'.
  91       * @param string $subtype optional subtype such as 'news' resulting to 'mod_forum_news'
  92       * @param string $target one of rendering target constants
  93       * @return renderer_base an object implementing the requested renderer interface.
  94       */
  95      public function get_renderer(moodle_page $page, $component, $subtype=null, $target=null);
  96  }
  97  
  98  
  99  /**
 100   * This is a base class to help you implement the renderer_factory interface.
 101   *
 102   * It keeps a cache of renderers that have been constructed, so you only need
 103   * to construct each one once in you subclass.
 104   *
 105   * It also has a method to get the name of, and include the renderer.php with
 106   * the definition of, the standard renderer class for a given module.
 107   *
 108   * @copyright 2009 Tim Hunt
 109   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 110   * @since Moodle 2.0
 111   * @package core
 112   * @category output
 113   */
 114  abstract class renderer_factory_base implements renderer_factory {
 115      /**
 116       * @var theme_config The theme we belong to.
 117       */
 118      protected $theme;
 119  
 120      /**
 121       * Constructor.
 122       *
 123       * @param theme_config $theme the theme we belong to.
 124       */
 125      public function __construct(theme_config $theme) {
 126          $this->theme = $theme;
 127      }
 128  
 129      /**
 130       * Returns suffix of renderer class expected for given target.
 131       *
 132       * @param string $target one of the renderer target constants, target is guessed if null used
 133       * @return array two element array, first element is target, second the target suffix string
 134       */
 135      protected function get_target_suffix($target) {
 136          if (empty($target) || $target === RENDERER_TARGET_MAINTENANCE) {
 137              // If the target hasn't been specified we need to guess the defaults.
 138              // We also override the target with the default if the maintenance target has been provided.
 139              // This ensures we don't use the maintenance renderer if we are processing a special target.
 140              if (defined('PREFERRED_RENDERER_TARGET')) {
 141                  $target = PREFERRED_RENDERER_TARGET;
 142              } else if (CLI_SCRIPT) {
 143                  $target = RENDERER_TARGET_CLI;
 144              } else if (AJAX_SCRIPT) {
 145                  $target = RENDERER_TARGET_AJAX;
 146              }
 147          }
 148  
 149          switch ($target) {
 150              case RENDERER_TARGET_CLI: $suffix = '_cli'; break;
 151              case RENDERER_TARGET_AJAX: $suffix = '_ajax'; break;
 152              case RENDERER_TARGET_TEXTEMAIL: $suffix = '_textemail'; break;
 153              case RENDERER_TARGET_HTMLEMAIL: $suffix = '_htmlemail'; break;
 154              case RENDERER_TARGET_MAINTENANCE: $suffix = '_maintenance'; break;
 155              default: $target = RENDERER_TARGET_GENERAL; $suffix = '';
 156          }
 157  
 158          return array($target, $suffix);
 159      }
 160  
 161      /**
 162       * For a given module name, return the possible class names
 163       * that defines the renderer interface for that module.
 164       *
 165       * Newer auto-loaded class names are returned as well as the old style _renderable classnames.
 166       *
 167       * Also, if it exists, include the renderer.php file for that module, so
 168       * the class definition of the default renderer has been loaded.
 169       *
 170       * @param string $component name such as 'core', 'mod_forum' or 'qtype_multichoice'.
 171       * @param string $subtype optional subtype such as 'news' resulting to:
 172       *              '\mod_forum\output\news_renderer'
 173       *              or '\mod_forum\output\news\renderer'
 174       *              or non-autoloaded 'mod_forum_news'
 175       * @return array[] Each element of the array is an array with keys:
 176       *                 classname - The class name to search
 177       *                 autoloaded - Does this classname assume autoloading?
 178       *                 validwithprefix - Is this class name valid when a prefix is added to it?
 179       *                 validwithoutprefix - Is this class name valid when no prefix is added to it?
 180       * @throws coding_exception
 181       */
 182      protected function standard_renderer_classnames($component, $subtype = null) {
 183          global $CFG; // Needed in included files.
 184          $classnames = array();
 185  
 186          // Standardize component name ala frankenstyle.
 187          list($plugin, $type) = core_component::normalize_component($component);
 188          if ($type === null) {
 189              $component = $plugin;
 190          } else {
 191              $component = $plugin.'_'.$type;
 192          }
 193  
 194          if ($component !== 'core') {
 195              // Renderers are stored in renderer.php files.
 196              if (!$compdirectory = core_component::get_component_directory($component)) {
 197                  throw new coding_exception('Invalid component specified in renderer request', $component);
 198              }
 199              $rendererfile = $compdirectory . '/renderer.php';
 200              if (file_exists($rendererfile)) {
 201                  include_once($rendererfile);
 202              }
 203  
 204          } else if (!empty($subtype)) {
 205              $coresubsystems = core_component::get_core_subsystems();
 206              if (!array_key_exists($subtype, $coresubsystems)) { // There may be nulls.
 207                  throw new coding_exception('Invalid core subtype "' . $subtype . '" in renderer request', $subtype);
 208              }
 209              if ($coresubsystems[$subtype]) {
 210                  $rendererfile = $coresubsystems[$subtype] . '/renderer.php';
 211                  if (file_exists($rendererfile)) {
 212                      include_once($rendererfile);
 213                  }
 214              }
 215          }
 216  
 217          if (empty($subtype)) {
 218              // Theme specific auto-loaded name (only valid when prefixed with the theme name).
 219              $classnames[] = array(
 220                  'validwithprefix' => true,
 221                  'validwithoutprefix' => false,
 222                  'autoloaded' => true,
 223                  'classname' => '\\output\\' . $component . '_renderer'
 224              );
 225  
 226              // Standard autoloaded plugin name (not valid with a prefix).
 227              $classnames[] = array(
 228                  'validwithprefix' => false,
 229                  'validwithoutprefix' => true,
 230                  'autoloaded' => true,
 231                  'classname' => '\\' . $component . '\\output\\renderer'
 232              );
 233              // Legacy class name - (valid with or without a prefix).
 234              $classnames[] = array(
 235                  'validwithprefix' => true,
 236                  'validwithoutprefix' => true,
 237                  'autoloaded' => false,
 238                  'classname' => $component . '_renderer'
 239              );
 240          } else {
 241              // Theme specific auto-loaded name (only valid when prefixed with the theme name).
 242              $classnames[] = array(
 243                  'validwithprefix' => true,
 244                  'validwithoutprefix' => false,
 245                  'autoloaded' => true,
 246                  'classname' => '\\output\\' . $component . '\\' . $subtype . '_renderer'
 247              );
 248              // Version of the above with subtype being a namespace level on it's own.
 249              $classnames[] = array(
 250                  'validwithprefix' => true,
 251                  'validwithoutprefix' => false,
 252                  'autoloaded' => true,
 253                  'classname' => '\\output\\' . $component . '\\' . $subtype . '\\renderer'
 254              );
 255              // Standard autoloaded plugin name (not valid with a prefix).
 256              $classnames[] = array(
 257                  'validwithprefix' => false,
 258                  'validwithoutprefix' => true,
 259                  'autoloaded' => true,
 260                  'classname' => '\\' . $component . '\\output\\' . $subtype . '_renderer'
 261              );
 262              // Version of the above with subtype being a namespace level on it's own.
 263              $classnames[] = array(
 264                  'validwithprefix' => false,
 265                  'validwithoutprefix' => true,
 266                  'autoloaded' => true,
 267                  'classname' => '\\' . $component . '\\output\\' . $subtype . '\\renderer'
 268              );
 269              // Legacy class name - (valid with or without a prefix).
 270              $classnames[] = array(
 271                  'validwithprefix' => true,
 272                  'validwithoutprefix' => true,
 273                  'autoloaded' => false,
 274                  'classname' => $component . '_' . $subtype . '_renderer'
 275              );
 276          }
 277          return $classnames;
 278      }
 279  }
 280  
 281  /**
 282   * This is the default renderer factory for Moodle.
 283   *
 284   * It simply returns an instance of the appropriate standard renderer class.
 285   *
 286   * @copyright 2009 Tim Hunt
 287   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 288   * @since Moodle 2.0
 289   * @package core
 290   * @category output
 291   */
 292  class standard_renderer_factory extends renderer_factory_base {
 293  
 294      /**
 295       * Implement the subclass method
 296       *
 297       * @param moodle_page $page the page the renderer is outputting content for.
 298       * @param string $component name such as 'core', 'mod_forum' or 'qtype_multichoice'.
 299       * @param string $subtype optional subtype such as 'news' resulting to 'mod_forum_news'
 300       * @param string $target one of rendering target constants
 301       * @return renderer_base an object implementing the requested renderer interface.
 302       */
 303      public function get_renderer(moodle_page $page, $component, $subtype = null, $target = null) {
 304          $classnames = $this->standard_renderer_classnames($component, $subtype);
 305          $classname = '';
 306  
 307          list($target, $suffix) = $this->get_target_suffix($target);
 308          // First look for a version with a suffix.
 309          foreach ($classnames as $classnamedetails) {
 310              if ($classnamedetails['validwithoutprefix']) {
 311                  $newclassname = $classnamedetails['classname'] . $suffix;
 312                  if (class_exists($newclassname)) {
 313                      $classname = $newclassname;
 314                      break;
 315                  } else {
 316                      $newclassname = $classnamedetails['classname'];
 317                      if (class_exists($newclassname)) {
 318                          $classname = $newclassname;
 319                          break;
 320                      }
 321                  }
 322              }
 323          }
 324          // Now look for a non-suffixed version.
 325          if (empty($classname)) {
 326              foreach ($classnames as $classnamedetails) {
 327                  if ($classnamedetails['validwithoutprefix']) {
 328                      $newclassname = $classnamedetails['classname'];
 329                      if (class_exists($newclassname)) {
 330                          $classname = $newclassname;
 331                          break;
 332                      }
 333                  }
 334              }
 335          }
 336  
 337          if (empty($classname)) {
 338              // Standard renderer must always exist.
 339              throw new coding_exception('Request for an unknown renderer class. Searched for: ' . var_export($classnames, true));
 340          }
 341  
 342          return new $classname($page, $target);
 343      }
 344  }
 345  
 346  
 347  /**
 348   * This is renderer factory allows themes to override the standard renderers using php code.
 349   *
 350   * It will load any code from theme/mytheme/renderers.php and
 351   * theme/parenttheme/renderers.php, if then exist. Then whenever you ask for
 352   * a renderer for 'component', it will create a mytheme_component_renderer or a
 353   * parenttheme_component_renderer, instead of a component_renderer,
 354   * if either of those classes exist.
 355   *
 356   * @copyright 2009 Tim Hunt
 357   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 358   * @since Moodle 2.0
 359   * @package core
 360   * @category output
 361   */
 362  class theme_overridden_renderer_factory extends renderer_factory_base {
 363  
 364      /**
 365       * @var array An array of renderer prefixes
 366       */
 367      protected $prefixes = array();
 368  
 369      /**
 370       * Constructor.
 371       * @param theme_config $theme the theme we are rendering for.
 372       */
 373      public function __construct(theme_config $theme) {
 374          parent::__construct($theme);
 375          // Initialise $this->prefixes.
 376          $this->prefixes = $theme->renderer_prefixes();
 377      }
 378  
 379      /**
 380       * Implement the subclass method
 381       *
 382       * @param moodle_page $page the page the renderer is outputting content for.
 383       * @param string $component name such as 'core', 'mod_forum' or 'qtype_multichoice'.
 384       * @param string $subtype optional subtype such as 'news' resulting to 'mod_forum_news'
 385       * @param string $target one of rendering target constants
 386       * @return renderer_base an object implementing the requested renderer interface.
 387       */
 388      public function get_renderer(moodle_page $page, $component, $subtype = null, $target = null) {
 389          $classnames = $this->standard_renderer_classnames($component, $subtype);
 390  
 391          list($target, $suffix) = $this->get_target_suffix($target);
 392  
 393          // Theme lib.php and renderers.php files are loaded automatically
 394          // when loading the theme configs.
 395  
 396          // First try the renderers with correct suffix.
 397          foreach ($this->prefixes as $prefix) {
 398              foreach ($classnames as $classnamedetails) {
 399                  if ($classnamedetails['validwithprefix']) {
 400                      if ($classnamedetails['autoloaded']) {
 401                          $newclassname = $prefix . $classnamedetails['classname'] . $suffix;
 402                      } else {
 403                          $newclassname = $prefix . '_' . $classnamedetails['classname'] . $suffix;
 404                      }
 405                      if (class_exists($newclassname)) {
 406                          return new $newclassname($page, $target);
 407                      }
 408                  }
 409              }
 410          }
 411          foreach ($classnames as $classnamedetails) {
 412              if ($classnamedetails['validwithoutprefix']) {
 413                  $newclassname = $classnamedetails['classname'] . $suffix;
 414                  if (class_exists($newclassname)) {
 415                      // Use the specialised renderer for given target, default renderer might also decide
 416                      // to implement support for more targets.
 417                      return new $newclassname($page, $target);
 418                  }
 419              }
 420          }
 421  
 422          // Then try general renderer.
 423          foreach ($this->prefixes as $prefix) {
 424              foreach ($classnames as $classnamedetails) {
 425                  if ($classnamedetails['validwithprefix']) {
 426                      if ($classnamedetails['autoloaded']) {
 427                          $newclassname = $prefix . $classnamedetails['classname'];
 428                      } else {
 429                          $newclassname = $prefix . '_' . $classnamedetails['classname'];
 430                      }
 431                      if (class_exists($newclassname)) {
 432                          return new $newclassname($page, $target);
 433                      }
 434                  }
 435              }
 436          }
 437  
 438          // Final attempt - no prefix or suffix.
 439          foreach ($classnames as $classnamedetails) {
 440              if ($classnamedetails['validwithoutprefix']) {
 441                  $newclassname = $classnamedetails['classname'];
 442                  if (class_exists($newclassname)) {
 443                      return new $newclassname($page, $target);
 444                  }
 445              }
 446          }
 447          throw new coding_exception('Request for an unknown renderer ' . $component . ', ' . $subtype . ', ' . $target);
 448      }
 449  }