Search moodle.org's
Developer Documentation

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