Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

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

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