Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403] [Versions 39 and 310]

   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   * Classes for rendering HTML output for Moodle.
  19   *
  20   * Please see {@link http://docs.moodle.org/en/Developement:How_Moodle_outputs_HTML}
  21   * for an overview.
  22   *
  23   * Included in this file are the primary renderer classes:
  24   *     - renderer_base:         The renderer outline class that all renderers
  25   *                              should inherit from.
  26   *     - core_renderer:         The standard HTML renderer.
  27   *     - core_renderer_cli:     An adaption of the standard renderer for CLI scripts.
  28   *     - core_renderer_ajax:    An adaption of the standard renderer for AJAX scripts.
  29   *     - plugin_renderer_base:  A renderer class that should be extended by all
  30   *                              plugin renderers.
  31   *
  32   * @package core
  33   * @category output
  34   * @copyright  2009 Tim Hunt
  35   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  36   */
  37  
  38  defined('MOODLE_INTERNAL') || die();
  39  
  40  /**
  41   * Simple base class for Moodle renderers.
  42   *
  43   * Tracks the xhtml_container_stack to use, which is passed in in the constructor.
  44   *
  45   * Also has methods to facilitate generating HTML output.
  46   *
  47   * @copyright 2009 Tim Hunt
  48   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  49   * @since Moodle 2.0
  50   * @package core
  51   * @category output
  52   */
  53  class renderer_base {
  54      /**
  55       * @var xhtml_container_stack The xhtml_container_stack to use.
  56       */
  57      protected $opencontainers;
  58  
  59      /**
  60       * @var moodle_page The Moodle page the renderer has been created to assist with.
  61       */
  62      protected $page;
  63  
  64      /**
  65       * @var string The requested rendering target.
  66       */
  67      protected $target;
  68  
  69      /**
  70       * @var Mustache_Engine $mustache The mustache template compiler
  71       */
  72      private $mustache;
  73  
  74      /**
  75       * Return an instance of the mustache class.
  76       *
  77       * @since 2.9
  78       * @return Mustache_Engine
  79       */
  80      protected function get_mustache() {
  81          global $CFG;
  82  
  83          if ($this->mustache === null) {
  84              require_once("{$CFG->libdir}/filelib.php");
  85  
  86              $themename = $this->page->theme->name;
  87              $themerev = theme_get_revision();
  88  
  89              // Create new localcache directory.
  90              $cachedir = make_localcache_directory("mustache/$themerev/$themename");
  91  
  92              // Remove old localcache directories.
  93              $mustachecachedirs = glob("{$CFG->localcachedir}/mustache/*", GLOB_ONLYDIR);
  94              foreach ($mustachecachedirs as $localcachedir) {
  95                  $cachedrev = [];
  96                  preg_match("/\/mustache\/([0-9]+)$/", $localcachedir, $cachedrev);
  97                  $cachedrev = isset($cachedrev[1]) ? intval($cachedrev[1]) : 0;
  98                  if ($cachedrev > 0 && $cachedrev < $themerev) {
  99                      fulldelete($localcachedir);
 100                  }
 101              }
 102  
 103              $loader = new \core\output\mustache_filesystem_loader();
 104              $stringhelper = new \core\output\mustache_string_helper();
 105              $quotehelper = new \core\output\mustache_quote_helper();
 106              $jshelper = new \core\output\mustache_javascript_helper($this->page);
 107              $pixhelper = new \core\output\mustache_pix_helper($this);
 108              $shortentexthelper = new \core\output\mustache_shorten_text_helper();
 109              $userdatehelper = new \core\output\mustache_user_date_helper();
 110  
 111              // We only expose the variables that are exposed to JS templates.
 112              $safeconfig = $this->page->requires->get_config_for_javascript($this->page, $this);
 113  
 114              $helpers = array('config' => $safeconfig,
 115                               'str' => array($stringhelper, 'str'),
 116                               'quote' => array($quotehelper, 'quote'),
 117                               'js' => array($jshelper, 'help'),
 118                               'pix' => array($pixhelper, 'pix'),
 119                               'shortentext' => array($shortentexthelper, 'shorten'),
 120                               'userdate' => array($userdatehelper, 'transform'),
 121                           );
 122  
 123              $this->mustache = new \core\output\mustache_engine(array(
 124                  'cache' => $cachedir,
 125                  'escape' => 's',
 126                  'loader' => $loader,
 127                  'helpers' => $helpers,
 128                  'pragmas' => [Mustache_Engine::PRAGMA_BLOCKS],
 129                  // Don't allow the JavaScript helper to be executed from within another
 130                  // helper. If it's allowed it can be used by users to inject malicious
 131                  // JS into the page.
 132                  'disallowednestedhelpers' => ['js']));
 133  
 134          }
 135  
 136          return $this->mustache;
 137      }
 138  
 139  
 140      /**
 141       * Constructor
 142       *
 143       * The constructor takes two arguments. The first is the page that the renderer
 144       * has been created to assist with, and the second is the target.
 145       * The target is an additional identifier that can be used to load different
 146       * renderers for different options.
 147       *
 148       * @param moodle_page $page the page we are doing output for.
 149       * @param string $target one of rendering target constants
 150       */
 151      public function __construct(moodle_page $page, $target) {
 152          $this->opencontainers = $page->opencontainers;
 153          $this->page = $page;
 154          $this->target = $target;
 155      }
 156  
 157      /**
 158       * Renders a template by name with the given context.
 159       *
 160       * The provided data needs to be array/stdClass made up of only simple types.
 161       * Simple types are array,stdClass,bool,int,float,string
 162       *
 163       * @since 2.9
 164       * @param array|stdClass $context Context containing data for the template.
 165       * @return string|boolean
 166       */
 167      public function render_from_template($templatename, $context) {
 168          static $templatecache = array();
 169          $mustache = $this->get_mustache();
 170  
 171          try {
 172              // Grab a copy of the existing helper to be restored later.
 173              $uniqidhelper = $mustache->getHelper('uniqid');
 174          } catch (Mustache_Exception_UnknownHelperException $e) {
 175              // Helper doesn't exist.
 176              $uniqidhelper = null;
 177          }
 178  
 179          // Provide 1 random value that will not change within a template
 180          // but will be different from template to template. This is useful for
 181          // e.g. aria attributes that only work with id attributes and must be
 182          // unique in a page.
 183          $mustache->addHelper('uniqid', new \core\output\mustache_uniqid_helper());
 184          if (isset($templatecache[$templatename])) {
 185              $template = $templatecache[$templatename];
 186          } else {
 187              try {
 188                  $template = $mustache->loadTemplate($templatename);
 189                  $templatecache[$templatename] = $template;
 190              } catch (Mustache_Exception_UnknownTemplateException $e) {
 191                  throw new moodle_exception('Unknown template: ' . $templatename);
 192              }
 193          }
 194  
 195          $renderedtemplate = trim($template->render($context));
 196  
 197          // If we had an existing uniqid helper then we need to restore it to allow
 198          // handle nested calls of render_from_template.
 199          if ($uniqidhelper) {
 200              $mustache->addHelper('uniqid', $uniqidhelper);
 201          }
 202  
 203          return $renderedtemplate;
 204      }
 205  
 206  
 207      /**
 208       * Returns rendered widget.
 209       *
 210       * The provided widget needs to be an object that extends the renderable
 211       * interface.
 212       * If will then be rendered by a method based upon the classname for the widget.
 213       * For instance a widget of class `crazywidget` will be rendered by a protected
 214       * render_crazywidget method of this renderer.
 215       * If no render_crazywidget method exists and crazywidget implements templatable,
 216       * look for the 'crazywidget' template in the same component and render that.
 217       *
 218       * @param renderable $widget instance with renderable interface
 219       * @return string
 220       */
 221      public function render(renderable $widget) {
 222          $classparts = explode('\\', get_class($widget));
 223          // Strip namespaces.
 224          $classname = array_pop($classparts);
 225          // Remove _renderable suffixes
 226          $classname = preg_replace('/_renderable$/', '', $classname);
 227  
 228          $rendermethod = 'render_'.$classname;
 229          if (method_exists($this, $rendermethod)) {
 230              return $this->$rendermethod($widget);
 231          }
 232          if ($widget instanceof templatable) {
 233              $component = array_shift($classparts);
 234              if (!$component) {
 235                  $component = 'core';
 236              }
 237              $template = $component . '/' . $classname;
 238              $context = $widget->export_for_template($this);
 239              return $this->render_from_template($template, $context);
 240          }
 241          throw new coding_exception('Can not render widget, renderer method ('.$rendermethod.') not found.');
 242      }
 243  
 244      /**
 245       * Adds a JS action for the element with the provided id.
 246       *
 247       * This method adds a JS event for the provided component action to the page
 248       * and then returns the id that the event has been attached to.
 249       * If no id has been provided then a new ID is generated by {@link html_writer::random_id()}
 250       *
 251       * @param component_action $action
 252       * @param string $id
 253       * @return string id of element, either original submitted or random new if not supplied
 254       */
 255      public function add_action_handler(component_action $action, $id = null) {
 256          if (!$id) {
 257              $id = html_writer::random_id($action->event);
 258          }
 259          $this->page->requires->event_handler("#$id", $action->event, $action->jsfunction, $action->jsfunctionargs);
 260          return $id;
 261      }
 262  
 263      /**
 264       * Returns true is output has already started, and false if not.
 265       *
 266       * @return boolean true if the header has been printed.
 267       */
 268      public function has_started() {
 269          return $this->page->state >= moodle_page::STATE_IN_BODY;
 270      }
 271  
 272      /**
 273       * Given an array or space-separated list of classes, prepares and returns the HTML class attribute value
 274       *
 275       * @param mixed $classes Space-separated string or array of classes
 276       * @return string HTML class attribute value
 277       */
 278      public static function prepare_classes($classes) {
 279          if (is_array($classes)) {
 280              return implode(' ', array_unique($classes));
 281          }
 282          return $classes;
 283      }
 284  
 285      /**
 286       * Return the direct URL for an image from the pix folder.
 287       *
 288       * Use this function sparingly and never for icons. For icons use pix_icon or the pix helper in a mustache template.
 289       *
 290       * @deprecated since Moodle 3.3
 291       * @param string $imagename the name of the icon.
 292       * @param string $component specification of one plugin like in get_string()
 293       * @return moodle_url
 294       */
 295      public function pix_url($imagename, $component = 'moodle') {
 296          debugging('pix_url is deprecated. Use image_url for images and pix_icon for icons.', DEBUG_DEVELOPER);
 297          return $this->page->theme->image_url($imagename, $component);
 298      }
 299  
 300      /**
 301       * Return the moodle_url for an image.
 302       *
 303       * The exact image location and extension is determined
 304       * automatically by searching for gif|png|jpg|jpeg, please
 305       * note there can not be diferent images with the different
 306       * extension. The imagename is for historical reasons
 307       * a relative path name, it may be changed later for core
 308       * images. It is recommended to not use subdirectories
 309       * in plugin and theme pix directories.
 310       *
 311       * There are three types of images:
 312       * 1/ theme images  - stored in theme/mytheme/pix/,
 313       *                    use component 'theme'
 314       * 2/ core images   - stored in /pix/,
 315       *                    overridden via theme/mytheme/pix_core/
 316       * 3/ plugin images - stored in mod/mymodule/pix,
 317       *                    overridden via theme/mytheme/pix_plugins/mod/mymodule/,
 318       *                    example: image_url('comment', 'mod_glossary')
 319       *
 320       * @param string $imagename the pathname of the image
 321       * @param string $component full plugin name (aka component) or 'theme'
 322       * @return moodle_url
 323       */
 324      public function image_url($imagename, $component = 'moodle') {
 325          return $this->page->theme->image_url($imagename, $component);
 326      }
 327  
 328      /**
 329       * Return the site's logo URL, if any.
 330       *
 331       * @param int $maxwidth The maximum width, or null when the maximum width does not matter.
 332       * @param int $maxheight The maximum height, or null when the maximum height does not matter.
 333       * @return moodle_url|false
 334       */
 335      public function get_logo_url($maxwidth = null, $maxheight = 200) {
 336          global $CFG;
 337          $logo = get_config('core_admin', 'logo');
 338          if (empty($logo)) {
 339              return false;
 340          }
 341  
 342          // 200px high is the default image size which should be displayed at 100px in the page to account for retina displays.
 343          // It's not worth the overhead of detecting and serving 2 different images based on the device.
 344  
 345          // Hide the requested size in the file path.
 346          $filepath = ((int) $maxwidth . 'x' . (int) $maxheight) . '/';
 347  
 348          // Use $CFG->themerev to prevent browser caching when the file changes.
 349          return moodle_url::make_pluginfile_url(context_system::instance()->id, 'core_admin', 'logo', $filepath,
 350              theme_get_revision(), $logo);
 351      }
 352  
 353      /**
 354       * Return the site's compact logo URL, if any.
 355       *
 356       * @param int $maxwidth The maximum width, or null when the maximum width does not matter.
 357       * @param int $maxheight The maximum height, or null when the maximum height does not matter.
 358       * @return moodle_url|false
 359       */
 360      public function get_compact_logo_url($maxwidth = 300, $maxheight = 300) {
 361          global $CFG;
 362          $logo = get_config('core_admin', 'logocompact');
 363          if (empty($logo)) {
 364              return false;
 365          }
 366  
 367          // Hide the requested size in the file path.
 368          $filepath = ((int) $maxwidth . 'x' . (int) $maxheight) . '/';
 369  
 370          // Use $CFG->themerev to prevent browser caching when the file changes.
 371          return moodle_url::make_pluginfile_url(context_system::instance()->id, 'core_admin', 'logocompact', $filepath,
 372              theme_get_revision(), $logo);
 373      }
 374  
 375      /**
 376       * Whether we should display the logo in the navbar.
 377       *
 378       * We will when there are no main logos, and we have compact logo.
 379       *
 380       * @return bool
 381       */
 382      public function should_display_navbar_logo() {
 383          $logo = $this->get_compact_logo_url();
 384          return !empty($logo) && !$this->should_display_main_logo();
 385      }
 386  
 387      /**
 388       * Whether we should display the main logo.
 389       *
 390       * @param int $headinglevel The heading level we want to check against.
 391       * @return bool
 392       */
 393      public function should_display_main_logo($headinglevel = 1) {
 394  
 395          // Only render the logo if we're on the front page or login page and the we have a logo.
 396          $logo = $this->get_logo_url();
 397          if ($headinglevel == 1 && !empty($logo)) {
 398              if ($this->page->pagelayout == 'frontpage' || $this->page->pagelayout == 'login') {
 399                  return true;
 400              }
 401          }
 402  
 403          return false;
 404      }
 405  
 406  }
 407  
 408  
 409  /**
 410   * Basis for all plugin renderers.
 411   *
 412   * @copyright Petr Skoda (skodak)
 413   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 414   * @since Moodle 2.0
 415   * @package core
 416   * @category output
 417   */
 418  class plugin_renderer_base extends renderer_base {
 419  
 420      /**
 421       * @var renderer_base|core_renderer A reference to the current renderer.
 422       * The renderer provided here will be determined by the page but will in 90%
 423       * of cases by the {@link core_renderer}
 424       */
 425      protected $output;
 426  
 427      /**
 428       * Constructor method, calls the parent constructor
 429       *
 430       * @param moodle_page $page
 431       * @param string $target one of rendering target constants
 432       */
 433      public function __construct(moodle_page $page, $target) {
 434          if (empty($target) && $page->pagelayout === 'maintenance') {
 435              // If the page is using the maintenance layout then we're going to force the target to maintenance.
 436              // This way we'll get a special maintenance renderer that is designed to block access to API's that are likely
 437              // unavailable for this page layout.
 438              $target = RENDERER_TARGET_MAINTENANCE;
 439          }
 440          $this->output = $page->get_renderer('core', null, $target);
 441          parent::__construct($page, $target);
 442      }
 443  
 444      /**
 445       * Renders the provided widget and returns the HTML to display it.
 446       *
 447       * @param renderable $widget instance with renderable interface
 448       * @return string
 449       */
 450      public function render(renderable $widget) {
 451          $classname = get_class($widget);
 452          // Strip namespaces.
 453          $classname = preg_replace('/^.*\\\/', '', $classname);
 454          // Keep a copy at this point, we may need to look for a deprecated method.
 455          $deprecatedmethod = 'render_'.$classname;
 456          // Remove _renderable suffixes
 457          $classname = preg_replace('/_renderable$/', '', $classname);
 458  
 459          $rendermethod = 'render_'.$classname;
 460          if (method_exists($this, $rendermethod)) {
 461              return $this->$rendermethod($widget);
 462          }
 463          if ($rendermethod !== $deprecatedmethod && method_exists($this, $deprecatedmethod)) {
 464              // This is exactly where we don't want to be.
 465              // If you have arrived here you have a renderable component within your plugin that has the name
 466              // blah_renderable, and you have a render method render_blah_renderable on your plugin.
 467              // In 2.8 we revamped output, as part of this change we changed slightly how renderables got rendered
 468              // and the _renderable suffix now gets removed when looking for a render method.
 469              // You need to change your renderers render_blah_renderable to render_blah.
 470              // Until you do this it will not be possible for a theme to override the renderer to override your method.
 471              // Please do it ASAP.
 472              static $debugged = array();
 473              if (!isset($debugged[$deprecatedmethod])) {
 474                  debugging(sprintf('Deprecated call. Please rename your renderables render method from %s to %s.',
 475                      $deprecatedmethod, $rendermethod), DEBUG_DEVELOPER);
 476                  $debugged[$deprecatedmethod] = true;
 477              }
 478              return $this->$deprecatedmethod($widget);
 479          }
 480          // pass to core renderer if method not found here
 481          return $this->output->render($widget);
 482      }
 483  
 484      /**
 485       * Magic method used to pass calls otherwise meant for the standard renderer
 486       * to it to ensure we don't go causing unnecessary grief.
 487       *
 488       * @param string $method
 489       * @param array $arguments
 490       * @return mixed
 491       */
 492      public function __call($method, $arguments) {
 493          if (method_exists('renderer_base', $method)) {
 494              throw new coding_exception('Protected method called against '.get_class($this).' :: '.$method);
 495          }
 496          if (method_exists($this->output, $method)) {
 497              return call_user_func_array(array($this->output, $method), $arguments);
 498          } else {
 499              throw new coding_exception('Unknown method called against '.get_class($this).' :: '.$method);
 500          }
 501      }
 502  }
 503  
 504  
 505  /**
 506   * The standard implementation of the core_renderer interface.
 507   *
 508   * @copyright 2009 Tim Hunt
 509   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 510   * @since Moodle 2.0
 511   * @package core
 512   * @category output
 513   */
 514  class core_renderer extends renderer_base {
 515      /**
 516       * Do NOT use, please use <?php echo $OUTPUT->main_content() ?>
 517       * in layout files instead.
 518       * @deprecated
 519       * @var string used in {@link core_renderer::header()}.
 520       */
 521      const MAIN_CONTENT_TOKEN = '[MAIN CONTENT GOES HERE]';
 522  
 523      /**
 524       * @var string Used to pass information from {@link core_renderer::doctype()} to
 525       * {@link core_renderer::standard_head_html()}.
 526       */
 527      protected $contenttype;
 528  
 529      /**
 530       * @var string Used by {@link core_renderer::redirect_message()} method to communicate
 531       * with {@link core_renderer::header()}.
 532       */
 533      protected $metarefreshtag = '';
 534  
 535      /**
 536       * @var string Unique token for the closing HTML
 537       */
 538      protected $unique_end_html_token;
 539  
 540      /**
 541       * @var string Unique token for performance information
 542       */
 543      protected $unique_performance_info_token;
 544  
 545      /**
 546       * @var string Unique token for the main content.
 547       */
 548      protected $unique_main_content_token;
 549  
 550      /** @var custom_menu_item language The language menu if created */
 551      protected $language = null;
 552  
 553      /**
 554       * Constructor
 555       *
 556       * @param moodle_page $page the page we are doing output for.
 557       * @param string $target one of rendering target constants
 558       */
 559      public function __construct(moodle_page $page, $target) {
 560          $this->opencontainers = $page->opencontainers;
 561          $this->page = $page;
 562          $this->target = $target;
 563  
 564          $this->unique_end_html_token = '%%ENDHTML-'.sesskey().'%%';
 565          $this->unique_performance_info_token = '%%PERFORMANCEINFO-'.sesskey().'%%';
 566          $this->unique_main_content_token = '[MAIN CONTENT GOES HERE - '.sesskey().']';
 567      }
 568  
 569      /**
 570       * Get the DOCTYPE declaration that should be used with this page. Designed to
 571       * be called in theme layout.php files.
 572       *
 573       * @return string the DOCTYPE declaration that should be used.
 574       */
 575      public function doctype() {
 576          if ($this->page->theme->doctype === 'html5') {
 577              $this->contenttype = 'text/html; charset=utf-8';
 578              return "<!DOCTYPE html>\n";
 579  
 580          } else if ($this->page->theme->doctype === 'xhtml5') {
 581              $this->contenttype = 'application/xhtml+xml; charset=utf-8';
 582              return "<!DOCTYPE html>\n";
 583  
 584          } else {
 585              // legacy xhtml 1.0
 586              $this->contenttype = 'text/html; charset=utf-8';
 587              return ('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">' . "\n");
 588          }
 589      }
 590  
 591      /**
 592       * The attributes that should be added to the <html> tag. Designed to
 593       * be called in theme layout.php files.
 594       *
 595       * @return string HTML fragment.
 596       */
 597      public function htmlattributes() {
 598          $return = get_html_lang(true);
 599          $attributes = array();
 600          if ($this->page->theme->doctype !== 'html5') {
 601              $attributes['xmlns'] = 'http://www.w3.org/1999/xhtml';
 602          }
 603  
 604          // Give plugins an opportunity to add things like xml namespaces to the html element.
 605          // This function should return an array of html attribute names => values.
 606          $pluginswithfunction = get_plugins_with_function('add_htmlattributes', 'lib.php');
 607          foreach ($pluginswithfunction as $plugins) {
 608              foreach ($plugins as $function) {
 609                  $newattrs = $function();
 610                  unset($newattrs['dir']);
 611                  unset($newattrs['lang']);
 612                  unset($newattrs['xmlns']);
 613                  unset($newattrs['xml:lang']);
 614                  $attributes += $newattrs;
 615              }
 616          }
 617  
 618          foreach ($attributes as $key => $val) {
 619              $val = s($val);
 620              $return .= " $key=\"$val\"";
 621          }
 622  
 623          return $return;
 624      }
 625  
 626      /**
 627       * The standard tags (meta tags, links to stylesheets and JavaScript, etc.)
 628       * that should be included in the <head> tag. Designed to be called in theme
 629       * layout.php files.
 630       *
 631       * @return string HTML fragment.
 632       */
 633      public function standard_head_html() {
 634          global $CFG, $SESSION, $SITE;
 635  
 636          // Before we output any content, we need to ensure that certain
 637          // page components are set up.
 638  
 639          // Blocks must be set up early as they may require javascript which
 640          // has to be included in the page header before output is created.
 641          foreach ($this->page->blocks->get_regions() as $region) {
 642              $this->page->blocks->ensure_content_created($region, $this);
 643          }
 644  
 645          $output = '';
 646  
 647          // Give plugins an opportunity to add any head elements. The callback
 648          // must always return a string containing valid html head content.
 649          $pluginswithfunction = get_plugins_with_function('before_standard_html_head', 'lib.php');
 650          foreach ($pluginswithfunction as $plugins) {
 651              foreach ($plugins as $function) {
 652                  $output .= $function();
 653              }
 654          }
 655  
 656          // Allow a url_rewrite plugin to setup any dynamic head content.
 657          if (isset($CFG->urlrewriteclass) && !isset($CFG->upgraderunning)) {
 658              $class = $CFG->urlrewriteclass;
 659              $output .= $class::html_head_setup();
 660          }
 661  
 662          $output .= '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />' . "\n";
 663          $output .= '<meta name="keywords" content="moodle, ' . $this->page->title . '" />' . "\n";
 664          // This is only set by the {@link redirect()} method
 665          $output .= $this->metarefreshtag;
 666  
 667          // Check if a periodic refresh delay has been set and make sure we arn't
 668          // already meta refreshing
 669          if ($this->metarefreshtag=='' && $this->page->periodicrefreshdelay!==null) {
 670              $output .= '<meta http-equiv="refresh" content="'.$this->page->periodicrefreshdelay.';url='.$this->page->url->out().'" />';
 671          }
 672  
 673          // Set up help link popups for all links with the helptooltip class
 674          $this->page->requires->js_init_call('M.util.help_popups.setup');
 675  
 676          $focus = $this->page->focuscontrol;
 677          if (!empty($focus)) {
 678              if (preg_match("#forms\['([a-zA-Z0-9]+)'\].elements\['([a-zA-Z0-9]+)'\]#", $focus, $matches)) {
 679                  // This is a horrifically bad way to handle focus but it is passed in
 680                  // through messy formslib::moodleform
 681                  $this->page->requires->js_function_call('old_onload_focus', array($matches[1], $matches[2]));
 682              } else if (strpos($focus, '.')!==false) {
 683                  // Old style of focus, bad way to do it
 684                  debugging('This code is using the old style focus event, Please update this code to focus on an element id or the moodleform focus method.', DEBUG_DEVELOPER);
 685                  $this->page->requires->js_function_call('old_onload_focus', explode('.', $focus, 2));
 686              } else {
 687                  // Focus element with given id
 688                  $this->page->requires->js_function_call('focuscontrol', array($focus));
 689              }
 690          }
 691  
 692          // Get the theme stylesheet - this has to be always first CSS, this loads also styles.css from all plugins;
 693          // any other custom CSS can not be overridden via themes and is highly discouraged
 694          $urls = $this->page->theme->css_urls($this->page);
 695          foreach ($urls as $url) {
 696              $this->page->requires->css_theme($url);
 697          }
 698  
 699          // Get the theme javascript head and footer
 700          if ($jsurl = $this->page->theme->javascript_url(true)) {
 701              $this->page->requires->js($jsurl, true);
 702          }
 703          if ($jsurl = $this->page->theme->javascript_url(false)) {
 704              $this->page->requires->js($jsurl);
 705          }
 706  
 707          // Get any HTML from the page_requirements_manager.
 708          $output .= $this->page->requires->get_head_code($this->page, $this);
 709  
 710          // List alternate versions.
 711          foreach ($this->page->alternateversions as $type => $alt) {
 712              $output .= html_writer::empty_tag('link', array('rel' => 'alternate',
 713                      'type' => $type, 'title' => $alt->title, 'href' => $alt->url));
 714          }
 715  
 716          // Add noindex tag if relevant page and setting applied.
 717          $allowindexing = isset($CFG->allowindexing) ? $CFG->allowindexing : 0;
 718          $loginpages = array('login-index', 'login-signup');
 719          if ($allowindexing == 2 || ($allowindexing == 0 && in_array($this->page->pagetype, $loginpages))) {
 720              if (!isset($CFG->additionalhtmlhead)) {
 721                  $CFG->additionalhtmlhead = '';
 722              }
 723              $CFG->additionalhtmlhead .= '<meta name="robots" content="noindex" />';
 724          }
 725  
 726          if (!empty($CFG->additionalhtmlhead)) {
 727              $output .= "\n".$CFG->additionalhtmlhead;
 728          }
 729  
 730          if ($this->page->pagelayout == 'frontpage') {
 731              $summary = s(strip_tags(format_text($SITE->summary, FORMAT_HTML)));
 732              if (!empty($summary)) {
 733                  $output .= "<meta name=\"description\" content=\"$summary\" />\n";
 734              }
 735          }
 736  
 737          return $output;
 738      }
 739  
 740      /**
 741       * The standard tags (typically skip links) that should be output just inside
 742       * the start of the <body> tag. Designed to be called in theme layout.php files.
 743       *
 744       * @return string HTML fragment.
 745       */
 746      public function standard_top_of_body_html() {
 747          global $CFG;
 748          $output = $this->page->requires->get_top_of_body_code($this);
 749          if ($this->page->pagelayout !== 'embedded' && !empty($CFG->additionalhtmltopofbody)) {
 750              $output .= "\n".$CFG->additionalhtmltopofbody;
 751          }
 752  
 753          // Give subsystems an opportunity to inject extra html content. The callback
 754          // must always return a string containing valid html.
 755          foreach (\core_component::get_core_subsystems() as $name => $path) {
 756              if ($path) {
 757                  $output .= component_callback($name, 'before_standard_top_of_body_html', [], '');
 758              }
 759          }
 760  
 761          // Give plugins an opportunity to inject extra html content. The callback
 762          // must always return a string containing valid html.
 763          $pluginswithfunction = get_plugins_with_function('before_standard_top_of_body_html', 'lib.php');
 764          foreach ($pluginswithfunction as $plugins) {
 765              foreach ($plugins as $function) {
 766                  $output .= $function();
 767              }
 768          }
 769  
 770          $output .= $this->maintenance_warning();
 771  
 772          return $output;
 773      }
 774  
 775      /**
 776       * Scheduled maintenance warning message.
 777       *
 778       * Note: This is a nasty hack to display maintenance notice, this should be moved
 779       *       to some general notification area once we have it.
 780       *
 781       * @return string
 782       */
 783      public function maintenance_warning() {
 784          global $CFG;
 785  
 786          $output = '';
 787          if (isset($CFG->maintenance_later) and $CFG->maintenance_later > time()) {
 788              $timeleft = $CFG->maintenance_later - time();
 789              // If timeleft less than 30 sec, set the class on block to error to highlight.
 790              $errorclass = ($timeleft < 30) ? 'alert-error alert-danger' : 'alert-warning';
 791              $output .= $this->box_start($errorclass . ' moodle-has-zindex maintenancewarning m-3 alert');
 792              $a = new stdClass();
 793              $a->hour = (int)($timeleft / 3600);
 794              $a->min = (int)(($timeleft / 60) % 60);
 795              $a->sec = (int)($timeleft % 60);
 796              if ($a->hour > 0) {
 797                  $output .= get_string('maintenancemodeisscheduledlong', 'admin', $a);
 798              } else {
 799                  $output .= get_string('maintenancemodeisscheduled', 'admin', $a);
 800              }
 801  
 802              $output .= $this->box_end();
 803              $this->page->requires->yui_module('moodle-core-maintenancemodetimer', 'M.core.maintenancemodetimer',
 804                      array(array('timeleftinsec' => $timeleft)));
 805              $this->page->requires->strings_for_js(
 806                      array('maintenancemodeisscheduled', 'maintenancemodeisscheduledlong', 'sitemaintenance'),
 807                      'admin');
 808          }
 809          return $output;
 810      }
 811  
 812      /**
 813       * The standard tags (typically performance information and validation links,
 814       * if we are in developer debug mode) that should be output in the footer area
 815       * of the page. Designed to be called in theme layout.php files.
 816       *
 817       * @return string HTML fragment.
 818       */
 819      public function standard_footer_html() {
 820          global $CFG, $SCRIPT;
 821  
 822          $output = '';
 823          if (during_initial_install()) {
 824              // Debugging info can not work before install is finished,
 825              // in any case we do not want any links during installation!
 826              return $output;
 827          }
 828  
 829          // Give plugins an opportunity to add any footer elements.
 830          // The callback must always return a string containing valid html footer content.
 831          $pluginswithfunction = get_plugins_with_function('standard_footer_html', 'lib.php');
 832          foreach ($pluginswithfunction as $plugins) {
 833              foreach ($plugins as $function) {
 834                  $output .= $function();
 835              }
 836          }
 837  
 838          if (core_userfeedback::can_give_feedback()) {
 839              $output .= html_writer::div(
 840                  $this->render_from_template('core/userfeedback_footer_link', ['url' => core_userfeedback::make_link()->out(false)])
 841              );
 842          }
 843  
 844          // This function is normally called from a layout.php file in {@link core_renderer::header()}
 845          // but some of the content won't be known until later, so we return a placeholder
 846          // for now. This will be replaced with the real content in {@link core_renderer::footer()}.
 847          $output .= $this->unique_performance_info_token;
 848          if ($this->page->devicetypeinuse == 'legacy') {
 849              // The legacy theme is in use print the notification
 850              $output .= html_writer::tag('div', get_string('legacythemeinuse'), array('class'=>'legacythemeinuse'));
 851          }
 852  
 853          // Get links to switch device types (only shown for users not on a default device)
 854          $output .= $this->theme_switch_links();
 855  
 856          if (!empty($CFG->debugpageinfo)) {
 857              $output .= '<div class="performanceinfo pageinfo">' . get_string('pageinfodebugsummary', 'core_admin',
 858                  $this->page->debug_summary()) . '</div>';
 859          }
 860          if (debugging(null, DEBUG_DEVELOPER) and has_capability('moodle/site:config', context_system::instance())) {  // Only in developer mode
 861              // Add link to profiling report if necessary
 862              if (function_exists('profiling_is_running') && profiling_is_running()) {
 863                  $txt = get_string('profiledscript', 'admin');
 864                  $title = get_string('profiledscriptview', 'admin');
 865                  $url = $CFG->wwwroot . '/admin/tool/profiling/index.php?script=' . urlencode($SCRIPT);
 866                  $link= '<a title="' . $title . '" href="' . $url . '">' . $txt . '</a>';
 867                  $output .= '<div class="profilingfooter">' . $link . '</div>';
 868              }
 869              $purgeurl = new moodle_url('/admin/purgecaches.php', array('confirm' => 1,
 870                  'sesskey' => sesskey(), 'returnurl' => $this->page->url->out_as_local_url(false)));
 871              $output .= '<div class="purgecaches">' .
 872                      html_writer::link($purgeurl, get_string('purgecaches', 'admin')) . '</div>';
 873          }
 874          if (!empty($CFG->debugvalidators)) {
 875              $siteurl = qualified_me();
 876              $nuurl = new moodle_url('https://validator.w3.org/nu/', ['doc' => $siteurl, 'showsource' => 'yes']);
 877              $waveurl = new moodle_url('https://wave.webaim.org/report#/' . urlencode($siteurl));
 878              $validatorlinks = [
 879                  html_writer::link($nuurl, get_string('validatehtml')),
 880                  html_writer::link($waveurl, get_string('wcagcheck'))
 881              ];
 882              $validatorlinkslist = html_writer::alist($validatorlinks, ['class' => 'list-unstyled ml-1']);
 883              $output .= html_writer::div($validatorlinkslist, 'validators');
 884          }
 885          return $output;
 886      }
 887  
 888      /**
 889       * Returns standard main content placeholder.
 890       * Designed to be called in theme layout.php files.
 891       *
 892       * @return string HTML fragment.
 893       */
 894      public function main_content() {
 895          // This is here because it is the only place we can inject the "main" role over the entire main content area
 896          // without requiring all theme's to manually do it, and without creating yet another thing people need to
 897          // remember in the theme.
 898          // This is an unfortunate hack. DO NO EVER add anything more here.
 899          // DO NOT add classes.
 900          // DO NOT add an id.
 901          return '<div role="main">'.$this->unique_main_content_token.'</div>';
 902      }
 903  
 904      /**
 905       * Returns standard navigation between activities in a course.
 906       *
 907       * @return string the navigation HTML.
 908       */
 909      public function activity_navigation() {
 910          // First we should check if we want to add navigation.
 911          $context = $this->page->context;
 912          if (($this->page->pagelayout !== 'incourse' && $this->page->pagelayout !== 'frametop')
 913              || $context->contextlevel != CONTEXT_MODULE) {
 914              return '';
 915          }
 916  
 917          // If the activity is in stealth mode, show no links.
 918          if ($this->page->cm->is_stealth()) {
 919              return '';
 920          }
 921  
 922          // Get a list of all the activities in the course.
 923          $course = $this->page->cm->get_course();
 924          $modules = get_fast_modinfo($course->id)->get_cms();
 925  
 926          // Put the modules into an array in order by the position they are shown in the course.
 927          $mods = [];
 928          $activitylist = [];
 929          foreach ($modules as $module) {
 930              // Only add activities the user can access, aren't in stealth mode and have a url (eg. mod_label does not).
 931              if (!$module->uservisible || $module->is_stealth() || empty($module->url)) {
 932                  continue;
 933              }
 934              $mods[$module->id] = $module;
 935  
 936              // No need to add the current module to the list for the activity dropdown menu.
 937              if ($module->id == $this->page->cm->id) {
 938                  continue;
 939              }
 940              // Module name.
 941              $modname = $module->get_formatted_name();
 942              // Display the hidden text if necessary.
 943              if (!$module->visible) {
 944                  $modname .= ' ' . get_string('hiddenwithbrackets');
 945              }
 946              // Module URL.
 947              $linkurl = new moodle_url($module->url, array('forceview' => 1));
 948              // Add module URL (as key) and name (as value) to the activity list array.
 949              $activitylist[$linkurl->out(false)] = $modname;
 950          }
 951  
 952          $nummods = count($mods);
 953  
 954          // If there is only one mod then do nothing.
 955          if ($nummods == 1) {
 956              return '';
 957          }
 958  
 959          // Get an array of just the course module ids used to get the cmid value based on their position in the course.
 960          $modids = array_keys($mods);
 961  
 962          // Get the position in the array of the course module we are viewing.
 963          $position = array_search($this->page->cm->id, $modids);
 964  
 965          $prevmod = null;
 966          $nextmod = null;
 967  
 968          // Check if we have a previous mod to show.
 969          if ($position > 0) {
 970              $prevmod = $mods[$modids[$position - 1]];
 971          }
 972  
 973          // Check if we have a next mod to show.
 974          if ($position < ($nummods - 1)) {
 975              $nextmod = $mods[$modids[$position + 1]];
 976          }
 977  
 978          $activitynav = new \core_course\output\activity_navigation($prevmod, $nextmod, $activitylist);
 979          $renderer = $this->page->get_renderer('core', 'course');
 980          return $renderer->render($activitynav);
 981      }
 982  
 983      /**
 984       * The standard tags (typically script tags that are not needed earlier) that
 985       * should be output after everything else. Designed to be called in theme layout.php files.
 986       *
 987       * @return string HTML fragment.
 988       */
 989      public function standard_end_of_body_html() {
 990          global $CFG;
 991  
 992          // This function is normally called from a layout.php file in {@link core_renderer::header()}
 993          // but some of the content won't be known until later, so we return a placeholder
 994          // for now. This will be replaced with the real content in {@link core_renderer::footer()}.
 995          $output = '';
 996          if ($this->page->pagelayout !== 'embedded' && !empty($CFG->additionalhtmlfooter)) {
 997              $output .= "\n".$CFG->additionalhtmlfooter;
 998          }
 999          $output .= $this->unique_end_html_token;
1000          return $output;
1001      }
1002  
1003      /**
1004       * The standard HTML that should be output just before the <footer> tag.
1005       * Designed to be called in theme layout.php files.
1006       *
1007       * @return string HTML fragment.
1008       */
1009      public function standard_after_main_region_html() {
1010          global $CFG;
1011          $output = '';
1012          if ($this->page->pagelayout !== 'embedded' && !empty($CFG->additionalhtmlbottomofbody)) {
1013              $output .= "\n".$CFG->additionalhtmlbottomofbody;
1014          }
1015  
1016          // Give subsystems an opportunity to inject extra html content. The callback
1017          // must always return a string containing valid html.
1018          foreach (\core_component::get_core_subsystems() as $name => $path) {
1019              if ($path) {
1020                  $output .= component_callback($name, 'standard_after_main_region_html', [], '');
1021              }
1022          }
1023  
1024          // Give plugins an opportunity to inject extra html content. The callback
1025          // must always return a string containing valid html.
1026          $pluginswithfunction = get_plugins_with_function('standard_after_main_region_html', 'lib.php');
1027          foreach ($pluginswithfunction as $plugins) {
1028              foreach ($plugins as $function) {
1029                  $output .= $function();
1030              }
1031          }
1032  
1033          return $output;
1034      }
1035  
1036      /**
1037       * Return the standard string that says whether you are logged in (and switched
1038       * roles/logged in as another user).
1039       * @param bool $withlinks if false, then don't include any links in the HTML produced.
1040       * If not set, the default is the nologinlinks option from the theme config.php file,
1041       * and if that is not set, then links are included.
1042       * @return string HTML fragment.
1043       */
1044      public function login_info($withlinks = null) {
1045          global $USER, $CFG, $DB, $SESSION;
1046  
1047          if (during_initial_install()) {
1048              return '';
1049          }
1050  
1051          if (is_null($withlinks)) {
1052              $withlinks = empty($this->page->layout_options['nologinlinks']);
1053          }
1054  
1055          $course = $this->page->course;
1056          if (\core\session\manager::is_loggedinas()) {
1057              $realuser = \core\session\manager::get_realuser();
1058              $fullname = fullname($realuser);
1059              if ($withlinks) {
1060                  $loginastitle = get_string('loginas');
1061                  $realuserinfo = " [<a href=\"$CFG->wwwroot/course/loginas.php?id=$course->id&amp;sesskey=".sesskey()."\"";
1062                  $realuserinfo .= "title =\"".$loginastitle."\">$fullname</a>] ";
1063              } else {
1064                  $realuserinfo = " [$fullname] ";
1065              }
1066          } else {
1067              $realuserinfo = '';
1068          }
1069  
1070          $loginpage = $this->is_login_page();
1071          $loginurl = get_login_url();
1072  
1073          if (empty($course->id)) {
1074              // $course->id is not defined during installation
1075              return '';
1076          } else if (isloggedin()) {
1077              $context = context_course::instance($course->id);
1078  
1079              $fullname = fullname($USER);
1080              // Since Moodle 2.0 this link always goes to the public profile page (not the course profile page)
1081              if ($withlinks) {
1082                  $linktitle = get_string('viewprofile');
1083                  $username = "<a href=\"$CFG->wwwroot/user/profile.php?id=$USER->id\" title=\"$linktitle\">$fullname</a>";
1084              } else {
1085                  $username = $fullname;
1086              }
1087              if (is_mnet_remote_user($USER) and $idprovider = $DB->get_record('mnet_host', array('id'=>$USER->mnethostid))) {
1088                  if ($withlinks) {
1089                      $username .= " from <a href=\"{$idprovider->wwwroot}\">{$idprovider->name}</a>";
1090                  } else {
1091                      $username .= " from {$idprovider->name}";
1092                  }
1093              }
1094              if (isguestuser()) {
1095                  $loggedinas = $realuserinfo.get_string('loggedinasguest');
1096                  if (!$loginpage && $withlinks) {
1097                      $loggedinas .= " (<a href=\"$loginurl\">".get_string('login').'</a>)';
1098                  }
1099              } else if (is_role_switched($course->id)) { // Has switched roles
1100                  $rolename = '';
1101                  if ($role = $DB->get_record('role', array('id'=>$USER->access['rsw'][$context->path]))) {
1102                      $rolename = ': '.role_get_name($role, $context);
1103                  }
1104                  $loggedinas = get_string('loggedinas', 'moodle', $username).$rolename;
1105                  if ($withlinks) {
1106                      $url = new moodle_url('/course/switchrole.php', array('id'=>$course->id,'sesskey'=>sesskey(), 'switchrole'=>0, 'returnurl'=>$this->page->url->out_as_local_url(false)));
1107                      $loggedinas .= ' ('.html_writer::tag('a', get_string('switchrolereturn'), array('href' => $url)).')';
1108                  }
1109              } else {
1110                  $loggedinas = $realuserinfo.get_string('loggedinas', 'moodle', $username);
1111                  if ($withlinks) {
1112                      $loggedinas .= " (<a href=\"$CFG->wwwroot/login/logout.php?sesskey=".sesskey()."\">".get_string('logout').'</a>)';
1113                  }
1114              }
1115          } else {
1116              $loggedinas = get_string('loggedinnot', 'moodle');
1117              if (!$loginpage && $withlinks) {
1118                  $loggedinas .= " (<a href=\"$loginurl\">".get_string('login').'</a>)';
1119              }
1120          }
1121  
1122          $loggedinas = '<div class="logininfo">'.$loggedinas.'</div>';
1123  
1124          if (isset($SESSION->justloggedin)) {
1125              unset($SESSION->justloggedin);
1126              if (!empty($CFG->displayloginfailures)) {
1127                  if (!isguestuser()) {
1128                      // Include this file only when required.
1129                      require_once($CFG->dirroot . '/user/lib.php');
1130                      if ($count = user_count_login_failures($USER)) {
1131                          $loggedinas .= '<div class="loginfailures">';
1132                          $a = new stdClass();
1133                          $a->attempts = $count;
1134                          $loggedinas .= get_string('failedloginattempts', '', $a);
1135                          if (file_exists("$CFG->dirroot/report/log/index.php") and has_capability('report/log:view', context_system::instance())) {
1136                              $loggedinas .= ' ('.html_writer::link(new moodle_url('/report/log/index.php', array('chooselog' => 1,
1137                                      'id' => 0 , 'modid' => 'site_errors')), get_string('logs')).')';
1138                          }
1139                          $loggedinas .= '</div>';
1140                      }
1141                  }
1142              }
1143          }
1144  
1145          return $loggedinas;
1146      }
1147  
1148      /**
1149       * Check whether the current page is a login page.
1150       *
1151       * @since Moodle 2.9
1152       * @return bool
1153       */
1154      protected function is_login_page() {
1155          // This is a real bit of a hack, but its a rarety that we need to do something like this.
1156          // In fact the login pages should be only these two pages and as exposing this as an option for all pages
1157          // could lead to abuse (or at least unneedingly complex code) the hack is the way to go.
1158          return in_array(
1159              $this->page->url->out_as_local_url(false, array()),
1160              array(
1161                  '/login/index.php',
1162                  '/login/forgot_password.php',
1163              )
1164          );
1165      }
1166  
1167      /**
1168       * Return the 'back' link that normally appears in the footer.
1169       *
1170       * @return string HTML fragment.
1171       */
1172      public function home_link() {
1173          global $CFG, $SITE;
1174  
1175          if ($this->page->pagetype == 'site-index') {
1176              // Special case for site home page - please do not remove
1177              return '<div class="sitelink">' .
1178                     '<a title="Moodle" class="d-inline-block aalink" href="http://moodle.org/">' .
1179                     '<img src="' . $this->image_url('moodlelogo_grayhat') . '" alt="'.get_string('moodlelogo').'" /></a></div>';
1180  
1181          } else if (!empty($CFG->target_release) && $CFG->target_release != $CFG->release) {
1182              // Special case for during install/upgrade.
1183              return '<div class="sitelink">'.
1184                     '<a title="Moodle" href="http://docs.moodle.org/en/Administrator_documentation" onclick="this.target=\'_blank\'">' .
1185                     '<img src="' . $this->image_url('moodlelogo_grayhat') . '" alt="'.get_string('moodlelogo').'" /></a></div>';
1186  
1187          } else if ($this->page->course->id == $SITE->id || strpos($this->page->pagetype, 'course-view') === 0) {
1188              return '<div class="homelink"><a href="' . $CFG->wwwroot . '/">' .
1189                      get_string('home') . '</a></div>';
1190  
1191          } else {
1192              return '<div class="homelink"><a href="' . $CFG->wwwroot . '/course/view.php?id=' . $this->page->course->id . '">' .
1193                      format_string($this->page->course->shortname, true, array('context' => $this->page->context)) . '</a></div>';
1194          }
1195      }
1196  
1197      /**
1198       * Redirects the user by any means possible given the current state
1199       *
1200       * This function should not be called directly, it should always be called using
1201       * the redirect function in lib/weblib.php
1202       *
1203       * The redirect function should really only be called before page output has started
1204       * however it will allow itself to be called during the state STATE_IN_BODY
1205       *
1206       * @param string $encodedurl The URL to send to encoded if required
1207       * @param string $message The message to display to the user if any
1208       * @param int $delay The delay before redirecting a user, if $message has been
1209       *         set this is a requirement and defaults to 3, set to 0 no delay
1210       * @param boolean $debugdisableredirect this redirect has been disabled for
1211       *         debugging purposes. Display a message that explains, and don't
1212       *         trigger the redirect.
1213       * @param string $messagetype The type of notification to show the message in.
1214       *         See constants on \core\output\notification.
1215       * @return string The HTML to display to the user before dying, may contain
1216       *         meta refresh, javascript refresh, and may have set header redirects
1217       */
1218      public function redirect_message($encodedurl, $message, $delay, $debugdisableredirect,
1219                                       $messagetype = \core\output\notification::NOTIFY_INFO) {
1220          global $CFG;
1221          $url = str_replace('&amp;', '&', $encodedurl);
1222  
1223          switch ($this->page->state) {
1224              case moodle_page::STATE_BEFORE_HEADER :
1225                  // No output yet it is safe to delivery the full arsenal of redirect methods
1226                  if (!$debugdisableredirect) {
1227                      // Don't use exactly the same time here, it can cause problems when both redirects fire at the same time.
1228                      $this->metarefreshtag = '<meta http-equiv="refresh" content="'. $delay .'; url='. $encodedurl .'" />'."\n";
1229                      $this->page->requires->js_function_call('document.location.replace', array($url), false, ($delay + 3));
1230                  }
1231                  $output = $this->header();
1232                  break;
1233              case moodle_page::STATE_PRINTING_HEADER :
1234                  // We should hopefully never get here
1235                  throw new coding_exception('You cannot redirect while printing the page header');
1236                  break;
1237              case moodle_page::STATE_IN_BODY :
1238                  // We really shouldn't be here but we can deal with this
1239                  debugging("You should really redirect before you start page output");
1240                  if (!$debugdisableredirect) {
1241                      $this->page->requires->js_function_call('document.location.replace', array($url), false, $delay);
1242                  }
1243                  $output = $this->opencontainers->pop_all_but_last();
1244                  break;
1245              case moodle_page::STATE_DONE :
1246                  // Too late to be calling redirect now
1247                  throw new coding_exception('You cannot redirect after the entire page has been generated');
1248                  break;
1249          }
1250          $output .= $this->notification($message, $messagetype);
1251          $output .= '<div class="continuebutton">(<a href="'. $encodedurl .'">'. get_string('continue') .'</a>)</div>';
1252          if ($debugdisableredirect) {
1253              $output .= '<p><strong>'.get_string('erroroutput', 'error').'</strong></p>';
1254          }
1255          $output .= $this->footer();
1256          return $output;
1257      }
1258  
1259      /**
1260       * Start output by sending the HTTP headers, and printing the HTML <head>
1261       * and the start of the <body>.
1262       *
1263       * To control what is printed, you should set properties on $PAGE.
1264       *
1265       * @return string HTML that you must output this, preferably immediately.
1266       */
1267      public function header() {
1268          global $USER, $CFG, $SESSION;
1269  
1270          // Give plugins an opportunity touch things before the http headers are sent
1271          // such as adding additional headers. The return value is ignored.
1272          $pluginswithfunction = get_plugins_with_function('before_http_headers', 'lib.php');
1273          foreach ($pluginswithfunction as $plugins) {
1274              foreach ($plugins as $function) {
1275                  $function();
1276              }
1277          }
1278  
1279          if (\core\session\manager::is_loggedinas()) {
1280              $this->page->add_body_class('userloggedinas');
1281          }
1282  
1283          if (isset($SESSION->justloggedin) && !empty($CFG->displayloginfailures)) {
1284              require_once($CFG->dirroot . '/user/lib.php');
1285              // Set second parameter to false as we do not want reset the counter, the same message appears on footer.
1286              if ($count = user_count_login_failures($USER, false)) {
1287                  $this->page->add_body_class('loginfailures');
1288              }
1289          }
1290  
1291          // If the user is logged in, and we're not in initial install,
1292          // check to see if the user is role-switched and add the appropriate
1293          // CSS class to the body element.
1294          if (!during_initial_install() && isloggedin() && is_role_switched($this->page->course->id)) {
1295              $this->page->add_body_class('userswitchedrole');
1296          }
1297  
1298          // Give themes a chance to init/alter the page object.
1299          $this->page->theme->init_page($this->page);
1300  
1301          $this->page->set_state(moodle_page::STATE_PRINTING_HEADER);
1302  
1303          // Find the appropriate page layout file, based on $this->page->pagelayout.
1304          $layoutfile = $this->page->theme->layout_file($this->page->pagelayout);
1305          // Render the layout using the layout file.
1306          $rendered = $this->render_page_layout($layoutfile);
1307  
1308          // Slice the rendered output into header and footer.
1309          $cutpos = strpos($rendered, $this->unique_main_content_token);
1310          if ($cutpos === false) {
1311              $cutpos = strpos($rendered, self::MAIN_CONTENT_TOKEN);
1312              $token = self::MAIN_CONTENT_TOKEN;
1313          } else {
1314              $token = $this->unique_main_content_token;
1315          }
1316  
1317          if ($cutpos === false) {
1318              throw new coding_exception('page layout file ' . $layoutfile . ' does not contain the main content placeholder, please include "<?php echo $OUTPUT->main_content() ?>" in theme layout file.');
1319          }
1320          $header = substr($rendered, 0, $cutpos);
1321          $footer = substr($rendered, $cutpos + strlen($token));
1322  
1323          if (empty($this->contenttype)) {
1324              debugging('The page layout file did not call $OUTPUT->doctype()');
1325              $header = $this->doctype() . $header;
1326          }
1327  
1328          // If this theme version is below 2.4 release and this is a course view page
1329          if ((!isset($this->page->theme->settings->version) || $this->page->theme->settings->version < 2012101500) &&
1330                  $this->page->pagelayout === 'course' && $this->page->url->compare(new moodle_url('/course/view.php'), URL_MATCH_BASE)) {
1331              // check if course content header/footer have not been output during render of theme layout
1332              $coursecontentheader = $this->course_content_header(true);
1333              $coursecontentfooter = $this->course_content_footer(true);
1334              if (!empty($coursecontentheader)) {
1335                  // display debug message and add header and footer right above and below main content
1336                  // Please note that course header and footer (to be displayed above and below the whole page)
1337                  // are not displayed in this case at all.
1338                  // Besides the content header and footer are not displayed on any other course page
1339                  debugging('The current theme is not optimised for 2.4, the course-specific header and footer defined in course format will not be output', DEBUG_DEVELOPER);
1340                  $header .= $coursecontentheader;
1341                  $footer = $coursecontentfooter. $footer;
1342              }
1343          }
1344  
1345          send_headers($this->contenttype, $this->page->cacheable);
1346  
1347          $this->opencontainers->push('header/footer', $footer);
1348          $this->page->set_state(moodle_page::STATE_IN_BODY);
1349  
1350          return $header . $this->skip_link_target('maincontent');
1351      }
1352  
1353      /**
1354       * Renders and outputs the page layout file.
1355       *
1356       * This is done by preparing the normal globals available to a script, and
1357       * then including the layout file provided by the current theme for the
1358       * requested layout.
1359       *
1360       * @param string $layoutfile The name of the layout file
1361       * @return string HTML code
1362       */
1363      protected function render_page_layout($layoutfile) {
1364          global $CFG, $SITE, $USER;
1365          // The next lines are a bit tricky. The point is, here we are in a method
1366          // of a renderer class, and this object may, or may not, be the same as
1367          // the global $OUTPUT object. When rendering the page layout file, we want to use
1368          // this object. However, people writing Moodle code expect the current
1369          // renderer to be called $OUTPUT, not $this, so define a variable called
1370          // $OUTPUT pointing at $this. The same comment applies to $PAGE and $COURSE.
1371          $OUTPUT = $this;
1372          $PAGE = $this->page;
1373          $COURSE = $this->page->course;
1374  
1375          ob_start();
1376          include($layoutfile);
1377          $rendered = ob_get_contents();
1378          ob_end_clean();
1379          return $rendered;
1380      }
1381  
1382      /**
1383       * Outputs the page's footer
1384       *
1385       * @return string HTML fragment
1386       */
1387      public function footer() {
1388          global $CFG, $DB;
1389  
1390          $output = '';
1391  
1392          // Give plugins an opportunity to touch the page before JS is finalized.
1393          $pluginswithfunction = get_plugins_with_function('before_footer', 'lib.php');
1394          foreach ($pluginswithfunction as $plugins) {
1395              foreach ($plugins as $function) {
1396                  $extrafooter = $function();
1397                  if (is_string($extrafooter)) {
1398                      $output .= $extrafooter;
1399                  }
1400              }
1401          }
1402  
1403          $output .= $this->container_end_all(true);
1404  
1405          $footer = $this->opencontainers->pop('header/footer');
1406  
1407          if (debugging() and $DB and $DB->is_transaction_started()) {
1408              // TODO: MDL-20625 print warning - transaction will be rolled back
1409          }
1410  
1411          // Provide some performance info if required
1412          $performanceinfo = '';
1413          if (defined('MDL_PERF') || (!empty($CFG->perfdebug) and $CFG->perfdebug > 7)) {
1414              $perf = get_performance_info();
1415              if (defined('MDL_PERFTOFOOT') || debugging() || $CFG->perfdebug > 7) {
1416                  $performanceinfo = $perf['html'];
1417              }
1418          }
1419  
1420          // We always want performance data when running a performance test, even if the user is redirected to another page.
1421          if (MDL_PERF_TEST && strpos($footer, $this->unique_performance_info_token) === false) {
1422              $footer = $this->unique_performance_info_token . $footer;
1423          }
1424          $footer = str_replace($this->unique_performance_info_token, $performanceinfo, $footer);
1425  
1426          // Only show notifications when the current page has a context id.
1427          if (!empty($this->page->context->id)) {
1428              $this->page->requires->js_call_amd('core/notification', 'init', array(
1429                  $this->page->context->id,
1430                  \core\notification::fetch_as_array($this)
1431              ));
1432          }
1433          $footer = str_replace($this->unique_end_html_token, $this->page->requires->get_end_code(), $footer);
1434  
1435          $this->page->set_state(moodle_page::STATE_DONE);
1436  
1437          return $output . $footer;
1438      }
1439  
1440      /**
1441       * Close all but the last open container. This is useful in places like error
1442       * handling, where you want to close all the open containers (apart from <body>)
1443       * before outputting the error message.
1444       *
1445       * @param bool $shouldbenone assert that the stack should be empty now - causes a
1446       *      developer debug warning if it isn't.
1447       * @return string the HTML required to close any open containers inside <body>.
1448       */
1449      public function container_end_all($shouldbenone = false) {
1450          return $this->opencontainers->pop_all_but_last($shouldbenone);
1451      }
1452  
1453      /**
1454       * Returns course-specific information to be output immediately above content on any course page
1455       * (for the current course)
1456       *
1457       * @param bool $onlyifnotcalledbefore output content only if it has not been output before
1458       * @return string
1459       */
1460      public function course_content_header($onlyifnotcalledbefore = false) {
1461          global $CFG;
1462          static $functioncalled = false;
1463          if ($functioncalled && $onlyifnotcalledbefore) {
1464              // we have already output the content header
1465              return '';
1466          }
1467  
1468          // Output any session notification.
1469          $notifications = \core\notification::fetch();
1470  
1471          $bodynotifications = '';
1472          foreach ($notifications as $notification) {
1473              $bodynotifications .= $this->render_from_template(
1474                      $notification->get_template_name(),
1475                      $notification->export_for_template($this)
1476                  );
1477          }
1478  
1479          $output = html_writer::span($bodynotifications, 'notifications', array('id' => 'user-notifications'));
1480  
1481          if ($this->page->course->id == SITEID) {
1482              // return immediately and do not include /course/lib.php if not necessary
1483              return $output;
1484          }
1485  
1486          require_once($CFG->dirroot.'/course/lib.php');
1487          $functioncalled = true;
1488          $courseformat = course_get_format($this->page->course);
1489          if (($obj = $courseformat->course_content_header()) !== null) {
1490              $output .= html_writer::div($courseformat->get_renderer($this->page)->render($obj), 'course-content-header');
1491          }
1492          return $output;
1493      }
1494  
1495      /**
1496       * Returns course-specific information to be output immediately below content on any course page
1497       * (for the current course)
1498       *
1499       * @param bool $onlyifnotcalledbefore output content only if it has not been output before
1500       * @return string
1501       */
1502      public function course_content_footer($onlyifnotcalledbefore = false) {
1503          global $CFG;
1504          if ($this->page->course->id == SITEID) {
1505              // return immediately and do not include /course/lib.php if not necessary
1506              return '';
1507          }
1508          static $functioncalled = false;
1509          if ($functioncalled && $onlyifnotcalledbefore) {
1510              // we have already output the content footer
1511              return '';
1512          }
1513          $functioncalled = true;
1514          require_once($CFG->dirroot.'/course/lib.php');
1515          $courseformat = course_get_format($this->page->course);
1516          if (($obj = $courseformat->course_content_footer()) !== null) {
1517              return html_writer::div($courseformat->get_renderer($this->page)->render($obj), 'course-content-footer');
1518          }
1519          return '';
1520      }
1521  
1522      /**
1523       * Returns course-specific information to be output on any course page in the header area
1524       * (for the current course)
1525       *
1526       * @return string
1527       */
1528      public function course_header() {
1529          global $CFG;
1530          if ($this->page->course->id == SITEID) {
1531              // return immediately and do not include /course/lib.php if not necessary
1532              return '';
1533          }
1534          require_once($CFG->dirroot.'/course/lib.php');
1535          $courseformat = course_get_format($this->page->course);
1536          if (($obj = $courseformat->course_header()) !== null) {
1537              return $courseformat->get_renderer($this->page)->render($obj);
1538          }
1539          return '';
1540      }
1541  
1542      /**
1543       * Returns course-specific information to be output on any course page in the footer area
1544       * (for the current course)
1545       *
1546       * @return string
1547       */
1548      public function course_footer() {
1549          global $CFG;
1550          if ($this->page->course->id == SITEID) {
1551              // return immediately and do not include /course/lib.php if not necessary
1552              return '';
1553          }
1554          require_once($CFG->dirroot.'/course/lib.php');
1555          $courseformat = course_get_format($this->page->course);
1556          if (($obj = $courseformat->course_footer()) !== null) {
1557              return $courseformat->get_renderer($this->page)->render($obj);
1558          }
1559          return '';
1560      }
1561  
1562      /**
1563       * Get the course pattern datauri to show on a course card.
1564       *
1565       * The datauri is an encoded svg that can be passed as a url.
1566       * @param int $id Id to use when generating the pattern
1567       * @return string datauri
1568       */
1569      public function get_generated_image_for_id($id) {
1570          $color = $this->get_generated_color_for_id($id);
1571          $pattern = new \core_geopattern();
1572          $pattern->setColor($color);
1573          $pattern->patternbyid($id);
1574          return $pattern->datauri();
1575      }
1576  
1577      /**
1578       * Get the course color to show on a course card.
1579       *
1580       * @param int $id Id to use when generating the color.
1581       * @return string hex color code.
1582       */
1583      public function get_generated_color_for_id($id) {
1584          $colornumbers = range(1, 10);
1585          $basecolors = [];
1586          foreach ($colornumbers as $number) {
1587              $basecolors[] = get_config('core_admin', 'coursecolor' . $number);
1588          }
1589  
1590          $color = $basecolors[$id % 10];
1591          return $color;
1592      }
1593  
1594      /**
1595       * Returns lang menu or '', this method also checks forcing of languages in courses.
1596       *
1597       * This function calls {@link core_renderer::render_single_select()} to actually display the language menu.
1598       *
1599       * @return string The lang menu HTML or empty string
1600       */
1601      public function lang_menu() {
1602          global $CFG;
1603  
1604          if (empty($CFG->langmenu)) {
1605              return '';
1606          }
1607  
1608          if ($this->page->course != SITEID and !empty($this->page->course->lang)) {
1609              // do not show lang menu if language forced
1610              return '';
1611          }
1612  
1613          $currlang = current_language();
1614          $langs = get_string_manager()->get_list_of_translations();
1615  
1616          if (count($langs) < 2) {
1617              return '';
1618          }
1619  
1620          $s = new single_select($this->page->url, 'lang', $langs, $currlang, null);
1621          $s->label = get_accesshide(get_string('language'));
1622          $s->class = 'langmenu';
1623          return $this->render($s);
1624      }
1625  
1626      /**
1627       * Output the row of editing icons for a block, as defined by the controls array.
1628       *
1629       * @param array $controls an array like {@link block_contents::$controls}.
1630       * @param string $blockid The ID given to the block.
1631       * @return string HTML fragment.
1632       */
1633      public function block_controls($actions, $blockid = null) {
1634          global $CFG;
1635          if (empty($actions)) {
1636              return '';
1637          }
1638          $menu = new action_menu($actions);
1639          if ($blockid !== null) {
1640              $menu->set_owner_selector('#'.$blockid);
1641          }
1642          $menu->set_constraint('.block-region');
1643          $menu->attributes['class'] .= ' block-control-actions commands';
1644          return $this->render($menu);
1645      }
1646  
1647      /**
1648       * Returns the HTML for a basic textarea field.
1649       *
1650       * @param string $name Name to use for the textarea element
1651       * @param string $id The id to use fort he textarea element
1652       * @param string $value Initial content to display in the textarea
1653       * @param int $rows Number of rows to display
1654       * @param int $cols Number of columns to display
1655       * @return string the HTML to display
1656       */
1657      public function print_textarea($name, $id, $value, $rows, $cols) {
1658          editors_head_setup();
1659          $editor = editors_get_preferred_editor(FORMAT_HTML);
1660          $editor->set_text($value);
1661          $editor->use_editor($id, []);
1662  
1663          $context = [
1664              'id' => $id,
1665              'name' => $name,
1666              'value' => $value,
1667              'rows' => $rows,
1668              'cols' => $cols
1669          ];
1670  
1671          return $this->render_from_template('core_form/editor_textarea', $context);
1672      }
1673  
1674      /**
1675       * Renders an action menu component.
1676       *
1677       * @param action_menu $menu
1678       * @return string HTML
1679       */
1680      public function render_action_menu(action_menu $menu) {
1681  
1682          // We don't want the class icon there!
1683          foreach ($menu->get_secondary_actions() as $action) {
1684              if ($action instanceof \action_menu_link && $action->has_class('icon')) {
1685                  $action->attributes['class'] = preg_replace('/(^|\s+)icon(\s+|$)/i', '', $action->attributes['class']);
1686              }
1687          }
1688  
1689          if ($menu->is_empty()) {
1690              return '';
1691          }
1692          $context = $menu->export_for_template($this);
1693  
1694          return $this->render_from_template('core/action_menu', $context);
1695      }
1696  
1697      /**
1698       * Renders a Check API result
1699       *
1700       * @param result $result
1701       * @return string HTML fragment
1702       */
1703      protected function render_check_result(core\check\result $result) {
1704          return $this->render_from_template($result->get_template_name(), $result->export_for_template($this));
1705      }
1706  
1707      /**
1708       * Renders a Check API result
1709       *
1710       * @param result $result
1711       * @return string HTML fragment
1712       */
1713      public function check_result(core\check\result $result) {
1714          return $this->render_check_result($result);
1715      }
1716  
1717      /**
1718       * Renders an action_menu_link item.
1719       *
1720       * @param action_menu_link $action
1721       * @return string HTML fragment
1722       */
1723      protected function render_action_menu_link(action_menu_link $action) {
1724          return $this->render_from_template('core/action_menu_link', $action->export_for_template($this));
1725      }
1726  
1727      /**
1728       * Renders a primary action_menu_filler item.
1729       *
1730       * @param action_menu_link_filler $action
1731       * @return string HTML fragment
1732       */
1733      protected function render_action_menu_filler(action_menu_filler $action) {
1734          return html_writer::span('&nbsp;', 'filler');
1735      }
1736  
1737      /**
1738       * Renders a primary action_menu_link item.
1739       *
1740       * @param action_menu_link_primary $action
1741       * @return string HTML fragment
1742       */
1743      protected function render_action_menu_link_primary(action_menu_link_primary $action) {
1744          return $this->render_action_menu_link($action);
1745      }
1746  
1747      /**
1748       * Renders a secondary action_menu_link item.
1749       *
1750       * @param action_menu_link_secondary $action
1751       * @return string HTML fragment
1752       */
1753      protected function render_action_menu_link_secondary(action_menu_link_secondary $action) {
1754          return $this->render_action_menu_link($action);
1755      }
1756  
1757      /**
1758       * Prints a nice side block with an optional header.
1759       *
1760       * @param block_contents $bc HTML for the content
1761       * @param string $region the region the block is appearing in.
1762       * @return string the HTML to be output.
1763       */
1764      public function block(block_contents $bc, $region) {
1765          $bc = clone($bc); // Avoid messing up the object passed in.
1766          if (empty($bc->blockinstanceid) || !strip_tags($bc->title)) {
1767              $bc->collapsible = block_contents::NOT_HIDEABLE;
1768          }
1769  
1770          $id = !empty($bc->attributes['id']) ? $bc->attributes['id'] : uniqid('block-');
1771          $context = new stdClass();
1772          $context->skipid = $bc->skipid;
1773          $context->blockinstanceid = $bc->blockinstanceid ?: uniqid('fakeid-');
1774          $context->dockable = $bc->dockable;
1775          $context->id = $id;
1776          $context->hidden = $bc->collapsible == block_contents::HIDDEN;
1777          $context->skiptitle = strip_tags($bc->title);
1778          $context->showskiplink = !empty($context->skiptitle);
1779          $context->arialabel = $bc->arialabel;
1780          $context->ariarole = !empty($bc->attributes['role']) ? $bc->attributes['role'] : 'complementary';
1781          $context->class = $bc->attributes['class'];
1782          $context->type = $bc->attributes['data-block'];
1783          $context->title = $bc->title;
1784          $context->content = $bc->content;
1785          $context->annotation = $bc->annotation;
1786          $context->footer = $bc->footer;
1787          $context->hascontrols = !empty($bc->controls);
1788          if ($context->hascontrols) {
1789              $context->controls = $this->block_controls($bc->controls, $id);
1790          }
1791  
1792          return $this->render_from_template('core/block', $context);
1793      }
1794  
1795      /**
1796       * Render the contents of a block_list.
1797       *
1798       * @param array $icons the icon for each item.
1799       * @param array $items the content of each item.
1800       * @return string HTML
1801       */
1802      public function list_block_contents($icons, $items) {
1803          $row = 0;
1804          $lis = array();
1805          foreach ($items as $key => $string) {
1806              $item = html_writer::start_tag('li', array('class' => 'r' . $row));
1807              if (!empty($icons[$key])) { //test if the content has an assigned icon
1808                  $item .= html_writer::tag('div', $icons[$key], array('class' => 'icon column c0'));
1809              }
1810              $item .= html_writer::tag('div', $string, array('class' => 'column c1'));
1811              $item .= html_writer::end_tag('li');
1812              $lis[] = $item;
1813              $row = 1 - $row; // Flip even/odd.
1814          }
1815          return html_writer::tag('ul', implode("\n", $lis), array('class' => 'unlist'));
1816      }
1817  
1818      /**
1819       * Output all the blocks in a particular region.
1820       *
1821       * @param string $region the name of a region on this page.
1822       * @return string the HTML to be output.
1823       */
1824      public function blocks_for_region($region) {
1825          $blockcontents = $this->page->blocks->get_content_for_region($region, $this);
1826          $lastblock = null;
1827          $zones = array();
1828          foreach ($blockcontents as $bc) {
1829              if ($bc instanceof block_contents) {
1830                  $zones[] = $bc->title;
1831              }
1832          }
1833          $output = '';
1834  
1835          foreach ($blockcontents as $bc) {
1836              if ($bc instanceof block_contents) {
1837                  $output .= $this->block($bc, $region);
1838                  $lastblock = $bc->title;
1839              } else if ($bc instanceof block_move_target) {
1840                  $output .= $this->block_move_target($bc, $zones, $lastblock, $region);
1841              } else {
1842                  throw new coding_exception('Unexpected type of thing (' . get_class($bc) . ') found in list of block contents.');
1843              }
1844          }
1845          return $output;
1846      }
1847  
1848      /**
1849       * Output a place where the block that is currently being moved can be dropped.
1850       *
1851       * @param block_move_target $target with the necessary details.
1852       * @param array $zones array of areas where the block can be moved to
1853       * @param string $previous the block located before the area currently being rendered.
1854       * @param string $region the name of the region
1855       * @return string the HTML to be output.
1856       */
1857      public function block_move_target($target, $zones, $previous, $region) {
1858          if ($previous == null) {
1859              if (empty($zones)) {
1860                  // There are no zones, probably because there are no blocks.
1861                  $regions = $this->page->theme->get_all_block_regions();
1862                  $position = get_string('moveblockinregion', 'block', $regions[$region]);
1863              } else {
1864                  $position = get_string('moveblockbefore', 'block', $zones[0]);
1865              }
1866          } else {
1867              $position = get_string('moveblockafter', 'block', $previous);
1868          }
1869          return html_writer::tag('a', html_writer::tag('span', $position, array('class' => 'accesshide')), array('href' => $target->url, 'class' => 'blockmovetarget'));
1870      }
1871  
1872      /**
1873       * Renders a special html link with attached action
1874       *
1875       * Theme developers: DO NOT OVERRIDE! Please override function
1876       * {@link core_renderer::render_action_link()} instead.
1877       *
1878       * @param string|moodle_url $url
1879       * @param string $text HTML fragment
1880       * @param component_action $action
1881       * @param array $attributes associative array of html link attributes + disabled
1882       * @param pix_icon optional pix icon to render with the link
1883       * @return string HTML fragment
1884       */
1885      public function action_link($url, $text, component_action $action = null, array $attributes = null, $icon = null) {
1886          if (!($url instanceof moodle_url)) {
1887              $url = new moodle_url($url);
1888          }
1889          $link = new action_link($url, $text, $action, $attributes, $icon);
1890  
1891          return $this->render($link);
1892      }
1893  
1894      /**
1895       * Renders an action_link object.
1896       *
1897       * The provided link is renderer and the HTML returned. At the same time the
1898       * associated actions are setup in JS by {@link core_renderer::add_action_handler()}
1899       *
1900       * @param action_link $link
1901       * @return string HTML fragment
1902       */
1903      protected function render_action_link(action_link $link) {
1904          return $this->render_from_template('core/action_link', $link->export_for_template($this));
1905      }
1906  
1907      /**
1908       * Renders an action_icon.
1909       *
1910       * This function uses the {@link core_renderer::action_link()} method for the
1911       * most part. What it does different is prepare the icon as HTML and use it
1912       * as the link text.
1913       *
1914       * Theme developers: If you want to change how action links and/or icons are rendered,
1915       * consider overriding function {@link core_renderer::render_action_link()} and
1916       * {@link core_renderer::render_pix_icon()}.
1917       *
1918       * @param string|moodle_url $url A string URL or moodel_url
1919       * @param pix_icon $pixicon
1920       * @param component_action $action
1921       * @param array $attributes associative array of html link attributes + disabled
1922       * @param bool $linktext show title next to image in link
1923       * @return string HTML fragment
1924       */
1925      public function action_icon($url, pix_icon $pixicon, component_action $action = null, array $attributes = null, $linktext=false) {
1926          if (!($url instanceof moodle_url)) {
1927              $url = new moodle_url($url);
1928          }
1929          $attributes = (array)$attributes;
1930  
1931          if (empty($attributes['class'])) {
1932              // let ppl override the class via $options
1933              $attributes['class'] = 'action-icon';
1934          }
1935  
1936          $icon = $this->render($pixicon);
1937  
1938          if ($linktext) {
1939              $text = $pixicon->attributes['alt'];
1940          } else {
1941              $text = '';
1942          }
1943  
1944          return $this->action_link($url, $text.$icon, $action, $attributes);
1945      }
1946  
1947     /**
1948      * Print a message along with button choices for Continue/Cancel
1949      *
1950      * If a string or moodle_url is given instead of a single_button, method defaults to post.
1951      *
1952      * @param string $message The question to ask the user
1953      * @param single_button|moodle_url|string $continue The single_button component representing the Continue answer. Can also be a moodle_url or string URL
1954      * @param single_button|moodle_url|string $cancel The single_button component representing the Cancel answer. Can also be a moodle_url or string URL
1955      * @return string HTML fragment
1956      */
1957      public function confirm($message, $continue, $cancel) {
1958          if ($continue instanceof single_button) {
1959              // ok
1960              $continue->primary = true;
1961          } else if (is_string($continue)) {
1962              $continue = new single_button(new moodle_url($continue), get_string('continue'), 'post', true);
1963          } else if ($continue instanceof moodle_url) {
1964              $continue = new single_button($continue, get_string('continue'), 'post', true);
1965          } else {
1966              throw new coding_exception('The continue param to $OUTPUT->confirm() must be either a URL (string/moodle_url) or a single_button instance.');
1967          }
1968  
1969          if ($cancel instanceof single_button) {
1970              // ok
1971          } else if (is_string($cancel)) {
1972              $cancel = new single_button(new moodle_url($cancel), get_string('cancel'), 'get');
1973          } else if ($cancel instanceof moodle_url) {
1974              $cancel = new single_button($cancel, get_string('cancel'), 'get');
1975          } else {
1976              throw new coding_exception('The cancel param to $OUTPUT->confirm() must be either a URL (string/moodle_url) or a single_button instance.');
1977          }
1978  
1979          $attributes = [
1980              'role'=>'alertdialog',
1981              'aria-labelledby'=>'modal-header',
1982              'aria-describedby'=>'modal-body',
1983              'aria-modal'=>'true'
1984          ];
1985  
1986          $output = $this->box_start('generalbox modal modal-dialog modal-in-page show', 'notice', $attributes);
1987          $output .= $this->box_start('modal-content', 'modal-content');
1988          $output .= $this->box_start('modal-header px-3', 'modal-header');
1989          $output .= html_writer::tag('h4', get_string('confirm'));
1990          $output .= $this->box_end();
1991          $attributes = [
1992              'role'=>'alert',
1993              'data-aria-autofocus'=>'true'
1994          ];
1995          $output .= $this->box_start('modal-body', 'modal-body', $attributes);
1996          $output .= html_writer::tag('p', $message);
1997          $output .= $this->box_end();
1998          $output .= $this->box_start('modal-footer', 'modal-footer');
1999          $output .= html_writer::tag('div', $this->render($continue) . $this->render($cancel), array('class' => 'buttons'));
2000          $output .= $this->box_end();
2001          $output .= $this->box_end();
2002          $output .= $this->box_end();
2003          return $output;
2004      }
2005  
2006      /**
2007       * Returns a form with a single button.
2008       *
2009       * Theme developers: DO NOT OVERRIDE! Please override function
2010       * {@link core_renderer::render_single_button()} instead.
2011       *
2012       * @param string|moodle_url $url
2013       * @param string $label button text
2014       * @param string $method get or post submit method
2015       * @param array $options associative array {disabled, title, etc.}
2016       * @return string HTML fragment
2017       */
2018      public function single_button($url, $label, $method='post', array $options=null) {
2019          if (!($url instanceof moodle_url)) {
2020              $url = new moodle_url($url);
2021          }
2022          $button = new single_button($url, $label, $method);
2023  
2024          foreach ((array)$options as $key=>$value) {
2025              if (property_exists($button, $key)) {
2026                  $button->$key = $value;
2027              } else {
2028                  $button->set_attribute($key, $value);
2029              }
2030          }
2031  
2032          return $this->render($button);
2033      }
2034  
2035      /**
2036       * Renders a single button widget.
2037       *
2038       * This will return HTML to display a form containing a single button.
2039       *
2040       * @param single_button $button
2041       * @return string HTML fragment
2042       */
2043      protected function render_single_button(single_button $button) {
2044          return $this->render_from_template('core/single_button', $button->export_for_template($this));
2045      }
2046  
2047      /**
2048       * Returns a form with a single select widget.
2049       *
2050       * Theme developers: DO NOT OVERRIDE! Please override function
2051       * {@link core_renderer::render_single_select()} instead.
2052       *
2053       * @param moodle_url $url form action target, includes hidden fields
2054       * @param string $name name of selection field - the changing parameter in url
2055       * @param array $options list of options
2056       * @param string $selected selected element
2057       * @param array $nothing
2058       * @param string $formid
2059       * @param array $attributes other attributes for the single select
2060       * @return string HTML fragment
2061       */
2062      public function single_select($url, $name, array $options, $selected = '',
2063                                  $nothing = array('' => 'choosedots'), $formid = null, $attributes = array()) {
2064          if (!($url instanceof moodle_url)) {
2065              $url = new moodle_url($url);
2066          }
2067          $select = new single_select($url, $name, $options, $selected, $nothing, $formid);
2068  
2069          if (array_key_exists('label', $attributes)) {
2070              $select->set_label($attributes['label']);
2071              unset($attributes['label']);
2072          }
2073          $select->attributes = $attributes;
2074  
2075          return $this->render($select);
2076      }
2077  
2078      /**
2079       * Returns a dataformat selection and download form
2080       *
2081       * @param string $label A text label
2082       * @param moodle_url|string $base The download page url
2083       * @param string $name The query param which will hold the type of the download
2084       * @param array $params Extra params sent to the download page
2085       * @return string HTML fragment
2086       */
2087      public function download_dataformat_selector($label, $base, $name = 'dataformat', $params = array()) {
2088  
2089          $formats = core_plugin_manager::instance()->get_plugins_of_type('dataformat');
2090          $options = array();
2091          foreach ($formats as $format) {
2092              if ($format->is_enabled()) {
2093                  $options[] = array(
2094                      'value' => $format->name,
2095                      'label' => get_string('dataformat', $format->component),
2096                  );
2097              }
2098          }
2099          $hiddenparams = array();
2100          foreach ($params as $key => $value) {
2101              $hiddenparams[] = array(
2102                  'name' => $key,
2103                  'value' => $value,
2104              );
2105          }
2106          $data = array(
2107              'label' => $label,
2108              'base' => $base,
2109              'name' => $name,
2110              'params' => $hiddenparams,
2111              'options' => $options,
2112              'sesskey' => sesskey(),
2113              'submit' => get_string('download'),
2114          );
2115  
2116          return $this->render_from_template('core/dataformat_selector', $data);
2117      }
2118  
2119  
2120      /**
2121       * Internal implementation of single_select rendering
2122       *
2123       * @param single_select $select
2124       * @return string HTML fragment
2125       */
2126      protected function render_single_select(single_select $select) {
2127          return $this->render_from_template('core/single_select', $select->export_for_template($this));
2128      }
2129  
2130      /**
2131       * Returns a form with a url select widget.
2132       *
2133       * Theme developers: DO NOT OVERRIDE! Please override function
2134       * {@link core_renderer::render_url_select()} instead.
2135       *
2136       * @param array $urls list of urls - array('/course/view.php?id=1'=>'Frontpage', ....)
2137       * @param string $selected selected element
2138       * @param array $nothing
2139       * @param string $formid
2140       * @return string HTML fragment
2141       */
2142      public function url_select(array $urls, $selected, $nothing = array('' => 'choosedots'), $formid = null) {
2143          $select = new url_select($urls, $selected, $nothing, $formid);
2144          return $this->render($select);
2145      }
2146  
2147      /**
2148       * Internal implementation of url_select rendering
2149       *
2150       * @param url_select $select
2151       * @return string HTML fragment
2152       */
2153      protected function render_url_select(url_select $select) {
2154          return $this->render_from_template('core/url_select', $select->export_for_template($this));
2155      }
2156  
2157      /**
2158       * Returns a string containing a link to the user documentation.
2159       * Also contains an icon by default. Shown to teachers and admin only.
2160       *
2161       * @param string $path The page link after doc root and language, no leading slash.
2162       * @param string $text The text to be displayed for the link
2163       * @param boolean $forcepopup Whether to force a popup regardless of the value of $CFG->doctonewwindow
2164       * @param array $attributes htm attributes
2165       * @return string
2166       */
2167      public function doc_link($path, $text = '', $forcepopup = false, array $attributes = []) {
2168          global $CFG;
2169  
2170          $icon = $this->pix_icon('docs', '', 'moodle', array('class'=>'iconhelp icon-pre', 'role'=>'presentation'));
2171  
2172          $attributes['href'] = new moodle_url(get_docs_url($path));
2173          if (!empty($CFG->doctonewwindow) || $forcepopup) {
2174              $attributes['class'] = 'helplinkpopup';
2175          }
2176  
2177          return html_writer::tag('a', $icon.$text, $attributes);
2178      }
2179  
2180      /**
2181       * Return HTML for an image_icon.
2182       *
2183       * Theme developers: DO NOT OVERRIDE! Please override function
2184       * {@link core_renderer::render_image_icon()} instead.
2185       *
2186       * @param string $pix short pix name
2187       * @param string $alt mandatory alt attribute
2188       * @param string $component standard compoennt name like 'moodle', 'mod_forum', etc.
2189       * @param array $attributes htm attributes
2190       * @return string HTML fragment
2191       */
2192      public function image_icon($pix, $alt, $component='moodle', array $attributes = null) {
2193          $icon = new image_icon($pix, $alt, $component, $attributes);
2194          return $this->render($icon);
2195      }
2196  
2197      /**
2198       * Renders a pix_icon widget and returns the HTML to display it.
2199       *
2200       * @param image_icon $icon
2201       * @return string HTML fragment
2202       */
2203      protected function render_image_icon(image_icon $icon) {
2204          $system = \core\output\icon_system::instance(\core\output\icon_system::STANDARD);
2205          return $system->render_pix_icon($this, $icon);
2206      }
2207  
2208      /**
2209       * Return HTML for a pix_icon.
2210       *
2211       * Theme developers: DO NOT OVERRIDE! Please override function
2212       * {@link core_renderer::render_pix_icon()} instead.
2213       *
2214       * @param string $pix short pix name
2215       * @param string $alt mandatory alt attribute
2216       * @param string $component standard compoennt name like 'moodle', 'mod_forum', etc.
2217       * @param array $attributes htm lattributes
2218       * @return string HTML fragment
2219       */
2220      public function pix_icon($pix, $alt, $component='moodle', array $attributes = null) {
2221          $icon = new pix_icon($pix, $alt, $component, $attributes);
2222          return $this->render($icon);
2223      }
2224  
2225      /**
2226       * Renders a pix_icon widget and returns the HTML to display it.
2227       *
2228       * @param pix_icon $icon
2229       * @return string HTML fragment
2230       */
2231      protected function render_pix_icon(pix_icon $icon) {
2232          $system = \core\output\icon_system::instance();
2233          return $system->render_pix_icon($this, $icon);
2234      }
2235  
2236      /**
2237       * Return HTML to display an emoticon icon.
2238       *
2239       * @param pix_emoticon $emoticon
2240       * @return string HTML fragment
2241       */
2242      protected function render_pix_emoticon(pix_emoticon $emoticon) {
2243          $system = \core\output\icon_system::instance(\core\output\icon_system::STANDARD);
2244          return $system->render_pix_icon($this, $emoticon);
2245      }
2246  
2247      /**
2248       * Produces the html that represents this rating in the UI
2249       *
2250       * @param rating $rating the page object on which this rating will appear
2251       * @return string
2252       */
2253      function render_rating(rating $rating) {
2254          global $CFG, $USER;
2255  
2256          if ($rating->settings->aggregationmethod == RATING_AGGREGATE_NONE) {
2257              return null;//ratings are turned off
2258          }
2259  
2260          $ratingmanager = new rating_manager();
2261          // Initialise the JavaScript so ratings can be done by AJAX.
2262          $ratingmanager->initialise_rating_javascript($this->page);
2263  
2264          $strrate = get_string("rate", "rating");
2265          $ratinghtml = ''; //the string we'll return
2266  
2267          // permissions check - can they view the aggregate?
2268          if ($rating->user_can_view_aggregate()) {
2269  
2270              $aggregatelabel = $ratingmanager->get_aggregate_label($rating->settings->aggregationmethod);
2271              $aggregatelabel = html_writer::tag('span', $aggregatelabel, array('class'=>'rating-aggregate-label'));
2272              $aggregatestr   = $rating->get_aggregate_string();
2273  
2274              $aggregatehtml  = html_writer::tag('span', $aggregatestr, array('id' => 'ratingaggregate'.$rating->itemid, 'class' => 'ratingaggregate')).' ';
2275              if ($rating->count > 0) {
2276                  $countstr = "({$rating->count})";
2277              } else {
2278                  $countstr = '-';
2279              }
2280              $aggregatehtml .= html_writer::tag('span', $countstr, array('id'=>"ratingcount{$rating->itemid}", 'class' => 'ratingcount')).' ';
2281  
2282              if ($rating->settings->permissions->viewall && $rating->settings->pluginpermissions->viewall) {
2283  
2284                  $nonpopuplink = $rating->get_view_ratings_url();
2285                  $popuplink = $rating->get_view_ratings_url(true);
2286  
2287                  $action = new popup_action('click', $popuplink, 'ratings', array('height' => 400, 'width' => 600));
2288                  $aggregatehtml = $this->action_link($nonpopuplink, $aggregatehtml, $action);
2289              }
2290  
2291              $ratinghtml .= html_writer::tag('span', $aggregatelabel . $aggregatehtml, array('class' => 'rating-aggregate-container'));
2292          }
2293  
2294          $formstart = null;
2295          // if the item doesn't belong to the current user, the user has permission to rate
2296          // and we're within the assessable period
2297          if ($rating->user_can_rate()) {
2298  
2299              $rateurl = $rating->get_rate_url();
2300              $inputs = $rateurl->params();
2301  
2302              //start the rating form
2303              $formattrs = array(
2304                  'id'     => "postrating{$rating->itemid}",
2305                  'class'  => 'postratingform',
2306                  'method' => 'post',
2307                  'action' => $rateurl->out_omit_querystring()
2308              );
2309              $formstart  = html_writer::start_tag('form', $formattrs);
2310              $formstart .= html_writer::start_tag('div', array('class' => 'ratingform'));
2311  
2312              // add the hidden inputs
2313              foreach ($inputs as $name => $value) {
2314                  $attributes = array('type' => 'hidden', 'class' => 'ratinginput', 'name' => $name, 'value' => $value);
2315                  $formstart .= html_writer::empty_tag('input', $attributes);
2316              }
2317  
2318              if (empty($ratinghtml)) {
2319                  $ratinghtml .= $strrate.': ';
2320              }
2321              $ratinghtml = $formstart.$ratinghtml;
2322  
2323              $scalearray = array(RATING_UNSET_RATING => $strrate.'...') + $rating->settings->scale->scaleitems;
2324              $scaleattrs = array('class'=>'postratingmenu ratinginput','id'=>'menurating'.$rating->itemid);
2325              $ratinghtml .= html_writer::label($rating->rating, 'menurating'.$rating->itemid, false, array('class' => 'accesshide'));
2326              $ratinghtml .= html_writer::select($scalearray, 'rating', $rating->rating, false, $scaleattrs);
2327  
2328              //output submit button
2329              $ratinghtml .= html_writer::start_tag('span', array('class'=>"ratingsubmit"));
2330  
2331              $attributes = array('type' => 'submit', 'class' => 'postratingmenusubmit', 'id' => 'postratingsubmit'.$rating->itemid, 'value' => s(get_string('rate', 'rating')));
2332              $ratinghtml .= html_writer::empty_tag('input', $attributes);
2333  
2334              if (!$rating->settings->scale->isnumeric) {
2335                  // If a global scale, try to find current course ID from the context
2336                  if (empty($rating->settings->scale->courseid) and $coursecontext = $rating->context->get_course_context(false)) {
2337                      $courseid = $coursecontext->instanceid;
2338                  } else {
2339                      $courseid = $rating->settings->scale->courseid;
2340                  }
2341                  $ratinghtml .= $this->help_icon_scale($courseid, $rating->settings->scale);
2342              }
2343              $ratinghtml .= html_writer::end_tag('span');
2344              $ratinghtml .= html_writer::end_tag('div');
2345              $ratinghtml .= html_writer::end_tag('form');
2346          }
2347  
2348          return $ratinghtml;
2349      }
2350  
2351      /**
2352       * Centered heading with attached help button (same title text)
2353       * and optional icon attached.
2354       *
2355       * @param string $text A heading text
2356       * @param string $helpidentifier The keyword that defines a help page
2357       * @param string $component component name
2358       * @param string|moodle_url $icon
2359       * @param string $iconalt icon alt text
2360       * @param int $level The level of importance of the heading. Defaulting to 2
2361       * @param string $classnames A space-separated list of CSS classes. Defaulting to null
2362       * @return string HTML fragment
2363       */
2364      public function heading_with_help($text, $helpidentifier, $component = 'moodle', $icon = '', $iconalt = '', $level = 2, $classnames = null) {
2365          $image = '';
2366          if ($icon) {
2367              $image = $this->pix_icon($icon, $iconalt, $component, array('class'=>'icon iconlarge'));
2368          }
2369  
2370          $help = '';
2371          if ($helpidentifier) {
2372              $help = $this->help_icon($helpidentifier, $component);
2373          }
2374  
2375          return $this->heading($image.$text.$help, $level, $classnames);
2376      }
2377  
2378      /**
2379       * Returns HTML to display a help icon.
2380       *
2381       * @deprecated since Moodle 2.0
2382       */
2383      public function old_help_icon($helpidentifier, $title, $component = 'moodle', $linktext = '') {
2384          throw new coding_exception('old_help_icon() can not be used any more, please see help_icon().');
2385      }
2386  
2387      /**
2388       * Returns HTML to display a help icon.
2389       *
2390       * Theme developers: DO NOT OVERRIDE! Please override function
2391       * {@link core_renderer::render_help_icon()} instead.
2392       *
2393       * @param string $identifier The keyword that defines a help page
2394       * @param string $component component name
2395       * @param string|bool $linktext true means use $title as link text, string means link text value
2396       * @return string HTML fragment
2397       */
2398      public function help_icon($identifier, $component = 'moodle', $linktext = '') {
2399          $icon = new help_icon($identifier, $component);
2400          $icon->diag_strings();
2401          if ($linktext === true) {
2402              $icon->linktext = get_string($icon->identifier, $icon->component);
2403          } else if (!empty($linktext)) {
2404              $icon->linktext = $linktext;
2405          }
2406          return $this->render($icon);
2407      }
2408  
2409      /**
2410       * Implementation of user image rendering.
2411       *
2412       * @param help_icon $helpicon A help icon instance
2413       * @return string HTML fragment
2414       */
2415      protected function render_help_icon(help_icon $helpicon) {
2416          $context = $helpicon->export_for_template($this);
2417          return $this->render_from_template('core/help_icon', $context);
2418      }
2419  
2420      /**
2421       * Returns HTML to display a scale help icon.
2422       *
2423       * @param int $courseid
2424       * @param stdClass $scale instance
2425       * @return string HTML fragment
2426       */
2427      public function help_icon_scale($courseid, stdClass $scale) {
2428          global $CFG;
2429  
2430          $title = get_string('helpprefix2', '', $scale->name) .' ('.get_string('newwindow').')';
2431  
2432          $icon = $this->pix_icon('help', get_string('scales'), 'moodle', array('class'=>'iconhelp'));
2433  
2434          $scaleid = abs($scale->id);
2435  
2436          $link = new moodle_url('/course/scales.php', array('id' => $courseid, 'list' => true, 'scaleid' => $scaleid));
2437          $action = new popup_action('click', $link, 'ratingscale');
2438  
2439          return html_writer::tag('span', $this->action_link($link, $icon, $action), array('class' => 'helplink'));
2440      }
2441  
2442      /**
2443       * Creates and returns a spacer image with optional line break.
2444       *
2445       * @param array $attributes Any HTML attributes to add to the spaced.
2446       * @param bool $br Include a BR after the spacer.... DON'T USE THIS. Don't be
2447       *     laxy do it with CSS which is a much better solution.
2448       * @return string HTML fragment
2449       */
2450      public function spacer(array $attributes = null, $br = false) {
2451          $attributes = (array)$attributes;
2452          if (empty($attributes['width'])) {
2453              $attributes['width'] = 1;
2454          }
2455          if (empty($attributes['height'])) {
2456              $attributes['height'] = 1;
2457          }
2458          $attributes['class'] = 'spacer';
2459  
2460          $output = $this->pix_icon('spacer', '', 'moodle', $attributes);
2461  
2462          if (!empty($br)) {
2463              $output .= '<br />';
2464          }
2465  
2466          return $output;
2467      }
2468  
2469      /**
2470       * Returns HTML to display the specified user's avatar.
2471       *
2472       * User avatar may be obtained in two ways:
2473       * <pre>
2474       * // Option 1: (shortcut for simple cases, preferred way)
2475       * // $user has come from the DB and has fields id, picture, imagealt, firstname and lastname
2476       * $OUTPUT->user_picture($user, array('popup'=>true));
2477       *
2478       * // Option 2:
2479       * $userpic = new user_picture($user);
2480       * // Set properties of $userpic
2481       * $userpic->popup = true;
2482       * $OUTPUT->render($userpic);
2483       * </pre>
2484       *
2485       * Theme developers: DO NOT OVERRIDE! Please override function
2486       * {@link core_renderer::render_user_picture()} instead.
2487       *
2488       * @param stdClass $user Object with at least fields id, picture, imagealt, firstname, lastname
2489       *     If any of these are missing, the database is queried. Avoid this
2490       *     if at all possible, particularly for reports. It is very bad for performance.
2491       * @param array $options associative array with user picture options, used only if not a user_picture object,
2492       *     options are:
2493       *     - courseid=$this->page->course->id (course id of user profile in link)
2494       *     - size=35 (size of image)
2495       *     - link=true (make image clickable - the link leads to user profile)
2496       *     - popup=false (open in popup)
2497       *     - alttext=true (add image alt attribute)
2498       *     - class = image class attribute (default 'userpicture')
2499       *     - visibletoscreenreaders=true (whether to be visible to screen readers)
2500       *     - includefullname=false (whether to include the user's full name together with the user picture)
2501       *     - includetoken = false (whether to use a token for authentication. True for current user, int value for other user id)
2502       * @return string HTML fragment
2503       */
2504      public function user_picture(stdClass $user, array $options = null) {
2505          $userpicture = new user_picture($user);
2506          foreach ((array)$options as $key=>$value) {
2507              if (property_exists($userpicture, $key)) {
2508                  $userpicture->$key = $value;
2509              }
2510          }
2511          return $this->render($userpicture);
2512      }
2513  
2514      /**
2515       * Internal implementation of user image rendering.
2516       *
2517       * @param user_picture $userpicture
2518       * @return string
2519       */
2520      protected function render_user_picture(user_picture $userpicture) {
2521          $user = $userpicture->user;
2522          $canviewfullnames = has_capability('moodle/site:viewfullnames', $this->page->context);
2523  
2524          if ($userpicture->alttext) {
2525              if (!empty($user->imagealt)) {
2526                  $alt = $user->imagealt;
2527              } else {
2528                  $alt = get_string('pictureof', '', fullname($user, $canviewfullnames));
2529              }
2530          } else {
2531              $alt = '';
2532          }
2533  
2534          if (empty($userpicture->size)) {
2535              $size = 35;
2536          } else if ($userpicture->size === true or $userpicture->size == 1) {
2537              $size = 100;
2538          } else {
2539              $size = $userpicture->size;
2540          }
2541  
2542          $class = $userpicture->class;
2543  
2544          if ($user->picture == 0) {
2545              $class .= ' defaultuserpic';
2546          }
2547  
2548          $src = $userpicture->get_url($this->page, $this);
2549  
2550          $attributes = array('src' => $src, 'class' => $class, 'width' => $size, 'height' => $size);
2551          if (!$userpicture->visibletoscreenreaders) {
2552              $alt = '';
2553          }
2554          $attributes['alt'] = $alt;
2555  
2556          if (!empty($alt)) {
2557              $attributes['title'] = $alt;
2558          }
2559  
2560          // get the image html output fisrt
2561          $output = html_writer::empty_tag('img', $attributes);
2562  
2563          // Show fullname together with the picture when desired.
2564          if ($userpicture->includefullname) {
2565              $output .= fullname($userpicture->user, $canviewfullnames);
2566          }
2567  
2568          // then wrap it in link if needed
2569          if (!$userpicture->link) {
2570              return $output;
2571          }
2572  
2573          if (empty($userpicture->courseid)) {
2574              $courseid = $this->page->course->id;
2575          } else {
2576              $courseid = $userpicture->courseid;
2577          }
2578  
2579          if ($courseid == SITEID) {
2580              $url = new moodle_url('/user/profile.php', array('id' => $user->id));
2581          } else {
2582              $url = new moodle_url('/user/view.php', array('id' => $user->id, 'course' => $courseid));
2583          }
2584  
2585          $attributes = array('href' => $url, 'class' => 'd-inline-block aabtn');
2586          if (!$userpicture->visibletoscreenreaders) {
2587              $attributes['tabindex'] = '-1';
2588              $attributes['aria-hidden'] = 'true';
2589          }
2590  
2591          if ($userpicture->popup) {
2592              $id = html_writer::random_id('userpicture');
2593              $attributes['id'] = $id;
2594              $this->add_action_handler(new popup_action('click', $url), $id);
2595          }
2596  
2597          return html_writer::tag('a', $output, $attributes);
2598      }
2599  
2600      /**
2601       * Internal implementation of file tree viewer items rendering.
2602       *
2603       * @param array $dir
2604       * @return string
2605       */
2606      public function htmllize_file_tree($dir) {
2607          if (empty($dir['subdirs']) and empty($dir['files'])) {
2608              return '';
2609          }
2610          $result = '<ul>';
2611          foreach ($dir['subdirs'] as $subdir) {
2612              $result .= '<li>'.s($subdir['dirname']).' '.$this->htmllize_file_tree($subdir).'</li>';
2613          }
2614          foreach ($dir['files'] as $file) {
2615              $filename = $file->get_filename();
2616              $result .= '<li><span>'.html_writer::link($file->fileurl, $filename).'</span></li>';
2617          }
2618          $result .= '</ul>';
2619  
2620          return $result;
2621      }
2622  
2623      /**
2624       * Returns HTML to display the file picker
2625       *
2626       * <pre>
2627       * $OUTPUT->file_picker($options);
2628       * </pre>
2629       *
2630       * Theme developers: DO NOT OVERRIDE! Please override function
2631       * {@link core_renderer::render_file_picker()} instead.
2632       *
2633       * @param array $options associative array with file manager options
2634       *   options are:
2635       *       maxbytes=>-1,
2636       *       itemid=>0,
2637       *       client_id=>uniqid(),
2638       *       acepted_types=>'*',
2639       *       return_types=>FILE_INTERNAL,
2640       *       context=>current page context
2641       * @return string HTML fragment
2642       */
2643      public function file_picker($options) {
2644          $fp = new file_picker($options);
2645          return $this->render($fp);
2646      }
2647  
2648      /**
2649       * Internal implementation of file picker rendering.
2650       *
2651       * @param file_picker $fp
2652       * @return string
2653       */
2654      public function render_file_picker(file_picker $fp) {
2655          $options = $fp->options;
2656          $client_id = $options->client_id;
2657          $strsaved = get_string('filesaved', 'repository');
2658          $straddfile = get_string('openpicker', 'repository');
2659          $strloading  = get_string('loading', 'repository');
2660          $strdndenabled = get_string('dndenabled_inbox', 'moodle');
2661          $strdroptoupload = get_string('droptoupload', 'moodle');
2662          $iconprogress = $this->pix_icon('i/loading_small', $strloading).'';
2663  
2664          $currentfile = $options->currentfile;
2665          if (empty($currentfile)) {
2666              $currentfile = '';
2667          } else {
2668              $currentfile .= ' - ';
2669          }
2670          if ($options->maxbytes) {
2671              $size = $options->maxbytes;
2672          } else {
2673              $size = get_max_upload_file_size();
2674          }
2675          if ($size == -1) {
2676              $maxsize = '';
2677          } else {
2678              $maxsize = get_string('maxfilesize', 'moodle', display_size($size));
2679          }
2680          if ($options->buttonname) {
2681              $buttonname = ' name="' . $options->buttonname . '"';
2682          } else {
2683              $buttonname = '';
2684          }
2685          $html = <<<EOD
2686  <div class="filemanager-loading mdl-align" id='filepicker-loading-{$client_id}'>
2687  $iconprogress
2688  </div>
2689  <div id="filepicker-wrapper-{$client_id}" class="mdl-left w-100" style="display:none">
2690      <div>
2691          <input type="button" class="btn btn-secondary fp-btn-choose" id="filepicker-button-{$client_id}" value="{$straddfile}"{$buttonname}/>
2692          <span> $maxsize </span>
2693      </div>
2694  EOD;
2695          if ($options->env != 'url') {
2696              $html .= <<<EOD
2697      <div id="file_info_{$client_id}" class="mdl-left filepicker-filelist" style="position: relative">
2698      <div class="filepicker-filename">
2699          <div class="filepicker-container">$currentfile<div class="dndupload-message">$strdndenabled <br/><div class="dndupload-arrow"></div></div></div>
2700          <div class="dndupload-progressbars"></div>
2701      </div>
2702      <div><div class="dndupload-target">{$strdroptoupload}<br/><div class="dndupload-arrow"></div></div></div>
2703      </div>
2704  EOD;
2705          }
2706          $html .= '</div>';
2707          return $html;
2708      }
2709  
2710      /**
2711       * @deprecated since Moodle 3.2
2712       */
2713      public function update_module_button() {
2714          throw new coding_exception('core_renderer::update_module_button() can not be used anymore. Activity ' .
2715              'modules should not add the edit module button, the link is already available in the Administration block. ' .
2716              'Themes can choose to display the link in the buttons row consistently for all module types.');
2717      }
2718  
2719      /**
2720       * Returns HTML to display a "Turn editing on/off" button in a form.
2721       *
2722       * @param moodle_url $url The URL + params to send through when clicking the button
2723       * @return string HTML the button
2724       */
2725      public function edit_button(moodle_url $url) {
2726  
2727          $url->param('sesskey', sesskey());
2728          if ($this->page->user_is_editing()) {
2729              $url->param('edit', 'off');
2730              $editstring = get_string('turneditingoff');
2731          } else {
2732              $url->param('edit', 'on');
2733              $editstring = get_string('turneditingon');
2734          }
2735  
2736          return $this->single_button($url, $editstring);
2737      }
2738  
2739      /**
2740       * Returns HTML to display a simple button to close a window
2741       *
2742       * @param string $text The lang string for the button's label (already output from get_string())
2743       * @return string html fragment
2744       */
2745      public function close_window_button($text='') {
2746          if (empty($text)) {
2747              $text = get_string('closewindow');
2748          }
2749          $button = new single_button(new moodle_url('#'), $text, 'get');
2750          $button->add_action(new component_action('click', 'close_window'));
2751  
2752          return $this->container($this->render($button), 'closewindow');
2753      }
2754  
2755      /**
2756       * Output an error message. By default wraps the error message in <span class="error">.
2757       * If the error message is blank, nothing is output.
2758       *
2759       * @param string $message the error message.
2760       * @return string the HTML to output.
2761       */
2762      public function error_text($message) {
2763          if (empty($message)) {
2764              return '';
2765          }
2766          $message = $this->pix_icon('i/warning', get_string('error'), '', array('class' => 'icon icon-pre', 'title'=>'')) . $message;
2767          return html_writer::tag('span', $message, array('class' => 'error'));
2768      }
2769  
2770      /**
2771       * Do not call this function directly.
2772       *
2773       * To terminate the current script with a fatal error, call the {@link print_error}
2774       * function, or throw an exception. Doing either of those things will then call this
2775       * function to display the error, before terminating the execution.
2776       *
2777       * @param string $message The message to output
2778       * @param string $moreinfourl URL where more info can be found about the error
2779       * @param string $link Link for the Continue button
2780       * @param array $backtrace The execution backtrace
2781       * @param string $debuginfo Debugging information
2782       * @return string the HTML to output.
2783       */
2784      public function fatal_error($message, $moreinfourl, $link, $backtrace, $debuginfo = null, $errorcode = "") {
2785          global $CFG;
2786  
2787          $output = '';
2788          $obbuffer = '';
2789  
2790          if ($this->has_started()) {
2791              // we can not always recover properly here, we have problems with output buffering,
2792              // html tables, etc.
2793              $output .= $this->opencontainers->pop_all_but_last();
2794  
2795          } else {
2796              // It is really bad if library code throws exception when output buffering is on,
2797              // because the buffered text would be printed before our start of page.
2798              // NOTE: this hack might be behave unexpectedly in case output buffering is enabled in PHP.ini
2799              error_reporting(0); // disable notices from gzip compression, etc.
2800              while (ob_get_level() > 0) {
2801                  $buff = ob_get_clean();
2802                  if ($buff === false) {
2803                      break;
2804                  }
2805                  $obbuffer .= $buff;
2806              }
2807              error_reporting($CFG->debug);
2808  
2809              // Output not yet started.
2810              $protocol = (isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0');
2811              if (empty($_SERVER['HTTP_RANGE'])) {
2812                  @header($protocol . ' 404 Not Found');
2813              } else if (core_useragent::check_safari_ios_version(602) && !empty($_SERVER['HTTP_X_PLAYBACK_SESSION_ID'])) {
2814                  // Coax iOS 10 into sending the session cookie.
2815                  @header($protocol . ' 403 Forbidden');
2816              } else {
2817                  // Must stop byteserving attempts somehow,
2818                  // this is weird but Chrome PDF viewer can be stopped only with 407!
2819                  @header($protocol . ' 407 Proxy Authentication Required');
2820              }
2821  
2822              $this->page->set_context(null); // ugly hack - make sure page context is set to something, we do not want bogus warnings here
2823              $this->page->set_url('/'); // no url
2824              //$this->page->set_pagelayout('base'); //TODO: MDL-20676 blocks on error pages are weird, unfortunately it somehow detect the pagelayout from URL :-(
2825              $this->page->set_title(get_string('error'));
2826              $this->page->set_heading($this->page->course->fullname);
2827              $output .= $this->header();
2828          }
2829  
2830          $message = '<p class="errormessage">' . s($message) . '</p>'.
2831                  '<p class="errorcode"><a href="' . s($moreinfourl) . '">' .
2832                  get_string('moreinformation') . '</a></p>';
2833          if (empty($CFG->rolesactive)) {
2834              $message .= '<p class="errormessage">' . get_string('installproblem', 'error') . '</p>';
2835              //It is usually not possible to recover from errors triggered during installation, you may need to create a new database or use a different database prefix for new installation.
2836          }
2837          $output .= $this->box($message, 'errorbox alert alert-danger', null, array('data-rel' => 'fatalerror'));
2838  
2839          if ($CFG->debugdeveloper) {
2840              $labelsep = get_string('labelsep', 'langconfig');
2841              if (!empty($debuginfo)) {
2842                  $debuginfo = s($debuginfo); // removes all nasty JS
2843                  $debuginfo = str_replace("\n", '<br />', $debuginfo); // keep newlines
2844                  $label = get_string('debuginfo', 'debug') . $labelsep;
2845                  $output .= $this->notification("<strong>$label</strong> " . $debuginfo, 'notifytiny');
2846              }
2847              if (!empty($backtrace)) {
2848                  $label = get_string('stacktrace', 'debug') . $labelsep;
2849                  $output .= $this->notification("<strong>$label</strong> " . format_backtrace($backtrace), 'notifytiny');
2850              }
2851              if ($obbuffer !== '' ) {
2852                  $label = get_string('outputbuffer', 'debug') . $labelsep;
2853                  $output .= $this->notification("<strong>$label</strong> " . s($obbuffer), 'notifytiny');
2854              }
2855          }
2856  
2857          if (empty($CFG->rolesactive)) {
2858              // continue does not make much sense if moodle is not installed yet because error is most probably not recoverable
2859          } else if (!empty($link)) {
2860              $output .= $this->continue_button($link);
2861          }
2862  
2863          $output .= $this->footer();
2864  
2865          // Padding to encourage IE to display our error page, rather than its own.
2866          $output .= str_repeat(' ', 512);
2867  
2868          return $output;
2869      }
2870  
2871      /**
2872       * Output a notification (that is, a status message about something that has just happened).
2873       *
2874       * Note: \core\notification::add() may be more suitable for your usage.
2875       *
2876       * @param string $message The message to print out.
2877       * @param string $type    The type of notification. See constants on \core\output\notification.
2878       * @return string the HTML to output.
2879       */
2880      public function notification($message, $type = null) {
2881          $typemappings = [
2882              // Valid types.
2883              'success'           => \core\output\notification::NOTIFY_SUCCESS,
2884              'info'              => \core\output\notification::NOTIFY_INFO,
2885              'warning'           => \core\output\notification::NOTIFY_WARNING,
2886              'error'             => \core\output\notification::NOTIFY_ERROR,
2887  
2888              // Legacy types mapped to current types.
2889              'notifyproblem'     => \core\output\notification::NOTIFY_ERROR,
2890              'notifytiny'        => \core\output\notification::NOTIFY_ERROR,
2891              'notifyerror'       => \core\output\notification::NOTIFY_ERROR,
2892              'notifysuccess'     => \core\output\notification::NOTIFY_SUCCESS,
2893              'notifymessage'     => \core\output\notification::NOTIFY_INFO,
2894              'notifyredirect'    => \core\output\notification::NOTIFY_INFO,
2895              'redirectmessage'   => \core\output\notification::NOTIFY_INFO,
2896          ];
2897  
2898          $extraclasses = [];
2899  
2900          if ($type) {
2901              if (strpos($type, ' ') === false) {
2902                  // No spaces in the list of classes, therefore no need to loop over and determine the class.
2903                  if (isset($typemappings[$type])) {
2904                      $type = $typemappings[$type];
2905                  } else {
2906                      // The value provided did not match a known type. It must be an extra class.
2907                      $extraclasses = [$type];
2908                  }
2909              } else {
2910                  // Identify what type of notification this is.
2911                  $classarray = explode(' ', self::prepare_classes($type));
2912  
2913                  // Separate out the type of notification from the extra classes.
2914                  foreach ($classarray as $class) {
2915                      if (isset($typemappings[$class])) {
2916                          $type = $typemappings[$class];
2917                      } else {
2918                          $extraclasses[] = $class;
2919                      }
2920                  }
2921              }
2922          }
2923  
2924          $notification = new \core\output\notification($message, $type);
2925          if (count($extraclasses)) {
2926              $notification->set_extra_classes($extraclasses);
2927          }
2928  
2929          // Return the rendered template.
2930          return $this->render_from_template($notification->get_template_name(), $notification->export_for_template($this));
2931      }
2932  
2933      /**
2934       * @deprecated since Moodle 3.1 MDL-30811 - please do not use this function any more.
2935       */
2936      public function notify_problem() {
2937          throw new coding_exception('core_renderer::notify_problem() can not be used any more, '.
2938              'please use \core\notification::add(), or \core\output\notification as required.');
2939      }
2940  
2941      /**
2942       * @deprecated since Moodle 3.1 MDL-30811 - please do not use this function any more.
2943       */
2944      public function notify_success() {
2945          throw new coding_exception('core_renderer::notify_success() can not be used any more, '.
2946              'please use \core\notification::add(), or \core\output\notification as required.');
2947      }
2948  
2949      /**
2950       * @deprecated since Moodle 3.1 MDL-30811 - please do not use this function any more.
2951       */
2952      public function notify_message() {
2953          throw new coding_exception('core_renderer::notify_message() can not be used any more, '.
2954              'please use \core\notification::add(), or \core\output\notification as required.');
2955      }
2956  
2957      /**
2958       * @deprecated since Moodle 3.1 MDL-30811 - please do not use this function any more.
2959       */
2960      public function notify_redirect() {
2961          throw new coding_exception('core_renderer::notify_redirect() can not be used any more, '.
2962              'please use \core\notification::add(), or \core\output\notification as required.');
2963      }
2964  
2965      /**
2966       * Render a notification (that is, a status message about something that has
2967       * just happened).
2968       *
2969       * @param \core\output\notification $notification the notification to print out
2970       * @return string the HTML to output.
2971       */
2972      protected function render_notification(\core\output\notification $notification) {
2973          return $this->render_from_template($notification->get_template_name(), $notification->export_for_template($this));
2974      }
2975  
2976      /**
2977       * Returns HTML to display a continue button that goes to a particular URL.
2978       *
2979       * @param string|moodle_url $url The url the button goes to.
2980       * @return string the HTML to output.
2981       */
2982      public function continue_button($url) {
2983          if (!($url instanceof moodle_url)) {
2984              $url = new moodle_url($url);
2985          }
2986          $button = new single_button($url, get_string('continue'), 'get', true);
2987          $button->class = 'continuebutton';
2988  
2989          return $this->render($button);
2990      }
2991  
2992      /**
2993       * Returns HTML to display a single paging bar to provide access to other pages  (usually in a search)
2994       *
2995       * Theme developers: DO NOT OVERRIDE! Please override function
2996       * {@link core_renderer::render_paging_bar()} instead.
2997       *
2998       * @param int $totalcount The total number of entries available to be paged through
2999       * @param int $page The page you are currently viewing
3000       * @param int $perpage The number of entries that should be shown per page
3001       * @param string|moodle_url $baseurl url of the current page, the $pagevar parameter is added
3002       * @param string $pagevar name of page parameter that holds the page number
3003       * @return string the HTML to output.
3004       */
3005      public function paging_bar($totalcount, $page, $perpage, $baseurl, $pagevar = 'page') {
3006          $pb = new paging_bar($totalcount, $page, $perpage, $baseurl, $pagevar);
3007          return $this->render($pb);
3008      }
3009  
3010      /**
3011       * Returns HTML to display the paging bar.
3012       *
3013       * @param paging_bar $pagingbar
3014       * @return string the HTML to output.
3015       */
3016      protected function render_paging_bar(paging_bar $pagingbar) {
3017          // Any more than 10 is not usable and causes weird wrapping of the pagination.
3018          $pagingbar->maxdisplay = 10;
3019          return $this->render_from_template('core/paging_bar', $pagingbar->export_for_template($this));
3020      }
3021  
3022      /**
3023       * Returns HTML to display initials bar to provide access to other pages  (usually in a search)
3024       *
3025       * @param string $current the currently selected letter.
3026       * @param string $class class name to add to this initial bar.
3027       * @param string $title the name to put in front of this initial bar.
3028       * @param string $urlvar URL parameter name for this initial.
3029       * @param string $url URL object.
3030       * @param array $alpha of letters in the alphabet.
3031       * @return string the HTML to output.
3032       */
3033      public function initials_bar($current, $class, $title, $urlvar, $url, $alpha = null) {
3034          $ib = new initials_bar($current, $class, $title, $urlvar, $url, $alpha);
3035          return $this->render($ib);
3036      }
3037  
3038      /**
3039       * Internal implementation of initials bar rendering.
3040       *
3041       * @param initials_bar $initialsbar
3042       * @return string
3043       */
3044      protected function render_initials_bar(initials_bar $initialsbar) {
3045          return $this->render_from_template('core/initials_bar', $initialsbar->export_for_template($this));
3046      }
3047  
3048      /**
3049       * Output the place a skip link goes to.
3050       *
3051       * @param string $id The target name from the corresponding $PAGE->requires->skip_link_to($target) call.
3052       * @return string the HTML to output.
3053       */
3054      public function skip_link_target($id = null) {
3055          return html_writer::span('', '', array('id' => $id));
3056      }
3057  
3058      /**
3059       * Outputs a heading
3060       *
3061       * @param string $text The text of the heading
3062       * @param int $level The level of importance of the heading. Defaulting to 2
3063       * @param string $classes A space-separated list of CSS classes. Defaulting to null
3064       * @param string $id An optional ID
3065       * @return string the HTML to output.
3066       */
3067      public function heading($text, $level = 2, $classes = null, $id = null) {
3068          $level = (integer) $level;
3069          if ($level < 1 or $level > 6) {
3070              throw new coding_exception('Heading level must be an integer between 1 and 6.');
3071          }
3072          return html_writer::tag('h' . $level, $text, array('id' => $id, 'class' => renderer_base::prepare_classes($classes)));
3073      }
3074  
3075      /**
3076       * Outputs a box.
3077       *
3078       * @param string $contents The contents of the box
3079       * @param string $classes A space-separated list of CSS classes
3080       * @param string $id An optional ID
3081       * @param array $attributes An array of other attributes to give the box.
3082       * @return string the HTML to output.
3083       */
3084      public function box($contents, $classes = 'generalbox', $id = null, $attributes = array()) {
3085          return $this->box_start($classes, $id, $attributes) . $contents . $this->box_end();
3086      }
3087  
3088      /**
3089       * Outputs the opening section of a box.
3090       *
3091       * @param string $classes A space-separated list of CSS classes
3092       * @param string $id An optional ID
3093       * @param array $attributes An array of other attributes to give the box.
3094       * @return string the HTML to output.
3095       */
3096      public function box_start($classes = 'generalbox', $id = null, $attributes = array()) {
3097          $this->opencontainers->push('box', html_writer::end_tag('div'));
3098          $attributes['id'] = $id;
3099          $attributes['class'] = 'box py-3 ' . renderer_base::prepare_classes($classes);
3100          return html_writer::start_tag('div', $attributes);
3101      }
3102  
3103      /**
3104       * Outputs the closing section of a box.
3105       *
3106       * @return string the HTML to output.
3107       */
3108      public function box_end() {
3109          return $this->opencontainers->pop('box');
3110      }
3111  
3112      /**
3113       * Outputs a container.
3114       *
3115       * @param string $contents The contents of the box
3116       * @param string $classes A space-separated list of CSS classes
3117       * @param string $id An optional ID
3118       * @return string the HTML to output.
3119       */
3120      public function container($contents, $classes = null, $id = null) {
3121          return $this->container_start($classes, $id) . $contents . $this->container_end();
3122      }
3123  
3124      /**
3125       * Outputs the opening section of a container.
3126       *
3127       * @param string $classes A space-separated list of CSS classes
3128       * @param string $id An optional ID
3129       * @return string the HTML to output.
3130       */
3131      public function container_start($classes = null, $id = null) {
3132          $this->opencontainers->push('container', html_writer::end_tag('div'));
3133          return html_writer::start_tag('div', array('id' => $id,
3134                  'class' => renderer_base::prepare_classes($classes)));
3135      }
3136  
3137      /**
3138       * Outputs the closing section of a container.
3139       *
3140       * @return string the HTML to output.
3141       */
3142      public function container_end() {
3143          return $this->opencontainers->pop('container');
3144      }
3145  
3146      /**
3147       * Make nested HTML lists out of the items
3148       *
3149       * The resulting list will look something like this:
3150       *
3151       * <pre>
3152       * <<ul>>
3153       * <<li>><div class='tree_item parent'>(item contents)</div>
3154       *      <<ul>
3155       *      <<li>><div class='tree_item'>(item contents)</div><</li>>
3156       *      <</ul>>
3157       * <</li>>
3158       * <</ul>>
3159       * </pre>
3160       *
3161       * @param array $items
3162       * @param array $attrs html attributes passed to the top ofs the list
3163       * @return string HTML
3164       */
3165      public function tree_block_contents($items, $attrs = array()) {
3166          // exit if empty, we don't want an empty ul element
3167          if (empty($items)) {
3168              return '';
3169          }
3170          // array of nested li elements
3171          $lis = array();
3172          foreach ($items as $item) {
3173              // this applies to the li item which contains all child lists too
3174              $content = $item->content($this);
3175              $liclasses = array($item->get_css_type());
3176              if (!$item->forceopen || (!$item->forceopen && $item->collapse) || ($item->children->count()==0  && $item->nodetype==navigation_node::NODETYPE_BRANCH)) {
3177                  $liclasses[] = 'collapsed';
3178              }
3179              if ($item->isactive === true) {
3180                  $liclasses[] = 'current_branch';
3181              }
3182              $liattr = array('class'=>join(' ',$liclasses));
3183              // class attribute on the div item which only contains the item content
3184              $divclasses = array('tree_item');
3185              if ($item->children->count()>0  || $item->nodetype==navigation_node::NODETYPE_BRANCH) {
3186                  $divclasses[] = 'branch';
3187              } else {
3188                  $divclasses[] = 'leaf';
3189              }
3190              if (!empty($item->classes) && count($item->classes)>0) {
3191                  $divclasses[] = join(' ', $item->classes);
3192              }
3193              $divattr = array('class'=>join(' ', $divclasses));
3194              if (!empty($item->id)) {
3195                  $divattr['id'] = $item->id;
3196              }
3197              $content = html_writer::tag('p', $content, $divattr) . $this->tree_block_contents($item->children);
3198              if (!empty($item->preceedwithhr) && $item->preceedwithhr===true) {
3199                  $content = html_writer::empty_tag('hr') . $content;
3200              }
3201              $content = html_writer::tag('li', $content, $liattr);
3202              $lis[] = $content;
3203          }
3204          return html_writer::tag('ul', implode("\n", $lis), $attrs);
3205      }
3206  
3207      /**
3208       * Returns a search box.
3209       *
3210       * @param  string $id     The search box wrapper div id, defaults to an autogenerated one.
3211       * @return string         HTML with the search form hidden by default.
3212       */
3213      public function search_box($id = false) {
3214          global $CFG;
3215  
3216          // Accessing $CFG directly as using \core_search::is_global_search_enabled would
3217          // result in an extra included file for each site, even the ones where global search
3218          // is disabled.
3219          if (empty($CFG->enableglobalsearch) || !has_capability('moodle/search:query', context_system::instance())) {
3220              return '';
3221          }
3222  
3223          $data = [
3224              'action' => new moodle_url('/search/index.php'),
3225              'hiddenfields' => (object) ['name' => 'context', 'value' => $this->page->context->id],
3226              'inputname' => 'q',
3227              'searchstring' => get_string('search'),
3228              ];
3229          return $this->render_from_template('core/search_input_navbar', $data);
3230      }
3231  
3232      /**
3233       * Allow plugins to provide some content to be rendered in the navbar.
3234       * The plugin must define a PLUGIN_render_navbar_output function that returns
3235       * the HTML they wish to add to the navbar.
3236       *
3237       * @return string HTML for the navbar
3238       */
3239      public function navbar_plugin_output() {
3240          $output = '';
3241  
3242          // Give subsystems an opportunity to inject extra html content. The callback
3243          // must always return a string containing valid html.
3244          foreach (\core_component::get_core_subsystems() as $name => $path) {
3245              if ($path) {
3246                  $output .= component_callback($name, 'render_navbar_output', [$this], '');
3247              }
3248          }
3249  
3250          if ($pluginsfunction = get_plugins_with_function('render_navbar_output')) {
3251              foreach ($pluginsfunction as $plugintype => $plugins) {
3252                  foreach ($plugins as $pluginfunction) {
3253                      $output .= $pluginfunction($this);
3254                  }
3255              }
3256          }
3257  
3258          return $output;
3259      }
3260  
3261      /**
3262       * Construct a user menu, returning HTML that can be echoed out by a
3263       * layout file.
3264       *
3265       * @param stdClass $user A user object, usually $USER.
3266       * @param bool $withlinks true if a dropdown should be built.
3267       * @return string HTML fragment.
3268       */
3269      public function user_menu($user = null, $withlinks = null) {
3270          global $USER, $CFG;
3271          require_once($CFG->dirroot . '/user/lib.php');
3272  
3273          if (is_null($user)) {
3274              $user = $USER;
3275          }
3276  
3277          // Note: this behaviour is intended to match that of core_renderer::login_info,
3278          // but should not be considered to be good practice; layout options are
3279          // intended to be theme-specific. Please don't copy this snippet anywhere else.
3280          if (is_null($withlinks)) {
3281              $withlinks = empty($this->page->layout_options['nologinlinks']);
3282          }
3283  
3284          // Add a class for when $withlinks is false.
3285          $usermenuclasses = 'usermenu';
3286          if (!$withlinks) {
3287              $usermenuclasses .= ' withoutlinks';
3288          }
3289  
3290          $returnstr = "";
3291  
3292          // If during initial install, return the empty return string.
3293          if (during_initial_install()) {
3294              return $returnstr;
3295          }
3296  
3297          $loginpage = $this->is_login_page();
3298          $loginurl = get_login_url();
3299          // If not logged in, show the typical not-logged-in string.
3300          if (!isloggedin()) {
3301              $returnstr = get_string('loggedinnot', 'moodle');
3302              if (!$loginpage) {
3303                  $returnstr .= " (<a href=\"$loginurl\">" . get_string('login') . '</a>)';
3304              }
3305              return html_writer::div(
3306                  html_writer::span(
3307                      $returnstr,
3308                      'login'
3309                  ),
3310                  $usermenuclasses
3311              );
3312  
3313          }
3314  
3315          // If logged in as a guest user, show a string to that effect.
3316          if (isguestuser()) {
3317              $returnstr = get_string('loggedinasguest');
3318              if (!$loginpage && $withlinks) {
3319                  $returnstr .= " (<a href=\"$loginurl\">".get_string('login').'</a>)';
3320              }
3321  
3322              return html_writer::div(
3323                  html_writer::span(
3324                      $returnstr,
3325                      'login'
3326                  ),
3327                  $usermenuclasses
3328              );
3329          }
3330  
3331          // Get some navigation opts.
3332          $opts = user_get_user_navigation_info($user, $this->page);
3333  
3334          $avatarclasses = "avatars";
3335          $avatarcontents = html_writer::span($opts->metadata['useravatar'], 'avatar current');
3336          $usertextcontents = $opts->metadata['userfullname'];
3337  
3338          // Other user.
3339          if (!empty($opts->metadata['asotheruser'])) {
3340              $avatarcontents .= html_writer::span(
3341                  $opts->metadata['realuseravatar'],
3342                  'avatar realuser'
3343              );
3344              $usertextcontents = $opts->metadata['realuserfullname'];
3345              $usertextcontents .= html_writer::tag(
3346                  'span',
3347                  get_string(
3348                      'loggedinas',
3349                      'moodle',
3350                      html_writer::span(
3351                          $opts->metadata['userfullname'],
3352                          'value'
3353                      )
3354                  ),
3355                  array('class' => 'meta viewingas')
3356              );
3357          }
3358  
3359          // Role.
3360          if (!empty($opts->metadata['asotherrole'])) {
3361              $role = core_text::strtolower(preg_replace('#[ ]+#', '-', trim($opts->metadata['rolename'])));
3362              $usertextcontents .= html_writer::span(
3363                  $opts->metadata['rolename'],
3364                  'meta role role-' . $role
3365              );
3366          }
3367  
3368          // User login failures.
3369          if (!empty($opts->metadata['userloginfail'])) {
3370              $usertextcontents .= html_writer::span(
3371                  $opts->metadata['userloginfail'],
3372                  'meta loginfailures'
3373              );
3374          }
3375  
3376          // MNet.
3377          if (!empty($opts->metadata['asmnetuser'])) {
3378              $mnet = strtolower(preg_replace('#[ ]+#', '-', trim($opts->metadata['mnetidprovidername'])));
3379              $usertextcontents .= html_writer::span(
3380                  $opts->metadata['mnetidprovidername'],
3381                  'meta mnet mnet-' . $mnet
3382              );
3383          }
3384  
3385          $returnstr .= html_writer::span(
3386              html_writer::span($usertextcontents, 'usertext mr-1') .
3387              html_writer::span($avatarcontents, $avatarclasses),
3388              'userbutton'
3389          );
3390  
3391          // Create a divider (well, a filler).
3392          $divider = new action_menu_filler();
3393          $divider->primary = false;
3394  
3395          $am = new action_menu();
3396          $am->set_menu_trigger(
3397              $returnstr
3398          );
3399          $am->set_action_label(get_string('usermenu'));
3400          $am->set_alignment(action_menu::TR, action_menu::BR);
3401          $am->set_nowrap_on_items();
3402          if ($withlinks) {
3403              $navitemcount = count($opts->navitems);
3404              $idx = 0;
3405              foreach ($opts->navitems as $key => $value) {
3406  
3407                  switch ($value->itemtype) {
3408                      case 'divider':
3409                          // If the nav item is a divider, add one and skip link processing.
3410                          $am->add($divider);
3411                          break;
3412  
3413                      case 'invalid':
3414                          // Silently skip invalid entries (should we post a notification?).
3415                          break;
3416  
3417                      case 'link':
3418                          // Process this as a link item.
3419                          $pix = null;
3420                          if (isset($value->pix) && !empty($value->pix)) {
3421                              $pix = new pix_icon($value->pix, '', null, array('class' => 'iconsmall'));
3422                          } else if (isset($value->imgsrc) && !empty($value->imgsrc)) {
3423                              $value->title = html_writer::img(
3424                                  $value->imgsrc,
3425                                  $value->title,
3426                                  array('class' => 'iconsmall')
3427                              ) . $value->title;
3428                          }
3429  
3430                          $al = new action_menu_link_secondary(
3431                              $value->url,
3432                              $pix,
3433                              $value->title,
3434                              array('class' => 'icon')
3435                          );
3436                          if (!empty($value->titleidentifier)) {
3437                              $al->attributes['data-title'] = $value->titleidentifier;
3438                          }
3439                          $am->add($al);
3440                          break;
3441                  }
3442  
3443                  $idx++;
3444  
3445                  // Add dividers after the first item and before the last item.
3446                  if ($idx == 1 || $idx == $navitemcount - 1) {
3447                      $am->add($divider);
3448                  }
3449              }
3450          }
3451  
3452          return html_writer::div(
3453              $this->render($am),
3454              $usermenuclasses
3455          );
3456      }
3457  
3458      /**
3459       * Secure layout login info.
3460       *
3461       * @return string
3462       */
3463      public function secure_layout_login_info() {
3464          if (get_config('core', 'logininfoinsecurelayout')) {
3465              return $this->login_info(false);
3466          } else {
3467              return '';
3468          }
3469      }
3470  
3471      /**
3472       * Returns the language menu in the secure layout.
3473       *
3474       * No custom menu items are passed though, such that it will render only the language selection.
3475       *
3476       * @return string
3477       */
3478      public function secure_layout_language_menu() {
3479          if (get_config('core', 'langmenuinsecurelayout')) {
3480              $custommenu = new custom_menu('', current_language());
3481              return $this->render_custom_menu($custommenu);
3482          } else {
3483              return '';
3484          }
3485      }
3486  
3487      /**
3488       * This renders the navbar.
3489       * Uses bootstrap compatible html.
3490       */
3491      public function navbar() {
3492          return $this->render_from_template('core/navbar', $this->page->navbar);
3493      }
3494  
3495      /**
3496       * Renders a breadcrumb navigation node object.
3497       *
3498       * @param breadcrumb_navigation_node $item The navigation node to render.
3499       * @return string HTML fragment
3500       */
3501      protected function render_breadcrumb_navigation_node(breadcrumb_navigation_node $item) {
3502  
3503          if ($item->action instanceof moodle_url) {
3504              $content = $item->get_content();
3505              $title = $item->get_title();
3506              $attributes = array();
3507              $attributes['itemprop'] = 'url';
3508              if ($title !== '') {
3509                  $attributes['title'] = $title;
3510              }
3511              if ($item->hidden) {
3512                  $attributes['class'] = 'dimmed_text';
3513              }
3514              if ($item->is_last()) {
3515                  $attributes['aria-current'] = 'page';
3516              }
3517              $content = html_writer::tag('span', $content, array('itemprop' => 'title'));
3518              $content = html_writer::link($item->action, $content, $attributes);
3519  
3520              $attributes = array();
3521              $attributes['itemscope'] = '';
3522              $attributes['itemtype'] = 'http://data-vocabulary.org/Breadcrumb';
3523              $content = html_writer::tag('span', $content, $attributes);
3524  
3525          } else {
3526              $content = $this->render_navigation_node($item);
3527          }
3528          return $content;
3529      }
3530  
3531      /**
3532       * Renders a navigation node object.
3533       *
3534       * @param navigation_node $item The navigation node to render.
3535       * @return string HTML fragment
3536       */
3537      protected function render_navigation_node(navigation_node $item) {
3538          $content = $item->get_content();
3539          $title = $item->get_title();
3540          if ($item->icon instanceof renderable && !$item->hideicon) {
3541              $icon = $this->render($item->icon);
3542              $content = $icon.$content; // use CSS for spacing of icons
3543          }
3544          if ($item->helpbutton !== null) {
3545              $content = trim($item->helpbutton).html_writer::tag('span', $content, array('class'=>'clearhelpbutton', 'tabindex'=>'0'));
3546          }
3547          if ($content === '') {
3548              return '';
3549          }
3550          if ($item->action instanceof action_link) {
3551              $link = $item->action;
3552              if ($item->hidden) {
3553                  $link->add_class('dimmed');
3554              }
3555              if (!empty($content)) {
3556                  // Providing there is content we will use that for the link content.
3557                  $link->text = $content;
3558              }
3559              $content = $this->render($link);
3560          } else if ($item->action instanceof moodle_url) {
3561              $attributes = array();
3562              if ($title !== '') {
3563                  $attributes['title'] = $title;
3564              }
3565              if ($item->hidden) {
3566                  $attributes['class'] = 'dimmed_text';
3567              }
3568              $content = html_writer::link($item->action, $content, $attributes);
3569  
3570          } else if (is_string($item->action) || empty($item->action)) {
3571              $attributes = array('tabindex'=>'0'); //add tab support to span but still maintain character stream sequence.
3572              if ($title !== '') {
3573                  $attributes['title'] = $title;
3574              }
3575              if ($item->hidden) {
3576                  $attributes['class'] = 'dimmed_text';
3577              }
3578              $content = html_writer::tag('span', $content, $attributes);
3579          }
3580          return $content;
3581      }
3582  
3583      /**
3584       * Accessibility: Right arrow-like character is
3585       * used in the breadcrumb trail, course navigation menu
3586       * (previous/next activity), calendar, and search forum block.
3587       * If the theme does not set characters, appropriate defaults
3588       * are set automatically. Please DO NOT
3589       * use &lt; &gt; &raquo; - these are confusing for blind users.
3590       *
3591       * @return string
3592       */
3593      public function rarrow() {
3594          return $this->page->theme->rarrow;
3595      }
3596  
3597      /**
3598       * Accessibility: Left arrow-like character is
3599       * used in the breadcrumb trail, course navigation menu
3600       * (previous/next activity), calendar, and search forum block.
3601       * If the theme does not set characters, appropriate defaults
3602       * are set automatically. Please DO NOT
3603       * use &lt; &gt; &raquo; - these are confusing for blind users.
3604       *
3605       * @return string
3606       */
3607      public function larrow() {
3608          return $this->page->theme->larrow;
3609      }
3610  
3611      /**
3612       * Accessibility: Up arrow-like character is used in
3613       * the book heirarchical navigation.
3614       * If the theme does not set characters, appropriate defaults
3615       * are set automatically. Please DO NOT
3616       * use ^ - this is confusing for blind users.
3617       *
3618       * @return string
3619       */
3620      public function uarrow() {
3621          return $this->page->theme->uarrow;
3622      }
3623  
3624      /**
3625       * Accessibility: Down arrow-like character.
3626       * If the theme does not set characters, appropriate defaults
3627       * are set automatically.
3628       *
3629       * @return string
3630       */
3631      public function darrow() {
3632          return $this->page->theme->darrow;
3633      }
3634  
3635      /**
3636       * Returns the custom menu if one has been set
3637       *
3638       * A custom menu can be configured by browsing to
3639       *    Settings: Administration > Appearance > Themes > Theme settings
3640       * and then configuring the custommenu config setting as described.
3641       *
3642       * Theme developers: DO NOT OVERRIDE! Please override function
3643       * {@link core_renderer::render_custom_menu()} instead.
3644       *
3645       * @param string $custommenuitems - custom menuitems set by theme instead of global theme settings
3646       * @return string
3647       */
3648      public function custom_menu($custommenuitems = '') {
3649          global $CFG;
3650  
3651          if (empty($custommenuitems) && !empty($CFG->custommenuitems)) {
3652              $custommenuitems = $CFG->custommenuitems;
3653          }
3654          $custommenu = new custom_menu($custommenuitems, current_language());
3655          return $this->render_custom_menu($custommenu);
3656      }
3657  
3658      /**
3659       * We want to show the custom menus as a list of links in the footer on small screens.
3660       * Just return the menu object exported so we can render it differently.
3661       */
3662      public function custom_menu_flat() {
3663          global $CFG;
3664          $custommenuitems = '';
3665  
3666          if (empty($custommenuitems) && !empty($CFG->custommenuitems)) {
3667              $custommenuitems = $CFG->custommenuitems;
3668          }
3669          $custommenu = new custom_menu($custommenuitems, current_language());
3670          $langs = get_string_manager()->get_list_of_translations();
3671          $haslangmenu = $this->lang_menu() != '';
3672  
3673          if ($haslangmenu) {
3674              $strlang = get_string('language');
3675              $currentlang = current_language();
3676              if (isset($langs[$currentlang])) {
3677                  $currentlang = $langs[$currentlang];
3678              } else {
3679                  $currentlang = $strlang;
3680              }
3681              $this->language = $custommenu->add($currentlang, new moodle_url('#'), $strlang, 10000);
3682              foreach ($langs as $langtype => $langname) {
3683                  $this->language->add($langname, new moodle_url($this->page->url, array('lang' => $langtype)), $langname);
3684              }
3685          }
3686  
3687          return $custommenu->export_for_template($this);
3688      }
3689  
3690      /**
3691       * Renders a custom menu object (located in outputcomponents.php)
3692       *
3693       * The custom menu this method produces makes use of the YUI3 menunav widget
3694       * and requires very specific html elements and classes.
3695       *
3696       * @staticvar int $menucount
3697       * @param custom_menu $menu
3698       * @return string
3699       */
3700      protected function render_custom_menu(custom_menu $menu) {
3701          global $CFG;
3702  
3703          $langs = get_string_manager()->get_list_of_translations();
3704          $haslangmenu = $this->lang_menu() != '';
3705  
3706          if (!$menu->has_children() && !$haslangmenu) {
3707              return '';
3708          }
3709  
3710          if ($haslangmenu) {
3711              $strlang = get_string('language');
3712              $currentlang = current_language();
3713              if (isset($langs[$currentlang])) {
3714                  $currentlang = $langs[$currentlang];
3715              } else {
3716                  $currentlang = $strlang;
3717              }
3718              $this->language = $menu->add($currentlang, new moodle_url('#'), $strlang, 10000);
3719              foreach ($langs as $langtype => $langname) {
3720                  $this->language->add($langname, new moodle_url($this->page->url, array('lang' => $langtype)), $langname);
3721              }
3722          }
3723  
3724          $content = '';
3725          foreach ($menu->get_children() as $item) {
3726              $context = $item->export_for_template($this);
3727              $content .= $this->render_from_template('core/custom_menu_item', $context);
3728          }
3729  
3730          return $content;
3731      }
3732  
3733      /**
3734       * Renders a custom menu node as part of a submenu
3735       *
3736       * The custom menu this method produces makes use of the YUI3 menunav widget
3737       * and requires very specific html elements and classes.
3738       *
3739       * @see core:renderer::render_custom_menu()
3740       *
3741       * @staticvar int $submenucount
3742       * @param custom_menu_item $menunode
3743       * @return string
3744       */
3745      protected function render_custom_menu_item(custom_menu_item $menunode) {
3746          // Required to ensure we get unique trackable id's
3747          static $submenucount = 0;
3748          if ($menunode->has_children()) {
3749              // If the child has menus render it as a sub menu
3750              $submenucount++;
3751              $content = html_writer::start_tag('li');
3752              if ($menunode->get_url() !== null) {
3753                  $url = $menunode->get_url();
3754              } else {
3755                  $url = '#cm_submenu_'.$submenucount;
3756              }
3757              $content .= html_writer::link($url, $menunode->get_text(), array('class'=>'yui3-menu-label', 'title'=>$menunode->get_title()));
3758              $content .= html_writer::start_tag('div', array('id'=>'cm_submenu_'.$submenucount, 'class'=>'yui3-menu custom_menu_submenu'));
3759              $content .= html_writer::start_tag('div', array('class'=>'yui3-menu-content'));
3760              $content .= html_writer::start_tag('ul');
3761              foreach ($menunode->get_children() as $menunode) {
3762                  $content .= $this->render_custom_menu_item($menunode);
3763              }
3764              $content .= html_writer::end_tag('ul');
3765              $content .= html_writer::end_tag('div');
3766              $content .= html_writer::end_tag('div');
3767              $content .= html_writer::end_tag('li');
3768          } else {
3769              // The node doesn't have children so produce a final menuitem.
3770              // Also, if the node's text matches '####', add a class so we can treat it as a divider.
3771              $content = '';
3772              if (preg_match("/^#+$/", $menunode->get_text())) {
3773  
3774                  // This is a divider.
3775                  $content = html_writer::start_tag('li', array('class' => 'yui3-menuitem divider'));
3776              } else {
3777                  $content = html_writer::start_tag(
3778                      'li',
3779                      array(
3780                          'class' => 'yui3-menuitem'
3781                      )
3782                  );
3783                  if ($menunode->get_url() !== null) {
3784                      $url = $menunode->get_url();
3785                  } else {
3786                      $url = '#';
3787                  }
3788                  $content .= html_writer::link(
3789                      $url,
3790                      $menunode->get_text(),
3791                      array('class' => 'yui3-menuitem-content', 'title' => $menunode->get_title())
3792                  );
3793              }
3794              $content .= html_writer::end_tag('li');
3795          }
3796          // Return the sub menu
3797          return $content;
3798      }
3799  
3800      /**
3801       * Renders theme links for switching between default and other themes.
3802       *
3803       * @return string
3804       */
3805      protected function theme_switch_links() {
3806  
3807          $actualdevice = core_useragent::get_device_type();
3808          $currentdevice = $this->page->devicetypeinuse;
3809          $switched = ($actualdevice != $currentdevice);
3810  
3811          if (!$switched && $currentdevice == 'default' && $actualdevice == 'default') {
3812              // The user is using the a default device and hasn't switched so don't shown the switch
3813              // device links.
3814              return '';
3815          }
3816  
3817          if ($switched) {
3818              $linktext = get_string('switchdevicerecommended');
3819              $devicetype = $actualdevice;
3820          } else {
3821              $linktext = get_string('switchdevicedefault');
3822              $devicetype = 'default';
3823          }
3824          $linkurl = new moodle_url('/theme/switchdevice.php', array('url' => $this->page->url, 'device' => $devicetype, 'sesskey' => sesskey()));
3825  
3826          $content  = html_writer::start_tag('div', array('id' => 'theme_switch_link'));
3827          $content .= html_writer::link($linkurl, $linktext, array('rel' => 'nofollow'));
3828          $content .= html_writer::end_tag('div');
3829  
3830          return $content;
3831      }
3832  
3833      /**
3834       * Renders tabs
3835       *
3836       * This function replaces print_tabs() used before Moodle 2.5 but with slightly different arguments
3837       *
3838       * Theme developers: In order to change how tabs are displayed please override functions
3839       * {@link core_renderer::render_tabtree()} and/or {@link core_renderer::render_tabobject()}
3840       *
3841       * @param array $tabs array of tabs, each of them may have it's own ->subtree
3842       * @param string|null $selected which tab to mark as selected, all parent tabs will
3843       *     automatically be marked as activated
3844       * @param array|string|null $inactive list of ids of inactive tabs, regardless of
3845       *     their level. Note that you can as weel specify tabobject::$inactive for separate instances
3846       * @return string
3847       */
3848      public final function tabtree($tabs, $selected = null, $inactive = null) {
3849          return $this->render(new tabtree($tabs, $selected, $inactive));
3850      }
3851  
3852      /**
3853       * Renders tabtree
3854       *
3855       * @param tabtree $tabtree
3856       * @return string
3857       */
3858      protected function render_tabtree(tabtree $tabtree) {
3859          if (empty($tabtree->subtree)) {
3860              return '';
3861          }
3862          $data = $tabtree->export_for_template($this);
3863          return $this->render_from_template('core/tabtree', $data);
3864      }
3865  
3866      /**
3867       * Renders tabobject (part of tabtree)
3868       *
3869       * This function is called from {@link core_renderer::render_tabtree()}
3870       * and also it calls itself when printing the $tabobject subtree recursively.
3871       *
3872       * Property $tabobject->level indicates the number of row of tabs.
3873       *
3874       * @param tabobject $tabobject
3875       * @return string HTML fragment
3876       */
3877      protected function render_tabobject(tabobject $tabobject) {
3878          $str = '';
3879  
3880          // Print name of the current tab.
3881          if ($tabobject instanceof tabtree) {
3882              // No name for tabtree root.
3883          } else if ($tabobject->inactive || $tabobject->activated || ($tabobject->selected && !$tabobject->linkedwhenselected)) {
3884              // Tab name without a link. The <a> tag is used for styling.
3885              $str .= html_writer::tag('a', html_writer::span($tabobject->text), array('class' => 'nolink moodle-has-zindex'));
3886          } else {
3887              // Tab name with a link.
3888              if (!($tabobject->link instanceof moodle_url)) {
3889                  // backward compartibility when link was passed as quoted string
3890                  $str .= "<a href=\"$tabobject->link\" title=\"$tabobject->title\"><span>$tabobject->text</span></a>";
3891              } else {
3892                  $str .= html_writer::link($tabobject->link, html_writer::span($tabobject->text), array('title' => $tabobject->title));
3893              }
3894          }
3895  
3896          if (empty($tabobject->subtree)) {
3897              if ($tabobject->selected) {
3898                  $str .= html_writer::tag('div', '&nbsp;', array('class' => 'tabrow'. ($tabobject->level + 1). ' empty'));
3899              }
3900              return $str;
3901          }
3902  
3903          // Print subtree.
3904          if ($tabobject->level == 0 || $tabobject->selected || $tabobject->activated) {
3905              $str .= html_writer::start_tag('ul', array('class' => 'tabrow'. $tabobject->level));
3906              $cnt = 0;
3907              foreach ($tabobject->subtree as $tab) {
3908                  $liclass = '';
3909                  if (!$cnt) {
3910                      $liclass .= ' first';
3911                  }
3912                  if ($cnt == count($tabobject->subtree) - 1) {
3913                      $liclass .= ' last';
3914                  }
3915                  if ((empty($tab->subtree)) && (!empty($tab->selected))) {
3916                      $liclass .= ' onerow';
3917                  }
3918  
3919                  if ($tab->selected) {
3920                      $liclass .= ' here selected';
3921                  } else if ($tab->activated) {
3922                      $liclass .= ' here active';
3923                  }
3924  
3925                  // This will recursively call function render_tabobject() for each item in subtree.
3926                  $str .= html_writer::tag('li', $this->render($tab), array('class' => trim($liclass)));
3927                  $cnt++;
3928              }
3929              $str .= html_writer::end_tag('ul');
3930          }
3931  
3932          return $str;
3933      }
3934  
3935      /**
3936       * Get the HTML for blocks in the given region.
3937       *
3938       * @since Moodle 2.5.1 2.6
3939       * @param string $region The region to get HTML for.
3940       * @return string HTML.
3941       */
3942      public function blocks($region, $classes = array(), $tag = 'aside') {
3943          $displayregion = $this->page->apply_theme_region_manipulations($region);
3944          $classes = (array)$classes;
3945          $classes[] = 'block-region';
3946          $attributes = array(
3947              'id' => 'block-region-'.preg_replace('#[^a-zA-Z0-9_\-]+#', '-', $displayregion),
3948              'class' => join(' ', $classes),
3949              'data-blockregion' => $displayregion,
3950              'data-droptarget' => '1'
3951          );
3952          if ($this->page->blocks->region_has_content($displayregion, $this)) {
3953              $content = $this->blocks_for_region($displayregion);
3954          } else {
3955              $content = '';
3956          }
3957          return html_writer::tag($tag, $content, $attributes);
3958      }
3959  
3960      /**
3961       * Renders a custom block region.
3962       *
3963       * Use this method if you want to add an additional block region to the content of the page.
3964       * Please note this should only be used in special situations.
3965       * We want to leave the theme is control where ever possible!
3966       *
3967       * This method must use the same method that the theme uses within its layout file.
3968       * As such it asks the theme what method it is using.
3969       * It can be one of two values, blocks or blocks_for_region (deprecated).
3970       *
3971       * @param string $regionname The name of the custom region to add.
3972       * @return string HTML for the block region.
3973       */
3974      public function custom_block_region($regionname) {
3975          if ($this->page->theme->get_block_render_method() === 'blocks') {
3976              return $this->blocks($regionname);
3977          } else {
3978              return $this->blocks_for_region($regionname);
3979          }
3980      }
3981  
3982      /**
3983       * Returns the CSS classes to apply to the body tag.
3984       *
3985       * @since Moodle 2.5.1 2.6
3986       * @param array $additionalclasses Any additional classes to apply.
3987       * @return string
3988       */
3989      public function body_css_classes(array $additionalclasses = array()) {
3990          return $this->page->bodyclasses . ' ' . implode(' ', $additionalclasses);
3991      }
3992  
3993      /**
3994       * The ID attribute to apply to the body tag.
3995       *
3996       * @since Moodle 2.5.1 2.6
3997       * @return string
3998       */
3999      public function body_id() {
4000          return $this->page->bodyid;
4001      }
4002  
4003      /**
4004       * Returns HTML attributes to use within the body tag. This includes an ID and classes.
4005       *
4006       * @since Moodle 2.5.1 2.6
4007       * @param string|array $additionalclasses Any additional classes to give the body tag,
4008       * @return string
4009       */
4010      public function body_attributes($additionalclasses = array()) {
4011          if (!is_array($additionalclasses)) {
4012              $additionalclasses = explode(' ', $additionalclasses);
4013          }
4014          return ' id="'. $this->body_id().'" class="'.$this->body_css_classes($additionalclasses).'"';
4015      }
4016  
4017      /**
4018       * Gets HTML for the page heading.
4019       *
4020       * @since Moodle 2.5.1 2.6
4021       * @param string $tag The tag to encase the heading in. h1 by default.
4022       * @return string HTML.
4023       */
4024      public function page_heading($tag = 'h1') {
4025          return html_writer::tag($tag, $this->page->heading);
4026      }
4027  
4028      /**
4029       * Gets the HTML for the page heading button.
4030       *
4031       * @since Moodle 2.5.1 2.6
4032       * @return string HTML.
4033       */
4034      public function page_heading_button() {
4035          return $this->page->button;
4036      }
4037  
4038      /**
4039       * Returns the Moodle docs link to use for this page.
4040       *
4041       * @since Moodle 2.5.1 2.6
4042       * @param string $text
4043       * @return string
4044       */
4045      public function page_doc_link($text = null) {
4046          if ($text === null) {
4047              $text = get_string('moodledocslink');
4048          }
4049          $path = page_get_doc_link_path($this->page);
4050          if (!$path) {
4051              return '';
4052          }
4053          return $this->doc_link($path, $text);
4054      }
4055  
4056      /**
4057       * Returns the page heading menu.
4058       *
4059       * @since Moodle 2.5.1 2.6
4060       * @return string HTML.
4061       */
4062      public function page_heading_menu() {
4063          return $this->page->headingmenu;
4064      }
4065  
4066      /**
4067       * Returns the title to use on the page.
4068       *
4069       * @since Moodle 2.5.1 2.6
4070       * @return string
4071       */
4072      public function page_title() {
4073          return $this->page->title;
4074      }
4075  
4076      /**
4077       * Returns the moodle_url for the favicon.
4078       *
4079       * @since Moodle 2.5.1 2.6
4080       * @return moodle_url The moodle_url for the favicon
4081       */
4082      public function favicon() {
4083          return $this->image_url('favicon', 'theme');
4084      }
4085  
4086      /**
4087       * Renders preferences groups.
4088       *
4089       * @param  preferences_groups $renderable The renderable
4090       * @return string The output.
4091       */
4092      public function render_preferences_groups(preferences_groups $renderable) {
4093          return $this->render_from_template('core/preferences_groups', $renderable);
4094      }
4095  
4096      /**
4097       * Renders preferences group.
4098       *
4099       * @param  preferences_group $renderable The renderable
4100       * @return string The output.
4101       */
4102      public function render_preferences_group(preferences_group $renderable) {
4103          $html = '';
4104          $html .= html_writer::start_tag('div', array('class' => 'col-sm-4 preferences-group'));
4105          $html .= $this->heading($renderable->title, 3);
4106          $html .= html_writer::start_tag('ul');
4107          foreach ($renderable->nodes as $node) {
4108              if ($node->has_children()) {
4109                  debugging('Preferences nodes do not support children', DEBUG_DEVELOPER);
4110              }
4111              $html .= html_writer::tag('li', $this->render($node));
4112          }
4113          $html .= html_writer::end_tag('ul');
4114          $html .= html_writer::end_tag('div');
4115          return $html;
4116      }
4117  
4118      public function context_header($headerinfo = null, $headinglevel = 1) {
4119          global $DB, $USER, $CFG, $SITE;
4120          require_once($CFG->dirroot . '/user/lib.php');
4121          $context = $this->page->context;
4122          $heading = null;
4123          $imagedata = null;
4124          $subheader = null;
4125          $userbuttons = null;
4126  
4127          // Make sure to use the heading if it has been set.
4128          if (isset($headerinfo['heading'])) {
4129              $heading = $headerinfo['heading'];
4130          } else {
4131              $heading = $this->page->heading;
4132          }
4133  
4134          // The user context currently has images and buttons. Other contexts may follow.
4135          if (isset($headerinfo['user']) || $context->contextlevel == CONTEXT_USER) {
4136              if (isset($headerinfo['user'])) {
4137                  $user = $headerinfo['user'];
4138              } else {
4139                  // Look up the user information if it is not supplied.
4140                  $user = $DB->get_record('user', array('id' => $context->instanceid));
4141              }
4142  
4143              // If the user context is set, then use that for capability checks.
4144              if (isset($headerinfo['usercontext'])) {
4145                  $context = $headerinfo['usercontext'];
4146              }
4147  
4148              // Only provide user information if the user is the current user, or a user which the current user can view.
4149              // When checking user_can_view_profile(), either:
4150              // If the page context is course, check the course context (from the page object) or;
4151              // If page context is NOT course, then check across all courses.
4152              $course = ($this->page->context->contextlevel == CONTEXT_COURSE) ? $this->page->course : null;
4153  
4154              if (user_can_view_profile($user, $course)) {
4155                  // Use the user's full name if the heading isn't set.
4156                  if (empty($heading)) {
4157                      $heading = fullname($user);
4158                  }
4159  
4160                  $imagedata = $this->user_picture($user, array('size' => 100));
4161  
4162                  // Check to see if we should be displaying a message button.
4163                  if (!empty($CFG->messaging) && has_capability('moodle/site:sendmessage', $context)) {
4164                      $userbuttons = array(
4165                          'messages' => array(
4166                              'buttontype' => 'message',
4167                              'title' => get_string('message', 'message'),
4168                              'url' => new moodle_url('/message/index.php', array('id' => $user->id)),
4169                              'image' => 'message',
4170                              'linkattributes' => \core_message\helper::messageuser_link_params($user->id),
4171                              'page' => $this->page
4172                          )
4173                      );
4174  
4175                      if ($USER->id != $user->id) {
4176                          $iscontact = \core_message\api::is_contact($USER->id, $user->id);
4177                          $contacttitle = $iscontact ? 'removefromyourcontacts' : 'addtoyourcontacts';
4178                          $contacturlaction = $iscontact ? 'removecontact' : 'addcontact';
4179                          $contactimage = $iscontact ? 'removecontact' : 'addcontact';
4180                          $userbuttons['togglecontact'] = array(
4181                                  'buttontype' => 'togglecontact',
4182                                  'title' => get_string($contacttitle, 'message'),
4183                                  'url' => new moodle_url('/message/index.php', array(
4184                                          'user1' => $USER->id,
4185                                          'user2' => $user->id,
4186                                          $contacturlaction => $user->id,
4187                                          'sesskey' => sesskey())
4188                                  ),
4189                                  'image' => $contactimage,
4190                                  'linkattributes' => \core_message\helper::togglecontact_link_params($user, $iscontact),
4191                                  'page' => $this->page
4192                              );
4193                      }
4194  
4195                      $this->page->requires->string_for_js('changesmadereallygoaway', 'moodle');
4196                  }
4197              } else {
4198                  $heading = null;
4199              }
4200          }
4201  
4202          if ($this->should_display_main_logo($headinglevel)) {
4203              $sitename = format_string($SITE->fullname, true, ['context' => context_course::instance(SITEID)]);
4204              // Logo.
4205              $html = html_writer::div(
4206                  html_writer::empty_tag('img', [
4207                      'src' => $this->get_logo_url(null, 150),
4208                      'alt' => get_string('logoof', '', $sitename),
4209                      'class' => 'img-fluid'
4210                  ]),
4211                  'logo'
4212              );
4213              // Heading.
4214              if (!isset($heading)) {
4215                  $html .= $this->heading($this->page->heading, $headinglevel, 'sr-only');
4216              } else {
4217                  $html .= $this->heading($heading, $headinglevel, 'sr-only');
4218              }
4219              return $html;
4220          }
4221  
4222          $contextheader = new context_header($heading, $headinglevel, $imagedata, $userbuttons);
4223          return $this->render_context_header($contextheader);
4224      }
4225  
4226      /**
4227       * Renders the skip links for the page.
4228       *
4229       * @param array $links List of skip links.
4230       * @return string HTML for the skip links.
4231       */
4232      public function render_skip_links($links) {
4233          $context = [ 'links' => []];
4234  
4235          foreach ($links as $url => $text) {
4236              $context['links'][] = [ 'url' => $url, 'text' => $text];
4237          }
4238  
4239          return $this->render_from_template('core/skip_links', $context);
4240      }
4241  
4242       /**
4243        * Renders the header bar.
4244        *
4245        * @param context_header $contextheader Header bar object.
4246        * @return string HTML for the header bar.
4247        */
4248      protected function render_context_header(context_header $contextheader) {
4249  
4250          // Generate the heading first and before everything else as we might have to do an early return.
4251          if (!isset($contextheader->heading)) {
4252              $heading = $this->heading($this->page->heading, $contextheader->headinglevel);
4253          } else {
4254              $heading = $this->heading($contextheader->heading, $contextheader->headinglevel);
4255          }
4256  
4257          $showheader = empty($this->page->layout_options['nocontextheader']);
4258          if (!$showheader) {
4259              // Return the heading wrapped in an sr-only element so it is only visible to screen-readers.
4260              return html_writer::div($heading, 'sr-only');
4261          }
4262  
4263          // All the html stuff goes here.
4264          $html = html_writer::start_div('page-context-header');
4265  
4266          // Image data.
4267          if (isset($contextheader->imagedata)) {
4268              // Header specific image.
4269              $html .= html_writer::div($contextheader->imagedata, 'page-header-image');
4270          }
4271  
4272          // Headings.
4273          $html .= html_writer::tag('div', $heading, array('class' => 'page-header-headings'));
4274  
4275          // Buttons.
4276          if (isset($contextheader->additionalbuttons)) {
4277              $html .= html_writer::start_div('btn-group header-button-group');
4278              foreach ($contextheader->additionalbuttons as $button) {
4279                  if (!isset($button->page)) {
4280                      // Include js for messaging.
4281                      if ($button['buttontype'] === 'togglecontact') {
4282                          \core_message\helper::togglecontact_requirejs();
4283                      }
4284                      if ($button['buttontype'] === 'message') {
4285                          \core_message\helper::messageuser_requirejs();
4286                      }
4287                      $image = $this->pix_icon($button['formattedimage'], $button['title'], 'moodle', array(
4288                          'class' => 'iconsmall',
4289                          'role' => 'presentation'
4290                      ));
4291                      $image .= html_writer::span($button['title'], 'header-button-title');
4292                  } else {
4293                      $image = html_writer::empty_tag('img', array(
4294                          'src' => $button['formattedimage'],
4295                          'role' => 'presentation'
4296                      ));
4297                  }
4298                  $html .= html_writer::link($button['url'], html_writer::tag('span', $image), $button['linkattributes']);
4299              }
4300              $html .= html_writer::end_div();
4301          }
4302          $html .= html_writer::end_div();
4303  
4304          return $html;
4305      }
4306  
4307      /**
4308       * Wrapper for header elements.
4309       *
4310       * @return string HTML to display the main header.
4311       */
4312      public function full_header() {
4313  
4314          if ($this->page->include_region_main_settings_in_header_actions() &&
4315                  !$this->page->blocks->is_block_present('settings')) {
4316              // Only include the region main settings if the page has requested it and it doesn't already have
4317              // the settings block on it. The region main settings are included in the settings block and
4318              // duplicating the content causes behat failures.
4319              $this->page->add_header_action(html_writer::div(
4320                  $this->region_main_settings_menu(),
4321                  'd-print-none',
4322                  ['id' => 'region-main-settings-menu']
4323              ));
4324          }
4325  
4326          $header = new stdClass();
4327          $header->settingsmenu = $this->context_header_settings_menu();
4328          $header->contextheader = $this->context_header();
4329          $header->hasnavbar = empty($this->page->layout_options['nonavbar']);
4330          $header->navbar = $this->navbar();
4331          $header->pageheadingbutton = $this->page_heading_button();
4332          $header->courseheader = $this->course_header();
4333          $header->headeractions = $this->page->get_header_actions();
4334          return $this->render_from_template('core/full_header', $header);
4335      }
4336  
4337      /**
4338       * This is an optional menu that can be added to a layout by a theme. It contains the
4339       * menu for the course administration, only on the course main page.
4340       *
4341       * @return string
4342       */
4343      public function context_header_settings_menu() {
4344          $context = $this->page->context;
4345          $menu = new action_menu();
4346  
4347          $items = $this->page->navbar->get_items();
4348          $currentnode = end($items);
4349  
4350          $showcoursemenu = false;
4351          $showfrontpagemenu = false;
4352          $showusermenu = false;
4353  
4354          // We are on the course home page.
4355          if (($context->contextlevel == CONTEXT_COURSE) &&
4356                  !empty($currentnode) &&
4357                  ($currentnode->type == navigation_node::TYPE_COURSE || $currentnode->type == navigation_node::TYPE_SECTION)) {
4358              $showcoursemenu = true;
4359          }
4360  
4361          $courseformat = course_get_format($this->page->course);
4362          // This is a single activity course format, always show the course menu on the activity main page.
4363          if ($context->contextlevel == CONTEXT_MODULE &&
4364                  !$courseformat->has_view_page()) {
4365  
4366              $this->page->navigation->initialise();
4367              $activenode = $this->page->navigation->find_active_node();
4368              // If the settings menu has been forced then show the menu.
4369              if ($this->page->is_settings_menu_forced()) {
4370                  $showcoursemenu = true;
4371              } else if (!empty($activenode) && ($activenode->type == navigation_node::TYPE_ACTIVITY ||
4372                              $activenode->type == navigation_node::TYPE_RESOURCE)) {
4373  
4374                  // We only want to show the menu on the first page of the activity. This means
4375                  // the breadcrumb has no additional nodes.
4376                  if ($currentnode && ($currentnode->key == $activenode->key && $currentnode->type == $activenode->type)) {
4377                      $showcoursemenu = true;
4378                  }
4379              }
4380          }
4381  
4382          // This is the site front page.
4383          if ($context->contextlevel == CONTEXT_COURSE &&
4384                  !empty($currentnode) &&
4385                  $currentnode->key === 'home') {
4386              $showfrontpagemenu = true;
4387          }
4388  
4389          // This is the user profile page.
4390          if ($context->contextlevel == CONTEXT_USER &&
4391                  !empty($currentnode) &&
4392                  ($currentnode->key === 'myprofile')) {
4393              $showusermenu = true;
4394          }
4395  
4396          if ($showfrontpagemenu) {
4397              $settingsnode = $this->page->settingsnav->find('frontpage', navigation_node::TYPE_SETTING);
4398              if ($settingsnode) {
4399                  // Build an action menu based on the visible nodes from this navigation tree.
4400                  $skipped = $this->build_action_menu_from_navigation($menu, $settingsnode, false, true);
4401  
4402                  // We only add a list to the full settings menu if we didn't include every node in the short menu.
4403                  if ($skipped) {
4404                      $text = get_string('morenavigationlinks');
4405                      $url = new moodle_url('/course/admin.php', array('courseid' => $this->page->course->id));
4406                      $link = new action_link($url, $text, null, null, new pix_icon('t/edit', $text));
4407                      $menu->add_secondary_action($link);
4408                  }
4409              }
4410          } else if ($showcoursemenu) {
4411              $settingsnode = $this->page->settingsnav->find('courseadmin', navigation_node::TYPE_COURSE);
4412              if ($settingsnode) {
4413                  // Build an action menu based on the visible nodes from this navigation tree.
4414                  $skipped = $this->build_action_menu_from_navigation($menu, $settingsnode, false, true);
4415  
4416                  // We only add a list to the full settings menu if we didn't include every node in the short menu.
4417                  if ($skipped) {
4418                      $text = get_string('morenavigationlinks');
4419                      $url = new moodle_url('/course/admin.php', array('courseid' => $this->page->course->id));
4420                      $link = new action_link($url, $text, null, null, new pix_icon('t/edit', $text));
4421                      $menu->add_secondary_action($link);
4422                  }
4423              }
4424          } else if ($showusermenu) {
4425              // Get the course admin node from the settings navigation.
4426              $settingsnode = $this->page->settingsnav->find('useraccount', navigation_node::TYPE_CONTAINER);
4427              if ($settingsnode) {
4428                  // Build an action menu based on the visible nodes from this navigation tree.
4429                  $this->build_action_menu_from_navigation($menu, $settingsnode);
4430              }
4431          }
4432  
4433          return $this->render($menu);
4434      }
4435  
4436      /**
4437       * Take a node in the nav tree and make an action menu out of it.
4438       * The links are injected in the action menu.
4439       *
4440       * @param action_menu $menu
4441       * @param navigation_node $node
4442       * @param boolean $indent
4443       * @param boolean $onlytopleafnodes
4444       * @return boolean nodesskipped - True if nodes were skipped in building the menu
4445       */
4446      protected function build_action_menu_from_navigation(action_menu $menu,
4447              navigation_node $node,
4448              $indent = false,
4449              $onlytopleafnodes = false) {
4450          $skipped = false;
4451          // Build an action menu based on the visible nodes from this navigation tree.
4452          foreach ($node->children as $menuitem) {
4453              if ($menuitem->display) {
4454                  if ($onlytopleafnodes && $menuitem->children->count()) {
4455                      $skipped = true;
4456                      continue;
4457                  }
4458                  if ($menuitem->action) {
4459                      if ($menuitem->action instanceof action_link) {
4460                          $link = $menuitem->action;
4461                          // Give preference to setting icon over action icon.
4462                          if (!empty($menuitem->icon)) {
4463                              $link->icon = $menuitem->icon;
4464                          }
4465                      } else {
4466                          $link = new action_link($menuitem->action, $menuitem->text, null, null, $menuitem->icon);
4467                      }
4468                  } else {
4469                      if ($onlytopleafnodes) {
4470                          $skipped = true;
4471                          continue;
4472                      }
4473                      $link = new action_link(new moodle_url('#'), $menuitem->text, null, ['disabled' => true], $menuitem->icon);
4474                  }
4475                  if ($indent) {
4476                      $link->add_class('ml-4');
4477                  }
4478                  if (!empty($menuitem->classes)) {
4479                      $link->add_class(implode(" ", $menuitem->classes));
4480                  }
4481  
4482                  $menu->add_secondary_action($link);
4483                  $skipped = $skipped || $this->build_action_menu_from_navigation($menu, $menuitem, true);
4484              }
4485          }
4486          return $skipped;
4487      }
4488  
4489      /**
4490       * This is an optional menu that can be added to a layout by a theme. It contains the
4491       * menu for the most specific thing from the settings block. E.g. Module administration.
4492       *
4493       * @return string
4494       */
4495      public function region_main_settings_menu() {
4496          $context = $this->page->context;
4497          $menu = new action_menu();
4498  
4499          if ($context->contextlevel == CONTEXT_MODULE) {
4500  
4501              $this->page->navigation->initialise();
4502              $node = $this->page->navigation->find_active_node();
4503              $buildmenu = false;
4504              // If the settings menu has been forced then show the menu.
4505              if ($this->page->is_settings_menu_forced()) {
4506                  $buildmenu = true;
4507              } else if (!empty($node) && ($node->type == navigation_node::TYPE_ACTIVITY ||
4508                              $node->type == navigation_node::TYPE_RESOURCE)) {
4509  
4510                  $items = $this->page->navbar->get_items();
4511                  $navbarnode = end($items);
4512                  // We only want to show the menu on the first page of the activity. This means
4513                  // the breadcrumb has no additional nodes.
4514                  if ($navbarnode && ($navbarnode->key === $node->key && $navbarnode->type == $node->type)) {
4515                      $buildmenu = true;
4516                  }
4517              }
4518              if ($buildmenu) {
4519                  // Get the course admin node from the settings navigation.
4520                  $node = $this->page->settingsnav->find('modulesettings', navigation_node::TYPE_SETTING);
4521                  if ($node) {
4522                      // Build an action menu based on the visible nodes from this navigation tree.
4523                      $this->build_action_menu_from_navigation($menu, $node);
4524                  }
4525              }
4526  
4527          } else if ($context->contextlevel == CONTEXT_COURSECAT) {
4528              // For course category context, show category settings menu, if we're on the course category page.
4529              if ($this->page->pagetype === 'course-index-category') {
4530                  $node = $this->page->settingsnav->find('categorysettings', navigation_node::TYPE_CONTAINER);
4531                  if ($node) {
4532                      // Build an action menu based on the visible nodes from this navigation tree.
4533                      $this->build_action_menu_from_navigation($menu, $node);
4534                  }
4535              }
4536  
4537          } else {
4538              $items = $this->page->navbar->get_items();
4539              $navbarnode = end($items);
4540  
4541              if ($navbarnode && ($navbarnode->key === 'participants')) {
4542                  $node = $this->page->settingsnav->find('users', navigation_node::TYPE_CONTAINER);
4543                  if ($node) {
4544                      // Build an action menu based on the visible nodes from this navigation tree.
4545                      $this->build_action_menu_from_navigation($menu, $node);
4546                  }
4547  
4548              }
4549          }
4550          return $this->render($menu);
4551      }
4552  
4553      /**
4554       * Displays the list of tags associated with an entry
4555       *
4556       * @param array $tags list of instances of core_tag or stdClass
4557       * @param string $label label to display in front, by default 'Tags' (get_string('tags')), set to null
4558       *               to use default, set to '' (empty string) to omit the label completely
4559       * @param string $classes additional classes for the enclosing div element
4560       * @param int $limit limit the number of tags to display, if size of $tags is more than this limit the "more" link
4561       *               will be appended to the end, JS will toggle the rest of the tags
4562       * @param context $pagecontext specify if needed to overwrite the current page context for the view tag link
4563       * @param bool $accesshidelabel if true, the label should have class="accesshide" added.
4564       * @return string
4565       */
4566      public function tag_list($tags, $label = null, $classes = '', $limit = 10,
4567              $pagecontext = null, $accesshidelabel = false) {
4568          $list = new \core_tag\output\taglist($tags, $label, $classes, $limit, $pagecontext, $accesshidelabel);
4569          return $this->render_from_template('core_tag/taglist', $list->export_for_template($this));
4570      }
4571  
4572      /**
4573       * Renders element for inline editing of any value
4574       *
4575       * @param \core\output\inplace_editable $element
4576       * @return string
4577       */
4578      public function render_inplace_editable(\core\output\inplace_editable $element) {
4579          return $this->render_from_template('core/inplace_editable', $element->export_for_template($this));
4580      }
4581  
4582      /**
4583       * Renders a bar chart.
4584       *
4585       * @param \core\chart_bar $chart The chart.
4586       * @return string.
4587       */
4588      public function render_chart_bar(\core\chart_bar $chart) {
4589          return $this->render_chart($chart);
4590      }
4591  
4592      /**
4593       * Renders a line chart.
4594       *
4595       * @param \core\chart_line $chart The chart.
4596       * @return string.
4597       */
4598      public function render_chart_line(\core\chart_line $chart) {
4599          return $this->render_chart($chart);
4600      }
4601  
4602      /**
4603       * Renders a pie chart.
4604       *
4605       * @param \core\chart_pie $chart The chart.
4606       * @return string.
4607       */
4608      public function render_chart_pie(\core\chart_pie $chart) {
4609          return $this->render_chart($chart);
4610      }
4611  
4612      /**
4613       * Renders a chart.
4614       *
4615       * @param \core\chart_base $chart The chart.
4616       * @param bool $withtable Whether to include a data table with the chart.
4617       * @return string.
4618       */
4619      public function render_chart(\core\chart_base $chart, $withtable = true) {
4620          $chartdata = json_encode($chart);
4621          return $this->render_from_template('core/chart', (object) [
4622              'chartdata' => $chartdata,
4623              'withtable' => $withtable
4624          ]);
4625      }
4626  
4627      /**
4628       * Renders the login form.
4629       *
4630       * @param \core_auth\output\login $form The renderable.
4631       * @return string
4632       */
4633      public function render_login(\core_auth\output\login $form) {
4634          global $CFG, $SITE;
4635  
4636          $context = $form->export_for_template($this);
4637  
4638          // Override because rendering is not supported in template yet.
4639          if ($CFG->rememberusername == 0) {
4640              $context->cookieshelpiconformatted = $this->help_icon('cookiesenabledonlysession');
4641          } else {
4642              $context->cookieshelpiconformatted = $this->help_icon('cookiesenabled');
4643          }
4644          $context->errorformatted = $this->error_text($context->error);
4645          $url = $this->get_logo_url();
4646          if ($url) {
4647              $url = $url->out(false);
4648          }
4649          $context->logourl = $url;
4650          $context->sitename = format_string($SITE->fullname, true,
4651                  ['context' => context_course::instance(SITEID), "escape" => false]);
4652  
4653          return $this->render_from_template('core/loginform', $context);
4654      }
4655  
4656      /**
4657       * Renders an mform element from a template.
4658       *
4659       * @param HTML_QuickForm_element $element element
4660       * @param bool $required if input is required field
4661       * @param bool $advanced if input is an advanced field
4662       * @param string $error error message to display
4663       * @param bool $ingroup True if this element is rendered as part of a group
4664       * @return mixed string|bool
4665       */
4666      public function mform_element($element, $required, $advanced, $error, $ingroup) {
4667          $templatename = 'core_form/element-' . $element->getType();
4668          if ($ingroup) {
4669              $templatename .= "-inline";
4670          }
4671          try {
4672              // We call this to generate a file not found exception if there is no template.
4673              // We don't want to call export_for_template if there is no template.
4674              core\output\mustache_template_finder::get_template_filepath($templatename);
4675  
4676              if ($element instanceof templatable) {
4677                  $elementcontext = $element->export_for_template($this);
4678  
4679                  $helpbutton = '';
4680                  if (method_exists($element, 'getHelpButton')) {
4681                      $helpbutton = $element->getHelpButton();
4682                  }
4683                  $label = $element->getLabel();
4684                  $text = '';
4685                  if (method_exists($element, 'getText')) {
4686                      // There currently exists code that adds a form element with an empty label.
4687                      // If this is the case then set the label to the description.
4688                      if (empty($label)) {
4689                          $label = $element->getText();
4690                      } else {
4691                          $text = $element->getText();
4692                      }
4693                  }
4694  
4695                  // Generate the form element wrapper ids and names to pass to the template.
4696                  // This differs between group and non-group elements.
4697                  if ($element->getType() === 'group') {
4698                      // Group element.
4699                      // The id will be something like 'fgroup_id_NAME'. E.g. fgroup_id_mygroup.
4700                      $elementcontext['wrapperid'] = $elementcontext['id'];
4701  
4702                      // Ensure group elements pass through the group name as the element name.
4703                      $elementcontext['name'] = $elementcontext['groupname'];
4704                  } else {
4705                      // Non grouped element.
4706                      // Creates an id like 'fitem_id_NAME'. E.g. fitem_id_mytextelement.
4707                      $elementcontext['wrapperid'] = 'fitem_' . $elementcontext['id'];
4708                  }
4709  
4710                  $context = array(
4711                      'element' => $elementcontext,
4712                      'label' => $label,
4713                      'text' => $text,
4714                      'required' => $required,
4715                      'advanced' => $advanced,
4716                      'helpbutton' => $helpbutton,
4717                      'error' => $error
4718                  );
4719                  return $this->render_from_template($templatename, $context);
4720              }
4721          } catch (Exception $e) {
4722              // No template for this element.
4723              return false;
4724          }
4725      }
4726  
4727      /**
4728       * Render the login signup form into a nice template for the theme.
4729       *
4730       * @param mform $form
4731       * @return string
4732       */
4733      public function render_login_signup_form($form) {
4734          global $SITE;
4735  
4736          $context = $form->export_for_template($this);
4737          $url = $this->get_logo_url();
4738          if ($url) {
4739              $url = $url->out(false);
4740          }
4741          $context['logourl'] = $url;
4742          $context['sitename'] = format_string($SITE->fullname, true,
4743                  ['context' => context_course::instance(SITEID), "escape" => false]);
4744  
4745          return $this->render_from_template('core/signup_form_layout', $context);
4746      }
4747  
4748      /**
4749       * Render the verify age and location page into a nice template for the theme.
4750       *
4751       * @param \core_auth\output\verify_age_location_page $page The renderable
4752       * @return string
4753       */
4754      protected function render_verify_age_location_page($page) {
4755          $context = $page->export_for_template($this);
4756  
4757          return $this->render_from_template('core/auth_verify_age_location_page', $context);
4758      }
4759  
4760      /**
4761       * Render the digital minor contact information page into a nice template for the theme.
4762       *
4763       * @param \core_auth\output\digital_minor_page $page The renderable
4764       * @return string
4765       */
4766      protected function render_digital_minor_page($page) {
4767          $context = $page->export_for_template($this);
4768  
4769          return $this->render_from_template('core/auth_digital_minor_page', $context);
4770      }
4771  
4772      /**
4773       * Renders a progress bar.
4774       *
4775       * Do not use $OUTPUT->render($bar), instead use progress_bar::create().
4776       *
4777       * @param  progress_bar $bar The bar.
4778       * @return string HTML fragment
4779       */
4780      public function render_progress_bar(progress_bar $bar) {
4781          $data = $bar->export_for_template($this);
4782          return $this->render_from_template('core/progress_bar', $data);
4783      }
4784  
4785      /**
4786       * Renders element for a toggle-all checkbox.
4787       *
4788       * @param \core\output\checkbox_toggleall $element
4789       * @return string
4790       */
4791      public function render_checkbox_toggleall(\core\output\checkbox_toggleall $element) {
4792          return $this->render_from_template($element->get_template(), $element->export_for_template($this));
4793      }
4794  }
4795  
4796  /**
4797   * A renderer that generates output for command-line scripts.
4798   *
4799   * The implementation of this renderer is probably incomplete.
4800   *
4801   * @copyright 2009 Tim Hunt
4802   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4803   * @since Moodle 2.0
4804   * @package core
4805   * @category output
4806   */
4807  class core_renderer_cli extends core_renderer {
4808  
4809      /**
4810       * Returns the page header.
4811       *
4812       * @return string HTML fragment
4813       */
4814      public function header() {
4815          return $this->page->heading . "\n";
4816      }
4817  
4818      /**
4819       * Renders a Check API result
4820       *
4821       * To aid in CLI consistency this status is NOT translated and the visual
4822       * width is always exactly 10 chars.
4823       *
4824       * @param core\check\result $result
4825       * @return string HTML fragment
4826       */
4827      protected function render_check_result(core\check\result $result) {
4828          $status = $result->get_status();
4829  
4830          $labels = [
4831              core\check\result::NA        => '      ' . cli_ansi_format('<colour:darkGray>' ) . ' NA ',
4832              core\check\result::OK        => '      ' . cli_ansi_format('<colour:green>') . ' OK ',
4833              core\check\result::INFO      => '    '   . cli_ansi_format('<colour:blue>' ) . ' INFO ',
4834              core\check\result::UNKNOWN   => ' '      . cli_ansi_format('<colour:darkGray>' ) . ' UNKNOWN ',
4835              core\check\result::WARNING   => ' '      . cli_ansi_format('<colour:black><bgcolour:yellow>') . ' WARNING ',
4836              core\check\result::ERROR     => '   '    . cli_ansi_format('<bgcolour:red>') . ' ERROR ',
4837              core\check\result::CRITICAL  => ''       . cli_ansi_format('<bgcolour:red>') . ' CRITICAL ',
4838          ];
4839          $string = $labels[$status] . cli_ansi_format('<colour:normal>');
4840          return $string;
4841      }
4842  
4843      /**
4844       * Renders a Check API result
4845       *
4846       * @param result $result
4847       * @return string fragment
4848       */
4849      public function check_result(core\check\result $result) {
4850          return $this->render_check_result($result);
4851      }
4852  
4853      /**
4854       * Returns a template fragment representing a Heading.
4855       *
4856       * @param string $text The text of the heading
4857       * @param int $level The level of importance of the heading
4858       * @param string $classes A space-separated list of CSS classes
4859       * @param string $id An optional ID
4860       * @return string A template fragment for a heading
4861       */
4862      public function heading($text, $level = 2, $classes = 'main', $id = null) {
4863          $text .= "\n";
4864          switch ($level) {
4865              case 1:
4866                  return '=>' . $text;
4867              case 2:
4868                  return '-->' . $text;
4869              default:
4870                  return $text;
4871          }
4872      }
4873  
4874      /**
4875       * Returns a template fragment representing a fatal error.
4876       *
4877       * @param string $message The message to output
4878       * @param string $moreinfourl URL where more info can be found about the error
4879       * @param string $link Link for the Continue button
4880       * @param array $backtrace The execution backtrace
4881       * @param string $debuginfo Debugging information
4882       * @return string A template fragment for a fatal error
4883       */
4884      public function fatal_error($message, $moreinfourl, $link, $backtrace, $debuginfo = null, $errorcode = "") {
4885          global $CFG;
4886  
4887          $output = "!!! $message !!!\n";
4888  
4889          if ($CFG->debugdeveloper) {
4890              if (!empty($debuginfo)) {
4891                  $output .= $this->notification($debuginfo, 'notifytiny');
4892              }
4893              if (!empty($backtrace)) {
4894                  $output .= $this->notification('Stack trace: ' . format_backtrace($backtrace, true), 'notifytiny');
4895              }
4896          }
4897  
4898          return $output;
4899      }
4900  
4901      /**
4902       * Returns a template fragment representing a notification.
4903       *
4904       * @param string $message The message to print out.
4905       * @param string $type    The type of notification. See constants on \core\output\notification.
4906       * @return string A template fragment for a notification
4907       */
4908      public function notification($message, $type = null) {
4909          $message = clean_text($message);
4910          if ($type === 'notifysuccess' || $type === 'success') {
4911              return "++ $message ++\n";
4912          }
4913          return "!! $message !!\n";
4914      }
4915  
4916      /**
4917       * There is no footer for a cli request, however we must override the
4918       * footer method to prevent the default footer.
4919       */
4920      public function footer() {}
4921  
4922      /**
4923       * Render a notification (that is, a status message about something that has
4924       * just happened).
4925       *
4926       * @param \core\output\notification $notification the notification to print out
4927       * @return string plain text output
4928       */
4929      public function render_notification(\core\output\notification $notification) {
4930          return $this->notification($notification->get_message(), $notification->get_message_type());
4931      }
4932  }
4933  
4934  
4935  /**
4936   * A renderer that generates output for ajax scripts.
4937   *
4938   * This renderer prevents accidental sends back only json
4939   * encoded error messages, all other output is ignored.
4940   *
4941   * @copyright 2010 Petr Skoda
4942   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4943   * @since Moodle 2.0
4944   * @package core
4945   * @category output
4946   */
4947  class core_renderer_ajax extends core_renderer {
4948  
4949      /**
4950       * Returns a template fragment representing a fatal error.
4951       *
4952       * @param string $message The message to output
4953       * @param string $moreinfourl URL where more info can be found about the error
4954       * @param string $link Link for the Continue button
4955       * @param array $backtrace The execution backtrace
4956       * @param string $debuginfo Debugging information
4957       * @return string A template fragment for a fatal error
4958       */
4959      public function fatal_error($message, $moreinfourl, $link, $backtrace, $debuginfo = null, $errorcode = "") {
4960          global $CFG;
4961  
4962          $this->page->set_context(null); // ugly hack - make sure page context is set to something, we do not want bogus warnings here
4963  
4964          $e = new stdClass();
4965          $e->error      = $message;
4966          $e->errorcode  = $errorcode;
4967          $e->stacktrace = NULL;
4968          $e->debuginfo  = NULL;
4969          $e->reproductionlink = NULL;
4970          if (!empty($CFG->debug) and $CFG->debug >= DEBUG_DEVELOPER) {
4971              $link = (string) $link;
4972              if ($link) {
4973                  $e->reproductionlink = $link;
4974              }
4975              if (!empty($debuginfo)) {
4976                  $e->debuginfo = $debuginfo;
4977              }
4978              if (!empty($backtrace)) {
4979                  $e->stacktrace = format_backtrace($backtrace, true);
4980              }
4981          }
4982          $this->header();
4983          return json_encode($e);
4984      }
4985  
4986      /**
4987       * Used to display a notification.
4988       * For the AJAX notifications are discarded.
4989       *
4990       * @param string $message The message to print out.
4991       * @param string $type    The type of notification. See constants on \core\output\notification.
4992       */
4993      public function notification($message, $type = null) {}
4994  
4995      /**
4996       * Used to display a redirection message.
4997       * AJAX redirections should not occur and as such redirection messages
4998       * are discarded.
4999       *
5000       * @param moodle_url|string $encodedurl
5001       * @param string $message
5002       * @param int $delay
5003       * @param bool $debugdisableredirect
5004       * @param string $messagetype The type of notification to show the message in.
5005       *         See constants on \core\output\notification.
5006       */
5007      public function redirect_message($encodedurl, $message, $delay, $debugdisableredirect,
5008                                       $messagetype = \core\output\notification::NOTIFY_INFO) {}
5009  
5010      /**
5011       * Prepares the start of an AJAX output.
5012       */
5013      public function header() {
5014          // unfortunately YUI iframe upload does not support application/json
5015          if (!empty($_FILES)) {
5016              @header('Content-type: text/plain; charset=utf-8');
5017              if (!core_useragent::supports_json_contenttype()) {
5018                  @header('X-Content-Type-Options: nosniff');
5019              }
5020          } else if (!core_useragent::supports_json_contenttype()) {
5021              @header('Content-type: text/plain; charset=utf-8');
5022              @header('X-Content-Type-Options: nosniff');
5023          } else {
5024              @header('Content-type: application/json; charset=utf-8');
5025          }
5026  
5027          // Headers to make it not cacheable and json
5028          @header('Cache-Control: no-store, no-cache, must-revalidate');
5029          @header('Cache-Control: post-check=0, pre-check=0', false);
5030          @header('Pragma: no-cache');
5031          @header('Expires: Mon, 20 Aug 1969 09:23:00 GMT');
5032          @header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
5033          @header('Accept-Ranges: none');
5034      }
5035  
5036      /**
5037       * There is no footer for an AJAX request, however we must override the
5038       * footer method to prevent the default footer.
5039       */
5040      public function footer() {}
5041  
5042      /**
5043       * No need for headers in an AJAX request... this should never happen.
5044       * @param string $text
5045       * @param int $level
5046       * @param string $classes
5047       * @param string $id
5048       */
5049      public function heading($text, $level = 2, $classes = 'main', $id = null) {}
5050  }
5051  
5052  
5053  
5054  /**
5055   * The maintenance renderer.
5056   *
5057   * The purpose of this renderer is to block out the core renderer methods that are not usable when the site
5058   * is running a maintenance related task.
5059   * It must always extend the core_renderer as we switch from the core_renderer to this renderer in a couple of places.
5060   *
5061   * @since Moodle 2.6
5062   * @package core
5063   * @category output
5064   * @copyright 2013 Sam Hemelryk
5065   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
5066   */
5067  class core_renderer_maintenance extends core_renderer {
5068  
5069      /**
5070       * Initialises the renderer instance.
5071       *
5072       * @param moodle_page $page
5073       * @param string $target
5074       * @throws coding_exception
5075       */
5076      public function __construct(moodle_page $page, $target) {
5077          if ($target !== RENDERER_TARGET_MAINTENANCE || $page->pagelayout !== 'maintenance') {
5078              throw new coding_exception('Invalid request for the maintenance renderer.');
5079          }
5080          parent::__construct($page, $target);
5081      }
5082  
5083      /**
5084       * Does nothing. The maintenance renderer cannot produce blocks.
5085       *
5086       * @param block_contents $bc
5087       * @param string $region
5088       * @return string
5089       */
5090      public function block(block_contents $bc, $region) {
5091          return '';
5092      }
5093  
5094      /**
5095       * Does nothing. The maintenance renderer cannot produce blocks.
5096       *
5097       * @param string $region
5098       * @param array $classes
5099       * @param string $tag
5100       * @return string
5101       */
5102      public function blocks($region, $classes = array(), $tag = 'aside') {
5103          return '';
5104      }
5105  
5106      /**
5107       * Does nothing. The maintenance renderer cannot produce blocks.
5108       *
5109       * @param string $region
5110       * @return string
5111       */
5112      public function blocks_for_region($region) {
5113          return '';
5114      }
5115  
5116      /**
5117       * Does nothing. The maintenance renderer cannot produce a course content header.
5118       *
5119       * @param bool $onlyifnotcalledbefore
5120       * @return string
5121       */
5122      public function course_content_header($onlyifnotcalledbefore = false) {
5123          return '';
5124      }
5125  
5126      /**
5127       * Does nothing. The maintenance renderer cannot produce a course content footer.
5128       *
5129       * @param bool $onlyifnotcalledbefore
5130       * @return string
5131       */
5132      public function course_content_footer($onlyifnotcalledbefore = false) {
5133          return '';
5134      }
5135  
5136      /**
5137       * Does nothing. The maintenance renderer cannot produce a course header.
5138       *
5139       * @return string
5140       */
5141      public function course_header() {
5142          return '';
5143      }
5144  
5145      /**
5146       * Does nothing. The maintenance renderer cannot produce a course footer.
5147       *
5148       * @return string
5149       */
5150      public function course_footer() {
5151          return '';
5152      }
5153  
5154      /**
5155       * Does nothing. The maintenance renderer cannot produce a custom menu.
5156       *
5157       * @param string $custommenuitems
5158       * @return string
5159       */
5160      public function custom_menu($custommenuitems = '') {
5161          return '';
5162      }
5163  
5164      /**
5165       * Does nothing. The maintenance renderer cannot produce a file picker.
5166       *
5167       * @param array $options
5168       * @return string
5169       */
5170      public function file_picker($options) {
5171          return '';
5172      }
5173  
5174      /**
5175       * Does nothing. The maintenance renderer cannot produce and HTML file tree.
5176       *
5177       * @param array $dir
5178       * @return string
5179       */
5180      public function htmllize_file_tree($dir) {
5181          return '';
5182  
5183      }
5184  
5185      /**
5186       * Overridden confirm message for upgrades.
5187       *
5188       * @param string $message The question to ask the user
5189       * @param single_button|moodle_url|string $continue The single_button component representing the Continue answer.
5190       * @param single_button|moodle_url|string $cancel The single_button component representing the Cancel answer.
5191       * @return string HTML fragment
5192       */
5193      public function confirm($message, $continue, $cancel) {
5194          // We need plain styling of confirm boxes on upgrade because we don't know which stylesheet we have (it could be
5195          // from any previous version of Moodle).
5196          if ($continue instanceof single_button) {
5197              $continue->primary = true;
5198          } else if (is_string($continue)) {
5199              $continue = new single_button(new moodle_url($continue), get_string('continue'), 'post', true);
5200          } else if ($continue instanceof moodle_url) {
5201              $continue = new single_button($continue, get_string('continue'), 'post', true);
5202          } else {
5203              throw new coding_exception('The continue param to $OUTPUT->confirm() must be either a URL' .
5204                                         ' (string/moodle_url) or a single_button instance.');
5205          }
5206  
5207          if ($cancel instanceof single_button) {
5208              $output = '';
5209          } else if (is_string($cancel)) {
5210              $cancel = new single_button(new moodle_url($cancel), get_string('cancel'), 'get');
5211          } else if ($cancel instanceof moodle_url) {
5212              $cancel = new single_button($cancel, get_string('cancel'), 'get');
5213          } else {
5214              throw new coding_exception('The cancel param to $OUTPUT->confirm() must be either a URL' .
5215                                         ' (string/moodle_url) or a single_button instance.');
5216          }
5217  
5218          $output = $this->box_start('generalbox', 'notice');
5219          $output .= html_writer::tag('h4', get_string('confirm'));
5220          $output .= html_writer::tag('p', $message);
5221          $output .= html_writer::tag('div', $this->render($continue) . $this->render($cancel), array('class' => 'buttons'));
5222          $output .= $this->box_end();
5223          return $output;
5224      }
5225  
5226      /**
5227       * Does nothing. The maintenance renderer does not support JS.
5228       *
5229       * @param block_contents $bc
5230       */
5231      public function init_block_hider_js(block_contents $bc) {
5232          // Does nothing.
5233      }
5234  
5235      /**
5236       * Does nothing. The maintenance renderer cannot produce language menus.
5237       *
5238       * @return string
5239       */
5240      public function lang_menu() {
5241          return '';
5242      }
5243  
5244      /**
5245       * Does nothing. The maintenance renderer has no need for login information.
5246       *
5247       * @param null $withlinks
5248       * @return string
5249       */
5250      public function login_info($withlinks = null) {
5251          return '';
5252      }
5253  
5254      /**
5255       * Secure login info.
5256       *
5257       * @return string
5258       */
5259      public function secure_login_info() {
5260          return $this->login_info(false);
5261      }
5262  
5263      /**
5264       * Does nothing. The maintenance renderer cannot produce user pictures.
5265       *
5266       * @param stdClass $user
5267       * @param array $options
5268       * @return string
5269       */
5270      public function user_picture(stdClass $user, array $options = null) {
5271          return '';
5272      }
5273  }