Search moodle.org's
Developer Documentation

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
  • Differences Between: [Versions 310 and 311] [Versions 311 and 400] [Versions 37 and 311] [Versions 38 and 311] [Versions 39 and 311]

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