Merge branch 'MDL-81713-main' of https://github.com/junpataleta/moodle
[moodle.git] / lib / outputrenderers.php
blobd4edea5323141c1967a063985956f528f16573e3
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.
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/>.
17 /**
18 * Classes for rendering HTML output for Moodle.
20 * Please see {@link http://docs.moodle.org/en/Developement:How_Moodle_outputs_HTML}
21 * for an overview.
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.
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
38 use core\di;
39 use core\hook\manager as hook_manager;
40 use core\hook\output\after_standard_main_region_html_generation;
41 use core\hook\output\before_footer_html_generation;
42 use core\hook\output\before_html_attributes;
43 use core\hook\output\before_http_headers;
44 use core\hook\output\before_standard_footer_html_generation;
45 use core\hook\output\before_standard_top_of_body_html_generation;
46 use core\output\named_templatable;
47 use core_completion\cm_completion_details;
48 use core_course\output\activity_information;
50 defined('MOODLE_INTERNAL') || die();
52 /**
53 * Simple base class for Moodle renderers.
55 * Tracks the xhtml_container_stack to use, which is passed in in the constructor.
57 * Also has methods to facilitate generating HTML output.
59 * @copyright 2009 Tim Hunt
60 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
61 * @since Moodle 2.0
62 * @package core
63 * @category output
65 class renderer_base {
66 /**
67 * @var xhtml_container_stack The xhtml_container_stack to use.
69 protected $opencontainers;
71 /**
72 * @var moodle_page The Moodle page the renderer has been created to assist with.
74 protected $page;
76 /**
77 * @var string The requested rendering target.
79 protected $target;
81 /**
82 * @var Mustache_Engine $mustache The mustache template compiler
84 private $mustache;
86 /**
87 * @var array $templatecache The mustache template cache.
89 protected $templatecache = [];
91 /**
92 * Return an instance of the mustache class.
94 * @since 2.9
95 * @return Mustache_Engine
97 protected function get_mustache() {
98 global $CFG;
100 if ($this->mustache === null) {
101 require_once("{$CFG->libdir}/filelib.php");
103 $themename = $this->page->theme->name;
104 $themerev = theme_get_revision();
106 // Create new localcache directory.
107 $cachedir = make_localcache_directory("mustache/$themerev/$themename");
109 // Remove old localcache directories.
110 $mustachecachedirs = glob("{$CFG->localcachedir}/mustache/*", GLOB_ONLYDIR);
111 foreach ($mustachecachedirs as $localcachedir) {
112 $cachedrev = [];
113 preg_match("/\/mustache\/([0-9]+)$/", $localcachedir, $cachedrev);
114 $cachedrev = isset($cachedrev[1]) ? intval($cachedrev[1]) : 0;
115 if ($cachedrev > 0 && $cachedrev < $themerev) {
116 fulldelete($localcachedir);
120 $loader = new \core\output\mustache_filesystem_loader();
121 $stringhelper = new \core\output\mustache_string_helper();
122 $cleanstringhelper = new \core\output\mustache_clean_string_helper();
123 $quotehelper = new \core\output\mustache_quote_helper();
124 $jshelper = new \core\output\mustache_javascript_helper($this->page);
125 $pixhelper = new \core\output\mustache_pix_helper($this);
126 $shortentexthelper = new \core\output\mustache_shorten_text_helper();
127 $userdatehelper = new \core\output\mustache_user_date_helper();
129 // We only expose the variables that are exposed to JS templates.
130 $safeconfig = $this->page->requires->get_config_for_javascript($this->page, $this);
132 $helpers = array('config' => $safeconfig,
133 'str' => array($stringhelper, 'str'),
134 'cleanstr' => array($cleanstringhelper, 'cleanstr'),
135 'quote' => array($quotehelper, 'quote'),
136 'js' => array($jshelper, 'help'),
137 'pix' => array($pixhelper, 'pix'),
138 'shortentext' => array($shortentexthelper, 'shorten'),
139 'userdate' => array($userdatehelper, 'transform'),
142 $this->mustache = new \core\output\mustache_engine(array(
143 'cache' => $cachedir,
144 'escape' => 's',
145 'loader' => $loader,
146 'helpers' => $helpers,
147 'pragmas' => [Mustache_Engine::PRAGMA_BLOCKS],
148 // Don't allow the JavaScript helper to be executed from within another
149 // helper. If it's allowed it can be used by users to inject malicious
150 // JS into the page.
151 'disallowednestedhelpers' => ['js'],
152 // Disable lambda rendering - content in helpers is already rendered, no need to render it again.
153 'disable_lambda_rendering' => true,
158 return $this->mustache;
163 * Constructor
165 * The constructor takes two arguments. The first is the page that the renderer
166 * has been created to assist with, and the second is the target.
167 * The target is an additional identifier that can be used to load different
168 * renderers for different options.
170 * @param moodle_page $page the page we are doing output for.
171 * @param string $target one of rendering target constants
173 public function __construct(moodle_page $page, $target) {
174 $this->opencontainers = $page->opencontainers;
175 $this->page = $page;
176 $this->target = $target;
180 * Renders a template by name with the given context.
182 * The provided data needs to be array/stdClass made up of only simple types.
183 * Simple types are array,stdClass,bool,int,float,string
185 * @since 2.9
186 * @param array|stdClass $context Context containing data for the template.
187 * @return string|boolean
189 public function render_from_template($templatename, $context) {
190 $mustache = $this->get_mustache();
192 if ($mustache->hasHelper('uniqid')) {
193 // Grab a copy of the existing helper to be restored later.
194 $uniqidhelper = $mustache->getHelper('uniqid');
195 } else {
196 // Helper doesn't exist.
197 $uniqidhelper = null;
200 // Provide 1 random value that will not change within a template
201 // but will be different from template to template. This is useful for
202 // e.g. aria attributes that only work with id attributes and must be
203 // unique in a page.
204 $mustache->addHelper('uniqid', new \core\output\mustache_uniqid_helper());
205 if (isset($this->templatecache[$templatename])) {
206 $template = $this->templatecache[$templatename];
207 } else {
208 try {
209 $template = $mustache->loadTemplate($templatename);
210 $this->templatecache[$templatename] = $template;
211 } catch (Mustache_Exception_UnknownTemplateException $e) {
212 throw new moodle_exception('Unknown template: ' . $templatename);
216 $renderedtemplate = trim($template->render($context));
218 // If we had an existing uniqid helper then we need to restore it to allow
219 // handle nested calls of render_from_template.
220 if ($uniqidhelper) {
221 $mustache->addHelper('uniqid', $uniqidhelper);
224 return $renderedtemplate;
229 * Returns rendered widget.
231 * The provided widget needs to be an object that extends the renderable
232 * interface.
233 * If will then be rendered by a method based upon the classname for the widget.
234 * For instance a widget of class `crazywidget` will be rendered by a protected
235 * render_crazywidget method of this renderer.
236 * If no render_crazywidget method exists and crazywidget implements templatable,
237 * look for the 'crazywidget' template in the same component and render that.
239 * @param renderable $widget instance with renderable interface
240 * @return string
242 public function render(renderable $widget) {
243 $classparts = explode('\\', get_class($widget));
244 // Strip namespaces.
245 $classname = array_pop($classparts);
246 // Remove _renderable suffixes.
247 $classname = preg_replace('/_renderable$/', '', $classname);
249 $rendermethod = "render_{$classname}";
250 if (method_exists($this, $rendermethod)) {
251 // Call the render_[widget_name] function.
252 // Note: This has a higher priority than the named_templatable to allow the theme to override the template.
253 return $this->$rendermethod($widget);
256 if ($widget instanceof named_templatable) {
257 // This is a named templatable.
258 // Fetch the template name from the get_template_name function instead.
259 // Note: This has higher priority than the guessed template name.
260 return $this->render_from_template(
261 $widget->get_template_name($this),
262 $widget->export_for_template($this)
266 if ($widget instanceof templatable) {
267 // Guess the templat ename based on the class name.
268 // Note: There's no benefit to moving this aboved the named_templatable and this approach is more costly.
269 $component = array_shift($classparts);
270 if (!$component) {
271 $component = 'core';
273 $template = $component . '/' . $classname;
274 $context = $widget->export_for_template($this);
275 return $this->render_from_template($template, $context);
277 throw new coding_exception("Can not render widget, renderer method ('{$rendermethod}') not found.");
281 * Adds a JS action for the element with the provided id.
283 * This method adds a JS event for the provided component action to the page
284 * and then returns the id that the event has been attached to.
285 * If no id has been provided then a new ID is generated by {@link html_writer::random_id()}
287 * @param component_action $action
288 * @param string $id
289 * @return string id of element, either original submitted or random new if not supplied
291 public function add_action_handler(component_action $action, $id = null) {
292 if (!$id) {
293 $id = html_writer::random_id($action->event);
295 $this->page->requires->event_handler("#$id", $action->event, $action->jsfunction, $action->jsfunctionargs);
296 return $id;
300 * Returns true is output has already started, and false if not.
302 * @return boolean true if the header has been printed.
304 public function has_started() {
305 return $this->page->state >= moodle_page::STATE_IN_BODY;
309 * Given an array or space-separated list of classes, prepares and returns the HTML class attribute value
311 * @param mixed $classes Space-separated string or array of classes
312 * @return string HTML class attribute value
314 public static function prepare_classes($classes) {
315 if (is_array($classes)) {
316 return implode(' ', array_unique($classes));
318 return $classes;
322 * Return the direct URL for an image from the pix folder.
324 * Use this function sparingly and never for icons. For icons use pix_icon or the pix helper in a mustache template.
326 * @deprecated since Moodle 3.3
327 * @param string $imagename the name of the icon.
328 * @param string $component specification of one plugin like in get_string()
329 * @return moodle_url
331 public function pix_url($imagename, $component = 'moodle') {
332 debugging('pix_url is deprecated. Use image_url for images and pix_icon for icons.', DEBUG_DEVELOPER);
333 return $this->page->theme->image_url($imagename, $component);
337 * Return the moodle_url for an image.
339 * The exact image location and extension is determined
340 * automatically by searching for gif|png|jpg|jpeg, please
341 * note there can not be diferent images with the different
342 * extension. The imagename is for historical reasons
343 * a relative path name, it may be changed later for core
344 * images. It is recommended to not use subdirectories
345 * in plugin and theme pix directories.
347 * There are three types of images:
348 * 1/ theme images - stored in theme/mytheme/pix/,
349 * use component 'theme'
350 * 2/ core images - stored in /pix/,
351 * overridden via theme/mytheme/pix_core/
352 * 3/ plugin images - stored in mod/mymodule/pix,
353 * overridden via theme/mytheme/pix_plugins/mod/mymodule/,
354 * example: image_url('comment', 'mod_glossary')
356 * @param string $imagename the pathname of the image
357 * @param string $component full plugin name (aka component) or 'theme'
358 * @return moodle_url
360 public function image_url($imagename, $component = 'moodle') {
361 return $this->page->theme->image_url($imagename, $component);
365 * Return the site's logo URL, if any.
367 * @param int $maxwidth The maximum width, or null when the maximum width does not matter.
368 * @param int $maxheight The maximum height, or null when the maximum height does not matter.
369 * @return moodle_url|false
371 public function get_logo_url($maxwidth = null, $maxheight = 200) {
372 global $CFG;
373 $logo = get_config('core_admin', 'logo');
374 if (empty($logo)) {
375 return false;
378 // 200px high is the default image size which should be displayed at 100px in the page to account for retina displays.
379 // It's not worth the overhead of detecting and serving 2 different images based on the device.
381 // Hide the requested size in the file path.
382 $filepath = ((int) $maxwidth . 'x' . (int) $maxheight) . '/';
384 // Use $CFG->themerev to prevent browser caching when the file changes.
385 return moodle_url::make_pluginfile_url(context_system::instance()->id, 'core_admin', 'logo', $filepath,
386 theme_get_revision(), $logo);
390 * Return the site's compact logo URL, if any.
392 * @param int $maxwidth The maximum width, or null when the maximum width does not matter.
393 * @param int $maxheight The maximum height, or null when the maximum height does not matter.
394 * @return moodle_url|false
396 public function get_compact_logo_url($maxwidth = 300, $maxheight = 300) {
397 global $CFG;
398 $logo = get_config('core_admin', 'logocompact');
399 if (empty($logo)) {
400 return false;
403 // Hide the requested size in the file path.
404 $filepath = ((int) $maxwidth . 'x' . (int) $maxheight) . '/';
406 // Use $CFG->themerev to prevent browser caching when the file changes.
407 return moodle_url::make_pluginfile_url(context_system::instance()->id, 'core_admin', 'logocompact', $filepath,
408 theme_get_revision(), $logo);
412 * Whether we should display the logo in the navbar.
414 * We will when there are no main logos, and we have compact logo.
416 * @return bool
418 public function should_display_navbar_logo() {
419 $logo = $this->get_compact_logo_url();
420 return !empty($logo);
424 * @deprecated since Moodle 4.0
426 #[\core\attribute\deprecated(null, reason: 'It is no longer used', since: '4.0', final: true)]
427 public function should_display_main_logo() {
428 \core\deprecation::emit_deprecation_if_present([self::class, __FUNCTION__]);
434 * Basis for all plugin renderers.
436 * @copyright Petr Skoda (skodak)
437 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
438 * @since Moodle 2.0
439 * @package core
440 * @category output
442 class plugin_renderer_base extends renderer_base {
445 * @var renderer_base|core_renderer A reference to the current renderer.
446 * The renderer provided here will be determined by the page but will in 90%
447 * of cases by the {@link core_renderer}
449 protected $output;
452 * Constructor method, calls the parent constructor
454 * @param moodle_page $page
455 * @param string $target one of rendering target constants
457 public function __construct(moodle_page $page, $target) {
458 if (empty($target) && $page->pagelayout === 'maintenance') {
459 // If the page is using the maintenance layout then we're going to force the target to maintenance.
460 // This way we'll get a special maintenance renderer that is designed to block access to API's that are likely
461 // unavailable for this page layout.
462 $target = RENDERER_TARGET_MAINTENANCE;
464 $this->output = $page->get_renderer('core', null, $target);
465 parent::__construct($page, $target);
469 * Renders the provided widget and returns the HTML to display it.
471 * @param renderable $widget instance with renderable interface
472 * @return string
474 public function render(renderable $widget) {
475 $classname = get_class($widget);
477 // Strip namespaces.
478 $classname = preg_replace('/^.*\\\/', '', $classname);
480 // Keep a copy at this point, we may need to look for a deprecated method.
481 $deprecatedmethod = "render_{$classname}";
483 // Remove _renderable suffixes.
484 $classname = preg_replace('/_renderable$/', '', $classname);
485 $rendermethod = "render_{$classname}";
487 if (method_exists($this, $rendermethod)) {
488 // Call the render_[widget_name] function.
489 // Note: This has a higher priority than the named_templatable to allow the theme to override the template.
490 return $this->$rendermethod($widget);
493 if ($widget instanceof named_templatable) {
494 // This is a named templatable.
495 // Fetch the template name from the get_template_name function instead.
496 // Note: This has higher priority than the deprecated method which is not overridable by themes anyway.
497 return $this->render_from_template(
498 $widget->get_template_name($this),
499 $widget->export_for_template($this)
503 if ($rendermethod !== $deprecatedmethod && method_exists($this, $deprecatedmethod)) {
504 // This is exactly where we don't want to be.
505 // If you have arrived here you have a renderable component within your plugin that has the name
506 // blah_renderable, and you have a render method render_blah_renderable on your plugin.
507 // In 2.8 we revamped output, as part of this change we changed slightly how renderables got rendered
508 // and the _renderable suffix now gets removed when looking for a render method.
509 // You need to change your renderers render_blah_renderable to render_blah.
510 // Until you do this it will not be possible for a theme to override the renderer to override your method.
511 // Please do it ASAP.
512 static $debugged = [];
513 if (!isset($debugged[$deprecatedmethod])) {
514 debugging(sprintf(
515 'Deprecated call. Please rename your renderables render method from %s to %s.',
516 $deprecatedmethod,
517 $rendermethod
518 ), DEBUG_DEVELOPER);
519 $debugged[$deprecatedmethod] = true;
521 return $this->$deprecatedmethod($widget);
524 // Pass to core renderer if method not found here.
525 // Note: this is not a parent. This is _new_ renderer which respects the requested format, and output type.
526 return $this->output->render($widget);
530 * Magic method used to pass calls otherwise meant for the standard renderer
531 * to it to ensure we don't go causing unnecessary grief.
533 * @param string $method
534 * @param array $arguments
535 * @return mixed
537 public function __call($method, $arguments) {
538 if (method_exists('renderer_base', $method)) {
539 throw new coding_exception('Protected method called against '.get_class($this).' :: '.$method);
541 if (method_exists($this->output, $method)) {
542 return call_user_func_array(array($this->output, $method), $arguments);
543 } else {
544 throw new coding_exception('Unknown method called against '.get_class($this).' :: '.$method);
551 * The standard implementation of the core_renderer interface.
553 * @copyright 2009 Tim Hunt
554 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
555 * @since Moodle 2.0
556 * @package core
557 * @category output
559 class core_renderer extends renderer_base {
561 * Do NOT use, please use <?php echo $OUTPUT->main_content() ?>
562 * in layout files instead.
563 * @deprecated
564 * @var string used in {@link core_renderer::header()}.
566 const MAIN_CONTENT_TOKEN = '[MAIN CONTENT GOES HERE]';
569 * @var string Used to pass information from {@link core_renderer::doctype()} to
570 * {@link core_renderer::standard_head_html()}.
572 protected $contenttype;
575 * @var string Used by {@link core_renderer::redirect_message()} method to communicate
576 * with {@link core_renderer::header()}.
578 protected $metarefreshtag = '';
581 * @var string Unique token for the closing HTML
583 protected $unique_end_html_token;
586 * @var string Unique token for performance information
588 protected $unique_performance_info_token;
591 * @var string Unique token for the main content.
593 protected $unique_main_content_token;
595 /** @var custom_menu_item language The language menu if created */
596 protected $language = null;
598 /** @var string The current selector for an element being streamed into */
599 protected $currentselector = '';
601 /** @var string The current element tag which is being streamed into */
602 protected $currentelement = '';
605 * Constructor
607 * @param moodle_page $page the page we are doing output for.
608 * @param string $target one of rendering target constants
610 public function __construct(moodle_page $page, $target) {
611 $this->opencontainers = $page->opencontainers;
612 $this->page = $page;
613 $this->target = $target;
615 $this->unique_end_html_token = '%%ENDHTML-'.sesskey().'%%';
616 $this->unique_performance_info_token = '%%PERFORMANCEINFO-'.sesskey().'%%';
617 $this->unique_main_content_token = '[MAIN CONTENT GOES HERE - '.sesskey().']';
621 * Get the DOCTYPE declaration that should be used with this page. Designed to
622 * be called in theme layout.php files.
624 * @return string the DOCTYPE declaration that should be used.
626 public function doctype() {
627 if ($this->page->theme->doctype === 'html5') {
628 $this->contenttype = 'text/html; charset=utf-8';
629 return "<!DOCTYPE html>\n";
631 } else if ($this->page->theme->doctype === 'xhtml5') {
632 $this->contenttype = 'application/xhtml+xml; charset=utf-8';
633 return "<!DOCTYPE html>\n";
635 } else {
636 // legacy xhtml 1.0
637 $this->contenttype = 'text/html; charset=utf-8';
638 return ('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">' . "\n");
643 * The attributes that should be added to the <html> tag. Designed to
644 * be called in theme layout.php files.
646 * @return string HTML fragment.
648 public function htmlattributes() {
649 $return = get_html_lang(true);
651 // Ensure that the callback exists prior to cache purge.
652 // This is a critical page path.
653 // TODO MDL-81134 Remove after LTS+1.
654 require_once(__DIR__ . '/classes/hook/output/before_html_attributes.php');
656 $hook = new before_html_attributes($this);
658 if ($this->page->theme->doctype !== 'html5') {
659 $hook->add_attribute('xmlns', 'http://www.w3.org/1999/xhtml');
662 $hook->process_legacy_callbacks();
663 di::get(hook_manager::class)->dispatch($hook);
665 foreach ($hook->get_attributes() as $key => $val) {
666 $val = s($val);
667 $return .= " $key=\"$val\"";
670 return $return;
674 * The standard tags (meta tags, links to stylesheets and JavaScript, etc.)
675 * that should be included in the <head> tag. Designed to be called in theme
676 * layout.php files.
678 * @return string HTML fragment.
680 public function standard_head_html() {
681 global $CFG, $SESSION, $SITE;
683 // Before we output any content, we need to ensure that certain
684 // page components are set up.
686 // Blocks must be set up early as they may require javascript which
687 // has to be included in the page header before output is created.
688 foreach ($this->page->blocks->get_regions() as $region) {
689 $this->page->blocks->ensure_content_created($region, $this);
692 // Give plugins an opportunity to add any head elements. The callback
693 // must always return a string containing valid html head content.
695 $hook = new \core\hook\output\before_standard_head_html_generation($this);
696 $hook->process_legacy_callbacks();
697 di::get(hook_manager::class)->dispatch($hook);
699 // Allow a url_rewrite plugin to setup any dynamic head content.
700 if (isset($CFG->urlrewriteclass) && !isset($CFG->upgraderunning)) {
701 $class = $CFG->urlrewriteclass;
702 $hook->add_html($class::html_head_setup());
705 $hook->add_html('<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />' . "\n");
706 $hook->add_html('<meta name="keywords" content="moodle, ' . $this->page->title . '" />' . "\n");
707 // This is only set by the {@link redirect()} method
708 $hook->add_html($this->metarefreshtag);
710 // Check if a periodic refresh delay has been set and make sure we arn't
711 // already meta refreshing
712 if ($this->metarefreshtag=='' && $this->page->periodicrefreshdelay!==null) {
713 $hook->add_html(
714 html_writer::empty_tag('meta', [
715 'http-equiv' => 'refresh',
716 'content' => $this->page->periodicrefreshdelay . ';url='.$this->page->url->out(),
721 $output = $hook->get_output();
723 // Set up help link popups for all links with the helptooltip class
724 $this->page->requires->js_init_call('M.util.help_popups.setup');
726 $focus = $this->page->focuscontrol;
727 if (!empty($focus)) {
728 if (preg_match("#forms\['([a-zA-Z0-9]+)'\].elements\['([a-zA-Z0-9]+)'\]#", $focus, $matches)) {
729 // This is a horrifically bad way to handle focus but it is passed in
730 // through messy formslib::moodleform
731 $this->page->requires->js_function_call('old_onload_focus', array($matches[1], $matches[2]));
732 } else if (strpos($focus, '.')!==false) {
733 // Old style of focus, bad way to do it
734 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);
735 $this->page->requires->js_function_call('old_onload_focus', explode('.', $focus, 2));
736 } else {
737 // Focus element with given id
738 $this->page->requires->js_function_call('focuscontrol', array($focus));
742 // Get the theme stylesheet - this has to be always first CSS, this loads also styles.css from all plugins;
743 // any other custom CSS can not be overridden via themes and is highly discouraged
744 $urls = $this->page->theme->css_urls($this->page);
745 foreach ($urls as $url) {
746 $this->page->requires->css_theme($url);
749 // Get the theme javascript head and footer
750 if ($jsurl = $this->page->theme->javascript_url(true)) {
751 $this->page->requires->js($jsurl, true);
753 if ($jsurl = $this->page->theme->javascript_url(false)) {
754 $this->page->requires->js($jsurl);
757 // Get any HTML from the page_requirements_manager.
758 $output .= $this->page->requires->get_head_code($this->page, $this);
760 // List alternate versions.
761 foreach ($this->page->alternateversions as $type => $alt) {
762 $output .= html_writer::empty_tag('link', array('rel' => 'alternate',
763 'type' => $type, 'title' => $alt->title, 'href' => $alt->url));
766 // Add noindex tag if relevant page and setting applied.
767 $allowindexing = isset($CFG->allowindexing) ? $CFG->allowindexing : 0;
768 $loginpages = array('login-index', 'login-signup');
769 if ($allowindexing == 2 || ($allowindexing == 0 && in_array($this->page->pagetype, $loginpages))) {
770 if (!isset($CFG->additionalhtmlhead)) {
771 $CFG->additionalhtmlhead = '';
773 $CFG->additionalhtmlhead .= '<meta name="robots" content="noindex" />';
776 if (!empty($CFG->additionalhtmlhead)) {
777 $output .= "\n".$CFG->additionalhtmlhead;
780 if ($this->page->pagelayout == 'frontpage') {
781 $summary = s(strip_tags(format_text($SITE->summary, FORMAT_HTML)));
782 if (!empty($summary)) {
783 $output .= "<meta name=\"description\" content=\"$summary\" />\n";
787 return $output;
791 * The standard tags (typically skip links) that should be output just inside
792 * the start of the <body> tag. Designed to be called in theme layout.php files.
794 * @return string HTML fragment.
796 public function standard_top_of_body_html() {
797 global $CFG;
798 $output = $this->page->requires->get_top_of_body_code($this);
799 if ($this->page->pagelayout !== 'embedded' && !empty($CFG->additionalhtmltopofbody)) {
800 $output .= "\n".$CFG->additionalhtmltopofbody;
803 // Ensure that the callback exists prior to cache purge.
804 // This is a critical page path.
805 // TODO MDL-81134 Remove after LTS+1.
806 require_once(__DIR__ . '/classes/hook/output/before_standard_top_of_body_html_generation.php');
808 // Allow components to add content to the top of the body.
809 $hook = new before_standard_top_of_body_html_generation($this, $output);
810 $hook->process_legacy_callbacks();
811 di::get(hook_manager::class)->dispatch($hook);
812 $output = $hook->get_output();
814 $output .= $this->maintenance_warning();
816 return $output;
820 * Scheduled maintenance warning message.
822 * Note: This is a nasty hack to display maintenance notice, this should be moved
823 * to some general notification area once we have it.
825 * @return string
827 public function maintenance_warning() {
828 global $CFG;
830 $output = '';
831 if (isset($CFG->maintenance_later) and $CFG->maintenance_later > time()) {
832 $timeleft = $CFG->maintenance_later - time();
833 // If timeleft less than 30 sec, set the class on block to error to highlight.
834 $errorclass = ($timeleft < 30) ? 'alert-error alert-danger' : 'alert-warning';
835 $output .= $this->box_start($errorclass . ' moodle-has-zindex maintenancewarning m-3 alert');
836 $a = new stdClass();
837 $a->hour = (int)($timeleft / 3600);
838 $a->min = (int)(floor($timeleft / 60) % 60);
839 $a->sec = (int)($timeleft % 60);
840 if ($a->hour > 0) {
841 $output .= get_string('maintenancemodeisscheduledlong', 'admin', $a);
842 } else {
843 $output .= get_string('maintenancemodeisscheduled', 'admin', $a);
846 $output .= $this->box_end();
847 $this->page->requires->yui_module('moodle-core-maintenancemodetimer', 'M.core.maintenancemodetimer',
848 array(array('timeleftinsec' => $timeleft)));
849 $this->page->requires->strings_for_js(
850 array('maintenancemodeisscheduled', 'maintenancemodeisscheduledlong', 'sitemaintenance'),
851 'admin');
853 return $output;
857 * content that should be output in the footer area
858 * of the page. Designed to be called in theme layout.php files.
860 * @return string HTML fragment.
862 public function standard_footer_html() {
863 if (during_initial_install()) {
864 // Debugging info can not work before install is finished,
865 // in any case we do not want any links during installation!
866 return '';
869 // Ensure that the callback exists prior to cache purge.
870 // This is a critical page path.
871 // TODO MDL-81134 Remove after LTS+1.
872 require_once(__DIR__ . '/classes/hook/output/before_standard_footer_html_generation.php');
874 $hook = new before_standard_footer_html_generation($this);
875 $hook->process_legacy_callbacks();
876 di::get(hook_manager::class)->dispatch($hook);
877 $output = $hook->get_output();
879 if ($this->page->devicetypeinuse == 'legacy') {
880 // The legacy theme is in use print the notification
881 $output .= html_writer::tag('div', get_string('legacythemeinuse'), array('class'=>'legacythemeinuse'));
884 // Get links to switch device types (only shown for users not on a default device)
885 $output .= $this->theme_switch_links();
887 return $output;
891 * Performance information and validation links for debugging.
893 * @return string HTML fragment.
895 public function debug_footer_html() {
896 global $CFG, $SCRIPT;
897 $output = '';
899 if (during_initial_install()) {
900 // Debugging info can not work before install is finished.
901 return $output;
904 // This function is normally called from a layout.php file
905 // but some of the content won't be known until later, so we return a placeholder
906 // for now. This will be replaced with the real content in the footer.
907 $output .= $this->unique_performance_info_token;
909 if (!empty($CFG->debugpageinfo)) {
910 $output .= '<div class="performanceinfo pageinfo">' . get_string('pageinfodebugsummary', 'core_admin',
911 $this->page->debug_summary()) . '</div>';
913 if (debugging(null, DEBUG_DEVELOPER) and has_capability('moodle/site:config', context_system::instance())) { // Only in developer mode
915 // Add link to profiling report if necessary
916 if (function_exists('profiling_is_running') && profiling_is_running()) {
917 $txt = get_string('profiledscript', 'admin');
918 $title = get_string('profiledscriptview', 'admin');
919 $url = $CFG->wwwroot . '/admin/tool/profiling/index.php?script=' . urlencode($SCRIPT);
920 $link= '<a title="' . $title . '" href="' . $url . '">' . $txt . '</a>';
921 $output .= '<div class="profilingfooter">' . $link . '</div>';
923 $purgeurl = new moodle_url('/admin/purgecaches.php', array('confirm' => 1,
924 'sesskey' => sesskey(), 'returnurl' => $this->page->url->out_as_local_url(false)));
925 $output .= '<div class="purgecaches">' .
926 html_writer::link($purgeurl, get_string('purgecaches', 'admin')) . '</div>';
928 // Reactive module debug panel.
929 $output .= $this->render_from_template('core/local/reactive/debugpanel', []);
931 if (!empty($CFG->debugvalidators)) {
932 $siteurl = qualified_me();
933 $nuurl = new moodle_url('https://validator.w3.org/nu/', ['doc' => $siteurl, 'showsource' => 'yes']);
934 $waveurl = new moodle_url('https://wave.webaim.org/report#/' . urlencode($siteurl));
935 $validatorlinks = [
936 html_writer::link($nuurl, get_string('validatehtml')),
937 html_writer::link($waveurl, get_string('wcagcheck'))
939 $validatorlinkslist = html_writer::alist($validatorlinks, ['class' => 'list-unstyled ml-1']);
940 $output .= html_writer::div($validatorlinkslist, 'validators');
942 return $output;
946 * Returns standard main content placeholder.
947 * Designed to be called in theme layout.php files.
949 * @return string HTML fragment.
951 public function main_content() {
952 // This is here because it is the only place we can inject the "main" role over the entire main content area
953 // without requiring all theme's to manually do it, and without creating yet another thing people need to
954 // remember in the theme.
955 // This is an unfortunate hack. DO NO EVER add anything more here.
956 // DO NOT add classes.
957 // DO NOT add an id.
958 return '<div role="main">'.$this->unique_main_content_token.'</div>';
962 * Returns information about an activity.
964 * @deprecated since Moodle 4.3 MDL-78744
965 * @todo MDL-78926 This method will be deleted in Moodle 4.7
966 * @param cm_info $cminfo The course module information.
967 * @param cm_completion_details $completiondetails The completion details for this activity module.
968 * @param array $activitydates The dates for this activity module.
969 * @return string the activity information HTML.
970 * @throws coding_exception
972 public function activity_information(cm_info $cminfo, cm_completion_details $completiondetails, array $activitydates): string {
973 debugging('activity_information method is deprecated.', DEBUG_DEVELOPER);
974 if (!$completiondetails->has_completion() && empty($activitydates)) {
975 // No need to render the activity information when there's no completion info and activity dates to show.
976 return '';
978 $activityinfo = new activity_information($cminfo, $completiondetails, $activitydates);
979 $renderer = $this->page->get_renderer('core', 'course');
980 return $renderer->render($activityinfo);
984 * Returns standard navigation between activities in a course.
986 * @return string the navigation HTML.
988 public function activity_navigation() {
989 // First we should check if we want to add navigation.
990 $context = $this->page->context;
991 if (($this->page->pagelayout !== 'incourse' && $this->page->pagelayout !== 'frametop')
992 || $context->contextlevel != CONTEXT_MODULE) {
993 return '';
996 // If the activity is in stealth mode, show no links.
997 if ($this->page->cm->is_stealth()) {
998 return '';
1001 $course = $this->page->cm->get_course();
1002 $courseformat = course_get_format($course);
1004 // If the theme implements course index and the current course format uses course index and the current
1005 // page layout is not 'frametop' (this layout does not support course index), show no links.
1006 if ($this->page->theme->usescourseindex && $courseformat->uses_course_index() &&
1007 $this->page->pagelayout !== 'frametop') {
1008 return '';
1011 // Get a list of all the activities in the course.
1012 $modules = get_fast_modinfo($course->id)->get_cms();
1014 // Put the modules into an array in order by the position they are shown in the course.
1015 $mods = [];
1016 $activitylist = [];
1017 foreach ($modules as $module) {
1018 // Only add activities the user can access, aren't in stealth mode and have a url (eg. mod_label does not).
1019 if (!$module->uservisible || $module->is_stealth() || empty($module->url)) {
1020 continue;
1022 $mods[$module->id] = $module;
1024 // No need to add the current module to the list for the activity dropdown menu.
1025 if ($module->id == $this->page->cm->id) {
1026 continue;
1028 // Module name.
1029 $modname = $module->get_formatted_name();
1030 // Display the hidden text if necessary.
1031 if (!$module->visible) {
1032 $modname .= ' ' . get_string('hiddenwithbrackets');
1034 // Module URL.
1035 $linkurl = new moodle_url($module->url, array('forceview' => 1));
1036 // Add module URL (as key) and name (as value) to the activity list array.
1037 $activitylist[$linkurl->out(false)] = $modname;
1040 $nummods = count($mods);
1042 // If there is only one mod then do nothing.
1043 if ($nummods == 1) {
1044 return '';
1047 // Get an array of just the course module ids used to get the cmid value based on their position in the course.
1048 $modids = array_keys($mods);
1050 // Get the position in the array of the course module we are viewing.
1051 $position = array_search($this->page->cm->id, $modids);
1053 $prevmod = null;
1054 $nextmod = null;
1056 // Check if we have a previous mod to show.
1057 if ($position > 0) {
1058 $prevmod = $mods[$modids[$position - 1]];
1061 // Check if we have a next mod to show.
1062 if ($position < ($nummods - 1)) {
1063 $nextmod = $mods[$modids[$position + 1]];
1066 $activitynav = new \core_course\output\activity_navigation($prevmod, $nextmod, $activitylist);
1067 $renderer = $this->page->get_renderer('core', 'course');
1068 return $renderer->render($activitynav);
1072 * The standard tags (typically script tags that are not needed earlier) that
1073 * should be output after everything else. Designed to be called in theme layout.php files.
1075 * @return string HTML fragment.
1077 public function standard_end_of_body_html() {
1078 global $CFG;
1080 // This function is normally called from a layout.php file in {@link core_renderer::header()}
1081 // but some of the content won't be known until later, so we return a placeholder
1082 // for now. This will be replaced with the real content in {@link core_renderer::footer()}.
1083 $output = '';
1084 if ($this->page->pagelayout !== 'embedded' && !empty($CFG->additionalhtmlfooter)) {
1085 $output .= "\n".$CFG->additionalhtmlfooter;
1087 $output .= $this->unique_end_html_token;
1088 return $output;
1092 * The standard HTML that should be output just before the <footer> tag.
1093 * Designed to be called in theme layout.php files.
1095 * @return string HTML fragment.
1097 public function standard_after_main_region_html() {
1098 global $CFG;
1100 // Ensure that the callback exists prior to cache purge.
1101 // This is a critical page path.
1102 // TODO MDL-81134 Remove after LTS+1.
1103 require_once(__DIR__ . '/classes/hook/output/after_standard_main_region_html_generation.php');
1105 $hook = new after_standard_main_region_html_generation($this);
1107 if ($this->page->pagelayout !== 'embedded' && !empty($CFG->additionalhtmlbottomofbody)) {
1108 $hook->add_html("\n");
1109 $hook->add_html($CFG->additionalhtmlbottomofbody);
1112 $hook->process_legacy_callbacks();
1113 di::get(hook_manager::class)->dispatch($hook);
1115 return $hook->get_output();
1119 * Return the standard string that says whether you are logged in (and switched
1120 * roles/logged in as another user).
1121 * @param bool $withlinks if false, then don't include any links in the HTML produced.
1122 * If not set, the default is the nologinlinks option from the theme config.php file,
1123 * and if that is not set, then links are included.
1124 * @return string HTML fragment.
1126 public function login_info($withlinks = null) {
1127 global $USER, $CFG, $DB, $SESSION;
1129 if (during_initial_install()) {
1130 return '';
1133 if (is_null($withlinks)) {
1134 $withlinks = empty($this->page->layout_options['nologinlinks']);
1137 $course = $this->page->course;
1138 if (\core\session\manager::is_loggedinas()) {
1139 $realuser = \core\session\manager::get_realuser();
1140 $fullname = fullname($realuser);
1141 if ($withlinks) {
1142 $loginastitle = get_string('loginas');
1143 $realuserinfo = " [<a href=\"$CFG->wwwroot/course/loginas.php?id=$course->id&amp;sesskey=".sesskey()."\"";
1144 $realuserinfo .= "title =\"".$loginastitle."\">$fullname</a>] ";
1145 } else {
1146 $realuserinfo = " [$fullname] ";
1148 } else {
1149 $realuserinfo = '';
1152 $loginpage = $this->is_login_page();
1153 $loginurl = get_login_url();
1155 if (empty($course->id)) {
1156 // $course->id is not defined during installation
1157 return '';
1158 } else if (isloggedin()) {
1159 $context = context_course::instance($course->id);
1161 $fullname = fullname($USER);
1162 // Since Moodle 2.0 this link always goes to the public profile page (not the course profile page)
1163 if ($withlinks) {
1164 $linktitle = get_string('viewprofile');
1165 $username = "<a href=\"$CFG->wwwroot/user/profile.php?id=$USER->id\" title=\"$linktitle\">$fullname</a>";
1166 } else {
1167 $username = $fullname;
1169 if (is_mnet_remote_user($USER) and $idprovider = $DB->get_record('mnet_host', array('id'=>$USER->mnethostid))) {
1170 if ($withlinks) {
1171 $username .= " from <a href=\"{$idprovider->wwwroot}\">{$idprovider->name}</a>";
1172 } else {
1173 $username .= " from {$idprovider->name}";
1176 if (isguestuser()) {
1177 $loggedinas = $realuserinfo.get_string('loggedinasguest');
1178 if (!$loginpage && $withlinks) {
1179 $loggedinas .= " (<a href=\"$loginurl\">".get_string('login').'</a>)';
1181 } else if (is_role_switched($course->id)) { // Has switched roles
1182 $rolename = '';
1183 if ($role = $DB->get_record('role', array('id'=>$USER->access['rsw'][$context->path]))) {
1184 $rolename = ': '.role_get_name($role, $context);
1186 $loggedinas = get_string('loggedinas', 'moodle', $username).$rolename;
1187 if ($withlinks) {
1188 $url = new moodle_url('/course/switchrole.php', array('id'=>$course->id,'sesskey'=>sesskey(), 'switchrole'=>0, 'returnurl'=>$this->page->url->out_as_local_url(false)));
1189 $loggedinas .= ' ('.html_writer::tag('a', get_string('switchrolereturn'), array('href' => $url)).')';
1191 } else {
1192 $loggedinas = $realuserinfo.get_string('loggedinas', 'moodle', $username);
1193 if ($withlinks) {
1194 $loggedinas .= " (<a href=\"$CFG->wwwroot/login/logout.php?sesskey=".sesskey()."\">".get_string('logout').'</a>)';
1197 } else {
1198 $loggedinas = get_string('loggedinnot', 'moodle');
1199 if (!$loginpage && $withlinks) {
1200 $loggedinas .= " (<a href=\"$loginurl\">".get_string('login').'</a>)';
1204 $loggedinas = '<div class="logininfo">'.$loggedinas.'</div>';
1206 if (isset($SESSION->justloggedin)) {
1207 unset($SESSION->justloggedin);
1208 if (!isguestuser()) {
1209 // Include this file only when required.
1210 require_once($CFG->dirroot . '/user/lib.php');
1211 if (($count = user_count_login_failures($USER)) && !empty($CFG->displayloginfailures)) {
1212 $loggedinas .= '<div class="loginfailures">';
1213 $a = new stdClass();
1214 $a->attempts = $count;
1215 $loggedinas .= get_string('failedloginattempts', '', $a);
1216 if (file_exists("$CFG->dirroot/report/log/index.php") and has_capability('report/log:view', context_system::instance())) {
1217 $loggedinas .= ' ('.html_writer::link(new moodle_url('/report/log/index.php', array('chooselog' => 1,
1218 'id' => 0 , 'modid' => 'site_errors')), get_string('logs')).')';
1220 $loggedinas .= '</div>';
1225 return $loggedinas;
1229 * Check whether the current page is a login page.
1231 * @since Moodle 2.9
1232 * @return bool
1234 protected function is_login_page() {
1235 // This is a real bit of a hack, but its a rarety that we need to do something like this.
1236 // In fact the login pages should be only these two pages and as exposing this as an option for all pages
1237 // could lead to abuse (or at least unneedingly complex code) the hack is the way to go.
1238 return in_array(
1239 $this->page->url->out_as_local_url(false, array()),
1240 array(
1241 '/login/index.php',
1242 '/login/forgot_password.php',
1248 * Return the 'back' link that normally appears in the footer.
1250 * @return string HTML fragment.
1252 public function home_link() {
1253 global $CFG, $SITE;
1255 if ($this->page->pagetype == 'site-index') {
1256 // Special case for site home page - please do not remove
1257 return '<div class="sitelink">' .
1258 '<a title="Moodle" class="d-inline-block aalink" href="http://moodle.org/">' .
1259 '<img src="' . $this->image_url('moodlelogo_grayhat') . '" alt="'.get_string('moodlelogo').'" /></a></div>';
1261 } else if (!empty($CFG->target_release) && $CFG->target_release != $CFG->release) {
1262 // Special case for during install/upgrade.
1263 return '<div class="sitelink">'.
1264 '<a title="Moodle" href="http://docs.moodle.org/en/Administrator_documentation" onclick="this.target=\'_blank\'">' .
1265 '<img src="' . $this->image_url('moodlelogo_grayhat') . '" alt="'.get_string('moodlelogo').'" /></a></div>';
1267 } else if ($this->page->course->id == $SITE->id || strpos($this->page->pagetype, 'course-view') === 0) {
1268 return '<div class="homelink"><a href="' . $CFG->wwwroot . '/">' .
1269 get_string('home') . '</a></div>';
1271 } else {
1272 return '<div class="homelink"><a href="' . $CFG->wwwroot . '/course/view.php?id=' . $this->page->course->id . '">' .
1273 format_string($this->page->course->shortname, true, array('context' => $this->page->context)) . '</a></div>';
1278 * Redirects the user by any means possible given the current state
1280 * This function should not be called directly, it should always be called using
1281 * the redirect function in lib/weblib.php
1283 * The redirect function should really only be called before page output has started
1284 * however it will allow itself to be called during the state STATE_IN_BODY
1286 * @param string $encodedurl The URL to send to encoded if required
1287 * @param string $message The message to display to the user if any
1288 * @param int $delay The delay before redirecting a user, if $message has been
1289 * set this is a requirement and defaults to 3, set to 0 no delay
1290 * @param boolean $debugdisableredirect this redirect has been disabled for
1291 * debugging purposes. Display a message that explains, and don't
1292 * trigger the redirect.
1293 * @param string $messagetype The type of notification to show the message in.
1294 * See constants on \core\output\notification.
1295 * @return string The HTML to display to the user before dying, may contain
1296 * meta refresh, javascript refresh, and may have set header redirects
1298 public function redirect_message($encodedurl, $message, $delay, $debugdisableredirect,
1299 $messagetype = \core\output\notification::NOTIFY_INFO) {
1300 global $CFG;
1301 $url = str_replace('&amp;', '&', $encodedurl);
1303 switch ($this->page->state) {
1304 case moodle_page::STATE_BEFORE_HEADER :
1305 // No output yet it is safe to delivery the full arsenal of redirect methods
1306 if (!$debugdisableredirect) {
1307 // Don't use exactly the same time here, it can cause problems when both redirects fire at the same time.
1308 $this->metarefreshtag = '<meta http-equiv="refresh" content="'. $delay .'; url='. $encodedurl .'" />'."\n";
1309 $this->page->requires->js_function_call('document.location.replace', array($url), false, ($delay + 3));
1311 $output = $this->header();
1312 break;
1313 case moodle_page::STATE_PRINTING_HEADER :
1314 // We should hopefully never get here
1315 throw new coding_exception('You cannot redirect while printing the page header');
1316 break;
1317 case moodle_page::STATE_IN_BODY :
1318 // We really shouldn't be here but we can deal with this
1319 debugging("You should really redirect before you start page output");
1320 if (!$debugdisableredirect) {
1321 $this->page->requires->js_function_call('document.location.replace', array($url), false, $delay);
1323 $output = $this->opencontainers->pop_all_but_last();
1324 break;
1325 case moodle_page::STATE_DONE :
1326 // Too late to be calling redirect now
1327 throw new coding_exception('You cannot redirect after the entire page has been generated');
1328 break;
1330 $output .= $this->notification($message, $messagetype);
1331 $output .= '<div class="continuebutton">(<a href="'. $encodedurl .'">'. get_string('continue') .'</a>)</div>';
1332 if ($debugdisableredirect) {
1333 $output .= '<p><strong>'.get_string('erroroutput', 'error').'</strong></p>';
1335 $output .= $this->footer();
1336 return $output;
1340 * Start output by sending the HTTP headers, and printing the HTML <head>
1341 * and the start of the <body>.
1343 * To control what is printed, you should set properties on $PAGE.
1345 * @return string HTML that you must output this, preferably immediately.
1347 public function header() {
1348 global $USER, $CFG, $SESSION;
1350 // Ensure that the callback exists prior to cache purge.
1351 // This is a critical page path.
1352 // TODO MDL-81134 Remove after LTS+1.
1353 require_once(__DIR__ . '/classes/hook/output/before_http_headers.php');
1355 $hook = new before_http_headers($this);
1356 $hook->process_legacy_callbacks();
1357 di::get(hook_manager::class)->dispatch($hook);
1359 if (\core\session\manager::is_loggedinas()) {
1360 $this->page->add_body_class('userloggedinas');
1363 if (isset($SESSION->justloggedin) && !empty($CFG->displayloginfailures)) {
1364 require_once($CFG->dirroot . '/user/lib.php');
1365 // Set second parameter to false as we do not want reset the counter, the same message appears on footer.
1366 if ($count = user_count_login_failures($USER, false)) {
1367 $this->page->add_body_class('loginfailures');
1371 // If the user is logged in, and we're not in initial install,
1372 // check to see if the user is role-switched and add the appropriate
1373 // CSS class to the body element.
1374 if (!during_initial_install() && isloggedin() && is_role_switched($this->page->course->id)) {
1375 $this->page->add_body_class('userswitchedrole');
1378 // Give themes a chance to init/alter the page object.
1379 $this->page->theme->init_page($this->page);
1381 $this->page->set_state(moodle_page::STATE_PRINTING_HEADER);
1383 // Find the appropriate page layout file, based on $this->page->pagelayout.
1384 $layoutfile = $this->page->theme->layout_file($this->page->pagelayout);
1385 // Render the layout using the layout file.
1386 $rendered = $this->render_page_layout($layoutfile);
1388 // Slice the rendered output into header and footer.
1389 $cutpos = strpos($rendered, $this->unique_main_content_token);
1390 if ($cutpos === false) {
1391 $cutpos = strpos($rendered, self::MAIN_CONTENT_TOKEN);
1392 $token = self::MAIN_CONTENT_TOKEN;
1393 } else {
1394 $token = $this->unique_main_content_token;
1397 if ($cutpos === false) {
1398 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.');
1401 $header = substr($rendered, 0, $cutpos);
1402 $footer = substr($rendered, $cutpos + strlen($token));
1404 if (empty($this->contenttype)) {
1405 debugging('The page layout file did not call $OUTPUT->doctype()');
1406 $header = $this->doctype() . $header;
1409 // If this theme version is below 2.4 release and this is a course view page
1410 if ((!isset($this->page->theme->settings->version) || $this->page->theme->settings->version < 2012101500) &&
1411 $this->page->pagelayout === 'course' && $this->page->url->compare(new moodle_url('/course/view.php'), URL_MATCH_BASE)) {
1412 // check if course content header/footer have not been output during render of theme layout
1413 $coursecontentheader = $this->course_content_header(true);
1414 $coursecontentfooter = $this->course_content_footer(true);
1415 if (!empty($coursecontentheader)) {
1416 // display debug message and add header and footer right above and below main content
1417 // Please note that course header and footer (to be displayed above and below the whole page)
1418 // are not displayed in this case at all.
1419 // Besides the content header and footer are not displayed on any other course page
1420 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);
1421 $header .= $coursecontentheader;
1422 $footer = $coursecontentfooter. $footer;
1426 send_headers($this->contenttype, $this->page->cacheable);
1428 $this->opencontainers->push('header/footer', $footer);
1429 $this->page->set_state(moodle_page::STATE_IN_BODY);
1431 // If an activity record has been set, activity_header will handle this.
1432 if (!$this->page->cm || !empty($this->page->layout_options['noactivityheader'])) {
1433 $header .= $this->skip_link_target('maincontent');
1435 return $header;
1439 * Renders and outputs the page layout file.
1441 * This is done by preparing the normal globals available to a script, and
1442 * then including the layout file provided by the current theme for the
1443 * requested layout.
1445 * @param string $layoutfile The name of the layout file
1446 * @return string HTML code
1448 protected function render_page_layout($layoutfile) {
1449 global $CFG, $SITE, $USER;
1450 // The next lines are a bit tricky. The point is, here we are in a method
1451 // of a renderer class, and this object may, or may not, be the same as
1452 // the global $OUTPUT object. When rendering the page layout file, we want to use
1453 // this object. However, people writing Moodle code expect the current
1454 // renderer to be called $OUTPUT, not $this, so define a variable called
1455 // $OUTPUT pointing at $this. The same comment applies to $PAGE and $COURSE.
1456 $OUTPUT = $this;
1457 $PAGE = $this->page;
1458 $COURSE = $this->page->course;
1460 ob_start();
1461 include($layoutfile);
1462 $rendered = ob_get_contents();
1463 ob_end_clean();
1464 return $rendered;
1468 * Outputs the page's footer
1470 * @return string HTML fragment
1472 public function footer() {
1473 global $CFG, $DB, $PERF;
1475 // Ensure that the callback exists prior to cache purge.
1476 // This is a critical page path.
1477 // TODO MDL-81134 Remove after LTS+1.
1478 require_once(__DIR__ . '/classes/hook/output/before_footer_html_generation.php');
1480 $hook = new before_footer_html_generation($this);
1481 $hook->process_legacy_callbacks();
1482 di::get(hook_manager::class)->dispatch($hook);
1483 $hook->add_html($this->container_end_all(true));
1484 $output = $hook->get_output();
1486 $footer = $this->opencontainers->pop('header/footer');
1488 if (debugging() and $DB and $DB->is_transaction_started()) {
1489 // TODO: MDL-20625 print warning - transaction will be rolled back
1492 // Provide some performance info if required
1493 $performanceinfo = '';
1494 if (MDL_PERF || (!empty($CFG->perfdebug) && $CFG->perfdebug > 7)) {
1495 if (MDL_PERFTOFOOT || debugging() || (!empty($CFG->perfdebug) && $CFG->perfdebug > 7)) {
1496 if (NO_OUTPUT_BUFFERING) {
1497 // If the output buffer was off then we render a placeholder and stream the
1498 // performance debugging into it at the very end in the shutdown handler.
1499 $PERF->perfdebugdeferred = true;
1500 $performanceinfo .= html_writer::tag('div',
1501 get_string('perfdebugdeferred', 'admin'),
1503 'id' => 'perfdebugfooter',
1504 'style' => 'min-height: 30em',
1506 } else {
1507 $perf = get_performance_info();
1508 $performanceinfo = $perf['html'];
1513 // We always want performance data when running a performance test, even if the user is redirected to another page.
1514 if (MDL_PERF_TEST && strpos($footer, $this->unique_performance_info_token) === false) {
1515 $footer = $this->unique_performance_info_token . $footer;
1517 $footer = str_replace($this->unique_performance_info_token, $performanceinfo, $footer);
1519 // Only show notifications when the current page has a context id.
1520 if (!empty($this->page->context->id)) {
1521 $this->page->requires->js_call_amd('core/notification', 'init', array(
1522 $this->page->context->id,
1523 \core\notification::fetch_as_array($this)
1526 $footer = str_replace($this->unique_end_html_token, $this->page->requires->get_end_code(), $footer);
1528 $this->page->set_state(moodle_page::STATE_DONE);
1530 // Here we remove the closing body and html tags and store them to be added back
1531 // in the shutdown handler so we can have valid html with streaming script tags
1532 // which are rendered after the visible footer.
1533 $tags = '';
1534 preg_match('#\<\/body>#i', $footer, $matches);
1535 $tags .= $matches[0];
1536 $footer = str_replace($matches[0], '', $footer);
1538 preg_match('#\<\/html>#i', $footer, $matches);
1539 $tags .= $matches[0];
1540 $footer = str_replace($matches[0], '', $footer);
1542 $CFG->closingtags = $tags;
1544 return $output . $footer;
1548 * Close all but the last open container. This is useful in places like error
1549 * handling, where you want to close all the open containers (apart from <body>)
1550 * before outputting the error message.
1552 * @param bool $shouldbenone assert that the stack should be empty now - causes a
1553 * developer debug warning if it isn't.
1554 * @return string the HTML required to close any open containers inside <body>.
1556 public function container_end_all($shouldbenone = false) {
1557 return $this->opencontainers->pop_all_but_last($shouldbenone);
1561 * Returns course-specific information to be output immediately above content on any course page
1562 * (for the current course)
1564 * @param bool $onlyifnotcalledbefore output content only if it has not been output before
1565 * @return string
1567 public function course_content_header($onlyifnotcalledbefore = false) {
1568 global $CFG;
1569 static $functioncalled = false;
1570 if ($functioncalled && $onlyifnotcalledbefore) {
1571 // we have already output the content header
1572 return '';
1575 // Output any session notification.
1576 $notifications = \core\notification::fetch();
1578 $bodynotifications = '';
1579 foreach ($notifications as $notification) {
1580 $bodynotifications .= $this->render_from_template(
1581 $notification->get_template_name(),
1582 $notification->export_for_template($this)
1586 $output = html_writer::span($bodynotifications, 'notifications', array('id' => 'user-notifications'));
1588 if ($this->page->course->id == SITEID) {
1589 // return immediately and do not include /course/lib.php if not necessary
1590 return $output;
1593 require_once($CFG->dirroot.'/course/lib.php');
1594 $functioncalled = true;
1595 $courseformat = course_get_format($this->page->course);
1596 if (($obj = $courseformat->course_content_header()) !== null) {
1597 $output .= html_writer::div($courseformat->get_renderer($this->page)->render($obj), 'course-content-header');
1599 return $output;
1603 * Returns course-specific information to be output immediately below content on any course page
1604 * (for the current course)
1606 * @param bool $onlyifnotcalledbefore output content only if it has not been output before
1607 * @return string
1609 public function course_content_footer($onlyifnotcalledbefore = false) {
1610 global $CFG;
1611 if ($this->page->course->id == SITEID) {
1612 // return immediately and do not include /course/lib.php if not necessary
1613 return '';
1615 static $functioncalled = false;
1616 if ($functioncalled && $onlyifnotcalledbefore) {
1617 // we have already output the content footer
1618 return '';
1620 $functioncalled = true;
1621 require_once($CFG->dirroot.'/course/lib.php');
1622 $courseformat = course_get_format($this->page->course);
1623 if (($obj = $courseformat->course_content_footer()) !== null) {
1624 return html_writer::div($courseformat->get_renderer($this->page)->render($obj), 'course-content-footer');
1626 return '';
1630 * Returns course-specific information to be output on any course page in the header area
1631 * (for the current course)
1633 * @return string
1635 public function course_header() {
1636 global $CFG;
1637 if ($this->page->course->id == SITEID) {
1638 // return immediately and do not include /course/lib.php if not necessary
1639 return '';
1641 require_once($CFG->dirroot.'/course/lib.php');
1642 $courseformat = course_get_format($this->page->course);
1643 if (($obj = $courseformat->course_header()) !== null) {
1644 return $courseformat->get_renderer($this->page)->render($obj);
1646 return '';
1650 * Returns course-specific information to be output on any course page in the footer area
1651 * (for the current course)
1653 * @return string
1655 public function course_footer() {
1656 global $CFG;
1657 if ($this->page->course->id == SITEID) {
1658 // return immediately and do not include /course/lib.php if not necessary
1659 return '';
1661 require_once($CFG->dirroot.'/course/lib.php');
1662 $courseformat = course_get_format($this->page->course);
1663 if (($obj = $courseformat->course_footer()) !== null) {
1664 return $courseformat->get_renderer($this->page)->render($obj);
1666 return '';
1670 * Get the course pattern datauri to show on a course card.
1672 * The datauri is an encoded svg that can be passed as a url.
1673 * @param int $id Id to use when generating the pattern
1674 * @return string datauri
1676 public function get_generated_image_for_id($id) {
1677 $color = $this->get_generated_color_for_id($id);
1678 $pattern = new \core_geopattern();
1679 $pattern->setColor($color);
1680 $pattern->patternbyid($id);
1681 return $pattern->datauri();
1685 * Get the course pattern image URL.
1687 * @param context_course $context course context object
1688 * @return string URL of the course pattern image in SVG format
1690 public function get_generated_url_for_course(context_course $context): string {
1691 return moodle_url::make_pluginfile_url($context->id, 'course', 'generated', null, '/', 'course.svg')->out();
1695 * Get the course pattern in SVG format to show on a course card.
1697 * @param int $id id to use when generating the pattern
1698 * @return string SVG file contents
1700 public function get_generated_svg_for_id(int $id): string {
1701 $color = $this->get_generated_color_for_id($id);
1702 $pattern = new \core_geopattern();
1703 $pattern->setColor($color);
1704 $pattern->patternbyid($id);
1705 return $pattern->toSVG();
1709 * Get the course color to show on a course card.
1711 * @param int $id Id to use when generating the color.
1712 * @return string hex color code.
1714 public function get_generated_color_for_id($id) {
1715 $colornumbers = range(1, 10);
1716 $basecolors = [];
1717 foreach ($colornumbers as $number) {
1718 $basecolors[] = get_config('core_admin', 'coursecolor' . $number);
1721 $color = $basecolors[$id % 10];
1722 return $color;
1726 * Returns lang menu or '', this method also checks forcing of languages in courses.
1728 * This function calls {@link core_renderer::render_single_select()} to actually display the language menu.
1730 * @return string The lang menu HTML or empty string
1732 public function lang_menu() {
1733 $languagemenu = new \core\output\language_menu($this->page);
1734 $data = $languagemenu->export_for_single_select($this);
1735 if ($data) {
1736 return $this->render_from_template('core/single_select', $data);
1738 return '';
1742 * Output the row of editing icons for a block, as defined by the controls array.
1744 * @param array $controls an array like {@link block_contents::$controls}.
1745 * @param string $blockid The ID given to the block.
1746 * @return string HTML fragment.
1748 public function block_controls($actions, $blockid = null) {
1749 if (empty($actions)) {
1750 return '';
1752 $menu = new action_menu($actions);
1753 if ($blockid !== null) {
1754 $menu->set_owner_selector('#'.$blockid);
1756 $menu->attributes['class'] .= ' block-control-actions commands';
1757 return $this->render($menu);
1761 * Returns the HTML for a basic textarea field.
1763 * @param string $name Name to use for the textarea element
1764 * @param string $id The id to use fort he textarea element
1765 * @param string $value Initial content to display in the textarea
1766 * @param int $rows Number of rows to display
1767 * @param int $cols Number of columns to display
1768 * @return string the HTML to display
1770 public function print_textarea($name, $id, $value, $rows, $cols) {
1771 editors_head_setup();
1772 $editor = editors_get_preferred_editor(FORMAT_HTML);
1773 $editor->set_text($value);
1774 $editor->use_editor($id, []);
1776 $context = [
1777 'id' => $id,
1778 'name' => $name,
1779 'value' => $value,
1780 'rows' => $rows,
1781 'cols' => $cols
1784 return $this->render_from_template('core_form/editor_textarea', $context);
1788 * Renders an action menu component.
1790 * @param action_menu $menu
1791 * @return string HTML
1793 public function render_action_menu(action_menu $menu) {
1795 // We don't want the class icon there!
1796 foreach ($menu->get_secondary_actions() as $action) {
1797 if ($action instanceof \action_menu_link && $action->has_class('icon')) {
1798 $action->attributes['class'] = preg_replace('/(^|\s+)icon(\s+|$)/i', '', $action->attributes['class']);
1802 if ($menu->is_empty()) {
1803 return '';
1805 $context = $menu->export_for_template($this);
1807 return $this->render_from_template('core/action_menu', $context);
1811 * Renders a full check API result including summary and details
1813 * @param core\check\check $check the check that was run to get details from
1814 * @param core\check\result $result the result of a check
1815 * @param bool $includedetails if true, details are included as well
1816 * @return string rendered html
1818 protected function render_check_full_result(core\check\check $check, core\check\result $result, bool $includedetails): string {
1819 // Initially render just badge itself.
1820 $renderedresult = $this->render_from_template($result->get_template_name(), $result->export_for_template($this));
1822 // Add summary.
1823 $renderedresult .= ' ' . $result->get_summary();
1825 // Wrap in notificaiton.
1826 $notificationmap = [
1827 \core\check\result::NA => \core\output\notification::NOTIFY_INFO,
1828 \core\check\result::OK => \core\output\notification::NOTIFY_SUCCESS,
1829 \core\check\result::INFO => \core\output\notification::NOTIFY_INFO,
1830 \core\check\result::UNKNOWN => \core\output\notification::NOTIFY_WARNING,
1831 \core\check\result::WARNING => \core\output\notification::NOTIFY_WARNING,
1832 \core\check\result::ERROR => \core\output\notification::NOTIFY_ERROR,
1833 \core\check\result::CRITICAL => \core\output\notification::NOTIFY_ERROR,
1836 // Get type, or default to error.
1837 $notificationtype = $notificationmap[$result->get_status()] ?? \core\output\notification::NOTIFY_ERROR;
1838 $renderedresult = $this->notification($renderedresult, $notificationtype, false);
1840 // If adding details, add on new line.
1841 if ($includedetails) {
1842 $renderedresult .= $result->get_details();
1845 // Add the action link.
1846 $renderedresult .= $this->render_action_link($check->get_action_link());
1848 return $renderedresult;
1852 * Renders a full check API result including summary and details
1854 * @param core\check\check $check the check that was run to get details from
1855 * @param core\check\result $result the result of a check
1856 * @param bool $includedetails if details should be included
1857 * @return string HTML fragment
1859 public function check_full_result(core\check\check $check, core\check\result $result, bool $includedetails = false) {
1860 return $this->render_check_full_result($check, $result, $includedetails);
1864 * Renders a Check API result
1866 * @param core\check\result $result
1867 * @return string HTML fragment
1869 protected function render_check_result(core\check\result $result) {
1870 return $this->render_from_template($result->get_template_name(), $result->export_for_template($this));
1874 * Renders a Check API result
1876 * @param core\check\result $result
1877 * @return string HTML fragment
1879 public function check_result(core\check\result $result) {
1880 return $this->render_check_result($result);
1884 * Renders an action_menu_link item.
1886 * @param action_menu_link $action
1887 * @return string HTML fragment
1889 protected function render_action_menu_link(action_menu_link $action) {
1890 return $this->render_from_template('core/action_menu_link', $action->export_for_template($this));
1894 * Renders a primary action_menu_filler item.
1896 * @param action_menu_filler $action
1897 * @return string HTML fragment
1899 protected function render_action_menu_filler(action_menu_filler $action) {
1900 return html_writer::span('&nbsp;', 'filler');
1904 * Renders a primary action_menu_link item.
1906 * @param action_menu_link_primary $action
1907 * @return string HTML fragment
1909 protected function render_action_menu_link_primary(action_menu_link_primary $action) {
1910 return $this->render_action_menu_link($action);
1914 * Renders a secondary action_menu_link item.
1916 * @param action_menu_link_secondary $action
1917 * @return string HTML fragment
1919 protected function render_action_menu_link_secondary(action_menu_link_secondary $action) {
1920 return $this->render_action_menu_link($action);
1924 * Prints a nice side block with an optional header.
1926 * @param block_contents $bc HTML for the content
1927 * @param string $region the region the block is appearing in.
1928 * @return string the HTML to be output.
1930 public function block(block_contents $bc, $region) {
1931 $bc = clone($bc); // Avoid messing up the object passed in.
1932 if (empty($bc->blockinstanceid) || !strip_tags($bc->title)) {
1933 $bc->collapsible = block_contents::NOT_HIDEABLE;
1936 $id = !empty($bc->attributes['id']) ? $bc->attributes['id'] : uniqid('block-');
1937 $context = new stdClass();
1938 $context->skipid = $bc->skipid;
1939 $context->blockinstanceid = $bc->blockinstanceid ?: uniqid('fakeid-');
1940 $context->dockable = $bc->dockable;
1941 $context->id = $id;
1942 $context->hidden = $bc->collapsible == block_contents::HIDDEN;
1943 $context->skiptitle = strip_tags($bc->title);
1944 $context->showskiplink = !empty($context->skiptitle);
1945 $context->arialabel = $bc->arialabel;
1946 $context->ariarole = !empty($bc->attributes['role']) ? $bc->attributes['role'] : 'complementary';
1947 $context->class = $bc->attributes['class'];
1948 $context->type = $bc->attributes['data-block'];
1949 $context->title = $bc->title;
1950 $context->content = $bc->content;
1951 $context->annotation = $bc->annotation;
1952 $context->footer = $bc->footer;
1953 $context->hascontrols = !empty($bc->controls);
1954 if ($context->hascontrols) {
1955 $context->controls = $this->block_controls($bc->controls, $id);
1958 return $this->render_from_template('core/block', $context);
1962 * Render the contents of a block_list.
1964 * @param array $icons the icon for each item.
1965 * @param array $items the content of each item.
1966 * @return string HTML
1968 public function list_block_contents($icons, $items) {
1969 $row = 0;
1970 $lis = array();
1971 foreach ($items as $key => $string) {
1972 $item = html_writer::start_tag('li', array('class' => 'r' . $row));
1973 if (!empty($icons[$key])) { //test if the content has an assigned icon
1974 $item .= html_writer::tag('div', $icons[$key], array('class' => 'icon column c0'));
1976 $item .= html_writer::tag('div', $string, array('class' => 'column c1'));
1977 $item .= html_writer::end_tag('li');
1978 $lis[] = $item;
1979 $row = 1 - $row; // Flip even/odd.
1981 return html_writer::tag('ul', implode("\n", $lis), array('class' => 'unlist'));
1985 * Output all the blocks in a particular region.
1987 * @param string $region the name of a region on this page.
1988 * @param boolean $fakeblocksonly Output fake block only.
1989 * @return string the HTML to be output.
1991 public function blocks_for_region($region, $fakeblocksonly = false) {
1992 $blockcontents = $this->page->blocks->get_content_for_region($region, $this);
1993 $lastblock = null;
1994 $zones = array();
1995 foreach ($blockcontents as $bc) {
1996 if ($bc instanceof block_contents) {
1997 $zones[] = $bc->title;
2000 $output = '';
2002 foreach ($blockcontents as $bc) {
2003 if ($bc instanceof block_contents) {
2004 if ($fakeblocksonly && !$bc->is_fake()) {
2005 // Skip rendering real blocks if we only want to show fake blocks.
2006 continue;
2008 $output .= $this->block($bc, $region);
2009 $lastblock = $bc->title;
2010 } else if ($bc instanceof block_move_target) {
2011 if (!$fakeblocksonly) {
2012 $output .= $this->block_move_target($bc, $zones, $lastblock, $region);
2014 } else {
2015 throw new coding_exception('Unexpected type of thing (' . get_class($bc) . ') found in list of block contents.');
2018 return $output;
2022 * Output a place where the block that is currently being moved can be dropped.
2024 * @param block_move_target $target with the necessary details.
2025 * @param array $zones array of areas where the block can be moved to
2026 * @param string $previous the block located before the area currently being rendered.
2027 * @param string $region the name of the region
2028 * @return string the HTML to be output.
2030 public function block_move_target($target, $zones, $previous, $region) {
2031 if ($previous == null) {
2032 if (empty($zones)) {
2033 // There are no zones, probably because there are no blocks.
2034 $regions = $this->page->theme->get_all_block_regions();
2035 $position = get_string('moveblockinregion', 'block', $regions[$region]);
2036 } else {
2037 $position = get_string('moveblockbefore', 'block', $zones[0]);
2039 } else {
2040 $position = get_string('moveblockafter', 'block', $previous);
2042 return html_writer::tag('a', html_writer::tag('span', $position, array('class' => 'accesshide')), array('href' => $target->url, 'class' => 'blockmovetarget'));
2046 * Renders a special html link with attached action
2048 * Theme developers: DO NOT OVERRIDE! Please override function
2049 * {@link core_renderer::render_action_link()} instead.
2051 * @param string|moodle_url $url
2052 * @param string $text HTML fragment
2053 * @param component_action $action
2054 * @param array $attributes associative array of html link attributes + disabled
2055 * @param pix_icon optional pix icon to render with the link
2056 * @return string HTML fragment
2058 public function action_link($url, $text, component_action $action = null, array $attributes = null, $icon = null) {
2059 if (!($url instanceof moodle_url)) {
2060 $url = new moodle_url($url);
2062 $link = new action_link($url, $text, $action, $attributes, $icon);
2064 return $this->render($link);
2068 * Renders an action_link object.
2070 * The provided link is renderer and the HTML returned. At the same time the
2071 * associated actions are setup in JS by {@link core_renderer::add_action_handler()}
2073 * @param action_link $link
2074 * @return string HTML fragment
2076 protected function render_action_link(action_link $link) {
2077 return $this->render_from_template('core/action_link', $link->export_for_template($this));
2081 * Renders an action_icon.
2083 * This function uses the {@link core_renderer::action_link()} method for the
2084 * most part. What it does different is prepare the icon as HTML and use it
2085 * as the link text.
2087 * Theme developers: If you want to change how action links and/or icons are rendered,
2088 * consider overriding function {@link core_renderer::render_action_link()} and
2089 * {@link core_renderer::render_pix_icon()}.
2091 * @param string|moodle_url $url A string URL or moodel_url
2092 * @param pix_icon $pixicon
2093 * @param component_action $action
2094 * @param array $attributes associative array of html link attributes + disabled
2095 * @param bool $linktext show title next to image in link
2096 * @return string HTML fragment
2098 public function action_icon($url, pix_icon $pixicon, component_action $action = null, array $attributes = null, $linktext=false) {
2099 if (!($url instanceof moodle_url)) {
2100 $url = new moodle_url($url);
2102 $attributes = (array)$attributes;
2104 if (empty($attributes['class'])) {
2105 // let ppl override the class via $options
2106 $attributes['class'] = 'action-icon';
2109 if ($linktext) {
2110 $text = $pixicon->attributes['alt'];
2111 // Set the icon as a decorative image if we're displaying the action text.
2112 // Otherwise, the action name will be read twice by assistive technologies.
2113 $pixicon->attributes['alt'] = '';
2114 $pixicon->attributes['title'] = '';
2115 $pixicon->attributes['aria-hidden'] = 'true';
2116 } else {
2117 $text = '';
2120 $icon = $this->render($pixicon);
2122 return $this->action_link($url, $text.$icon, $action, $attributes);
2126 * Print a message along with button choices for Continue/Cancel
2128 * If a string or moodle_url is given instead of a single_button, method defaults to post.
2130 * @param string $message The question to ask the user
2131 * @param single_button|moodle_url|string $continue The single_button component representing the Continue answer. Can also be a moodle_url or string URL
2132 * @param single_button|moodle_url|string $cancel The single_button component representing the Cancel answer. Can also be a moodle_url or string URL
2133 * @param array $displayoptions optional extra display options
2134 * @return string HTML fragment
2136 public function confirm($message, $continue, $cancel, array $displayoptions = []) {
2138 // Check existing displayoptions.
2139 $displayoptions['confirmtitle'] = $displayoptions['confirmtitle'] ?? get_string('confirm');
2140 $displayoptions['continuestr'] = $displayoptions['continuestr'] ?? get_string('continue');
2141 $displayoptions['cancelstr'] = $displayoptions['cancelstr'] ?? get_string('cancel');
2143 if ($continue instanceof single_button) {
2144 // Continue button should be primary if set to secondary type as it is the fefault.
2145 if ($continue->type === single_button::BUTTON_SECONDARY) {
2146 $continue->type = single_button::BUTTON_PRIMARY;
2148 } else if (is_string($continue)) {
2149 $continue = new single_button(new moodle_url($continue), $displayoptions['continuestr'], 'post',
2150 $displayoptions['type'] ?? single_button::BUTTON_PRIMARY);
2151 } else if ($continue instanceof moodle_url) {
2152 $continue = new single_button($continue, $displayoptions['continuestr'], 'post',
2153 $displayoptions['type'] ?? single_button::BUTTON_PRIMARY);
2154 } else {
2155 throw new coding_exception('The continue param to $OUTPUT->confirm() must be either a URL (string/moodle_url) or a single_button instance.');
2158 if ($cancel instanceof single_button) {
2159 // ok
2160 } else if (is_string($cancel)) {
2161 $cancel = new single_button(new moodle_url($cancel), $displayoptions['cancelstr'], 'get');
2162 } else if ($cancel instanceof moodle_url) {
2163 $cancel = new single_button($cancel, $displayoptions['cancelstr'], 'get');
2164 } else {
2165 throw new coding_exception('The cancel param to $OUTPUT->confirm() must be either a URL (string/moodle_url) or a single_button instance.');
2168 $attributes = [
2169 'role'=>'alertdialog',
2170 'aria-labelledby'=>'modal-header',
2171 'aria-describedby'=>'modal-body',
2172 'aria-modal'=>'true'
2175 $output = $this->box_start('generalbox modal modal-dialog modal-in-page show', 'notice', $attributes);
2176 $output .= $this->box_start('modal-content', 'modal-content');
2177 $output .= $this->box_start('modal-header px-3', 'modal-header');
2178 $output .= html_writer::tag('h4', $displayoptions['confirmtitle']);
2179 $output .= $this->box_end();
2180 $attributes = [
2181 'role'=>'alert',
2182 'data-aria-autofocus'=>'true'
2184 $output .= $this->box_start('modal-body', 'modal-body', $attributes);
2185 $output .= html_writer::tag('p', $message);
2186 $output .= $this->box_end();
2187 $output .= $this->box_start('modal-footer', 'modal-footer');
2188 $output .= html_writer::tag('div', $this->render($cancel) . $this->render($continue), ['class' => 'buttons']);
2189 $output .= $this->box_end();
2190 $output .= $this->box_end();
2191 $output .= $this->box_end();
2192 return $output;
2196 * Returns a form with a single button.
2198 * Theme developers: DO NOT OVERRIDE! Please override function
2199 * {@link core_renderer::render_single_button()} instead.
2201 * @param string|moodle_url $url
2202 * @param string $label button text
2203 * @param string $method get or post submit method
2204 * @param array $options associative array {disabled, title, etc.}
2205 * @return string HTML fragment
2207 public function single_button($url, $label, $method='post', array $options=null) {
2208 if (!($url instanceof moodle_url)) {
2209 $url = new moodle_url($url);
2211 $button = new single_button($url, $label, $method);
2213 foreach ((array)$options as $key=>$value) {
2214 if (property_exists($button, $key)) {
2215 $button->$key = $value;
2216 } else {
2217 $button->set_attribute($key, $value);
2221 return $this->render($button);
2225 * Renders a single button widget.
2227 * This will return HTML to display a form containing a single button.
2229 * @param single_button $button
2230 * @return string HTML fragment
2232 protected function render_single_button(single_button $button) {
2233 return $this->render_from_template('core/single_button', $button->export_for_template($this));
2237 * Returns a form with a single select widget.
2239 * Theme developers: DO NOT OVERRIDE! Please override function
2240 * {@link core_renderer::render_single_select()} instead.
2242 * @param moodle_url $url form action target, includes hidden fields
2243 * @param string $name name of selection field - the changing parameter in url
2244 * @param array $options list of options
2245 * @param string $selected selected element
2246 * @param array $nothing
2247 * @param string $formid
2248 * @param array $attributes other attributes for the single select
2249 * @return string HTML fragment
2251 public function single_select($url, $name, array $options, $selected = '',
2252 $nothing = array('' => 'choosedots'), $formid = null, $attributes = array()) {
2253 if (!($url instanceof moodle_url)) {
2254 $url = new moodle_url($url);
2256 $select = new single_select($url, $name, $options, $selected, $nothing, $formid);
2258 if (array_key_exists('label', $attributes)) {
2259 $select->set_label($attributes['label']);
2260 unset($attributes['label']);
2262 $select->attributes = $attributes;
2264 return $this->render($select);
2268 * Returns a dataformat selection and download form
2270 * @param string $label A text label
2271 * @param moodle_url|string $base The download page url
2272 * @param string $name The query param which will hold the type of the download
2273 * @param array $params Extra params sent to the download page
2274 * @return string HTML fragment
2276 public function download_dataformat_selector($label, $base, $name = 'dataformat', $params = array()) {
2278 $formats = core_plugin_manager::instance()->get_plugins_of_type('dataformat');
2279 $options = array();
2280 foreach ($formats as $format) {
2281 if ($format->is_enabled()) {
2282 $options[] = array(
2283 'value' => $format->name,
2284 'label' => get_string('dataformat', $format->component),
2288 $hiddenparams = array();
2289 foreach ($params as $key => $value) {
2290 $hiddenparams[] = array(
2291 'name' => $key,
2292 'value' => $value,
2295 $data = array(
2296 'label' => $label,
2297 'base' => $base,
2298 'name' => $name,
2299 'params' => $hiddenparams,
2300 'options' => $options,
2301 'sesskey' => sesskey(),
2302 'submit' => get_string('download'),
2305 return $this->render_from_template('core/dataformat_selector', $data);
2310 * Internal implementation of single_select rendering
2312 * @param single_select $select
2313 * @return string HTML fragment
2315 protected function render_single_select(single_select $select) {
2316 return $this->render_from_template('core/single_select', $select->export_for_template($this));
2320 * Returns a form with a url select widget.
2322 * Theme developers: DO NOT OVERRIDE! Please override function
2323 * {@link core_renderer::render_url_select()} instead.
2325 * @param array $urls list of urls - array('/course/view.php?id=1'=>'Frontpage', ....)
2326 * @param string $selected selected element
2327 * @param array $nothing
2328 * @param string $formid
2329 * @return string HTML fragment
2331 public function url_select(array $urls, $selected, $nothing = array('' => 'choosedots'), $formid = null) {
2332 $select = new url_select($urls, $selected, $nothing, $formid);
2333 return $this->render($select);
2337 * Internal implementation of url_select rendering
2339 * @param url_select $select
2340 * @return string HTML fragment
2342 protected function render_url_select(url_select $select) {
2343 return $this->render_from_template('core/url_select', $select->export_for_template($this));
2347 * Returns a string containing a link to the user documentation.
2348 * Also contains an icon by default. Shown to teachers and admin only.
2350 * @param string $path The page link after doc root and language, no leading slash.
2351 * @param string $text The text to be displayed for the link
2352 * @param boolean $forcepopup Whether to force a popup regardless of the value of $CFG->doctonewwindow
2353 * @param array $attributes htm attributes
2354 * @return string
2356 public function doc_link($path, $text = '', $forcepopup = false, array $attributes = []) {
2357 global $CFG;
2359 $icon = $this->pix_icon('book', '', 'moodle', array('class' => 'iconhelp icon-pre'));
2361 $attributes['href'] = new moodle_url(get_docs_url($path));
2362 $newwindowicon = '';
2363 if (!empty($CFG->doctonewwindow) || $forcepopup) {
2364 $attributes['target'] = '_blank';
2365 $newwindowicon = $this->pix_icon('i/externallink', get_string('opensinnewwindow'), 'moodle',
2366 ['class' => 'fa fa-externallink fa-fw']);
2369 return html_writer::tag('a', $icon . $text . $newwindowicon, $attributes);
2373 * Return HTML for an image_icon.
2375 * Theme developers: DO NOT OVERRIDE! Please override function
2376 * {@link core_renderer::render_image_icon()} instead.
2378 * @param string $pix short pix name
2379 * @param string $alt mandatory alt attribute
2380 * @param string $component standard compoennt name like 'moodle', 'mod_forum', etc.
2381 * @param array $attributes htm attributes
2382 * @return string HTML fragment
2384 public function image_icon($pix, $alt, $component='moodle', array $attributes = null) {
2385 $icon = new image_icon($pix, $alt, $component, $attributes);
2386 return $this->render($icon);
2390 * Renders a pix_icon widget and returns the HTML to display it.
2392 * @param image_icon $icon
2393 * @return string HTML fragment
2395 protected function render_image_icon(image_icon $icon) {
2396 $system = \core\output\icon_system::instance(\core\output\icon_system::STANDARD);
2397 return $system->render_pix_icon($this, $icon);
2401 * Return HTML for a pix_icon.
2403 * Theme developers: DO NOT OVERRIDE! Please override function
2404 * {@link core_renderer::render_pix_icon()} instead.
2406 * @param string $pix short pix name
2407 * @param string $alt mandatory alt attribute
2408 * @param string $component standard compoennt name like 'moodle', 'mod_forum', etc.
2409 * @param array $attributes htm lattributes
2410 * @return string HTML fragment
2412 public function pix_icon($pix, $alt, $component='moodle', array $attributes = null) {
2413 $icon = new pix_icon($pix, $alt, $component, $attributes);
2414 return $this->render($icon);
2418 * Renders a pix_icon widget and returns the HTML to display it.
2420 * @param pix_icon $icon
2421 * @return string HTML fragment
2423 protected function render_pix_icon(pix_icon $icon) {
2424 $system = \core\output\icon_system::instance();
2425 return $system->render_pix_icon($this, $icon);
2429 * Return HTML to display an emoticon icon.
2431 * @param pix_emoticon $emoticon
2432 * @return string HTML fragment
2434 protected function render_pix_emoticon(pix_emoticon $emoticon) {
2435 $system = \core\output\icon_system::instance(\core\output\icon_system::STANDARD);
2436 return $system->render_pix_icon($this, $emoticon);
2440 * Produces the html that represents this rating in the UI
2442 * @param rating $rating the page object on which this rating will appear
2443 * @return string
2445 function render_rating(rating $rating) {
2446 global $CFG, $USER;
2448 if ($rating->settings->aggregationmethod == RATING_AGGREGATE_NONE) {
2449 return null;//ratings are turned off
2452 $ratingmanager = new rating_manager();
2453 // Initialise the JavaScript so ratings can be done by AJAX.
2454 $ratingmanager->initialise_rating_javascript($this->page);
2456 $strrate = get_string("rate", "rating");
2457 $ratinghtml = ''; //the string we'll return
2459 // permissions check - can they view the aggregate?
2460 if ($rating->user_can_view_aggregate()) {
2462 $aggregatelabel = $ratingmanager->get_aggregate_label($rating->settings->aggregationmethod);
2463 $aggregatelabel = html_writer::tag('span', $aggregatelabel, array('class'=>'rating-aggregate-label'));
2464 $aggregatestr = $rating->get_aggregate_string();
2466 $aggregatehtml = html_writer::tag('span', $aggregatestr, array('id' => 'ratingaggregate'.$rating->itemid, 'class' => 'ratingaggregate')).' ';
2467 if ($rating->count > 0) {
2468 $countstr = "({$rating->count})";
2469 } else {
2470 $countstr = '-';
2472 $aggregatehtml .= html_writer::tag('span', $countstr, array('id'=>"ratingcount{$rating->itemid}", 'class' => 'ratingcount')).' ';
2474 if ($rating->settings->permissions->viewall && $rating->settings->pluginpermissions->viewall) {
2476 $nonpopuplink = $rating->get_view_ratings_url();
2477 $popuplink = $rating->get_view_ratings_url(true);
2479 $action = new popup_action('click', $popuplink, 'ratings', array('height' => 400, 'width' => 600));
2480 $aggregatehtml = $this->action_link($nonpopuplink, $aggregatehtml, $action);
2483 $ratinghtml .= html_writer::tag('span', $aggregatelabel . $aggregatehtml, array('class' => 'rating-aggregate-container'));
2486 $formstart = null;
2487 // if the item doesn't belong to the current user, the user has permission to rate
2488 // and we're within the assessable period
2489 if ($rating->user_can_rate()) {
2491 $rateurl = $rating->get_rate_url();
2492 $inputs = $rateurl->params();
2494 //start the rating form
2495 $formattrs = array(
2496 'id' => "postrating{$rating->itemid}",
2497 'class' => 'postratingform',
2498 'method' => 'post',
2499 'action' => $rateurl->out_omit_querystring()
2501 $formstart = html_writer::start_tag('form', $formattrs);
2502 $formstart .= html_writer::start_tag('div', array('class' => 'ratingform'));
2504 // add the hidden inputs
2505 foreach ($inputs as $name => $value) {
2506 $attributes = array('type' => 'hidden', 'class' => 'ratinginput', 'name' => $name, 'value' => $value);
2507 $formstart .= html_writer::empty_tag('input', $attributes);
2510 if (empty($ratinghtml)) {
2511 $ratinghtml .= $strrate.': ';
2513 $ratinghtml = $formstart.$ratinghtml;
2515 $scalearray = array(RATING_UNSET_RATING => $strrate.'...') + $rating->settings->scale->scaleitems;
2516 $scaleattrs = array('class'=>'postratingmenu ratinginput','id'=>'menurating'.$rating->itemid);
2517 $ratinghtml .= html_writer::label($rating->rating, 'menurating'.$rating->itemid, false, array('class' => 'accesshide'));
2518 $ratinghtml .= html_writer::select($scalearray, 'rating', $rating->rating, false, $scaleattrs);
2520 //output submit button
2521 $ratinghtml .= html_writer::start_tag('span', array('class'=>"ratingsubmit"));
2523 $attributes = array('type' => 'submit', 'class' => 'postratingmenusubmit', 'id' => 'postratingsubmit'.$rating->itemid, 'value' => s(get_string('rate', 'rating')));
2524 $ratinghtml .= html_writer::empty_tag('input', $attributes);
2526 if (!$rating->settings->scale->isnumeric) {
2527 // If a global scale, try to find current course ID from the context
2528 if (empty($rating->settings->scale->courseid) and $coursecontext = $rating->context->get_course_context(false)) {
2529 $courseid = $coursecontext->instanceid;
2530 } else {
2531 $courseid = $rating->settings->scale->courseid;
2533 $ratinghtml .= $this->help_icon_scale($courseid, $rating->settings->scale);
2535 $ratinghtml .= html_writer::end_tag('span');
2536 $ratinghtml .= html_writer::end_tag('div');
2537 $ratinghtml .= html_writer::end_tag('form');
2540 return $ratinghtml;
2544 * Centered heading with attached help button (same title text)
2545 * and optional icon attached.
2547 * @param string $text A heading text
2548 * @param string $helpidentifier The keyword that defines a help page
2549 * @param string $component component name
2550 * @param string|moodle_url $icon
2551 * @param string $iconalt icon alt text
2552 * @param int $level The level of importance of the heading. Defaulting to 2
2553 * @param string $classnames A space-separated list of CSS classes. Defaulting to null
2554 * @return string HTML fragment
2556 public function heading_with_help($text, $helpidentifier, $component = 'moodle', $icon = '', $iconalt = '', $level = 2, $classnames = null) {
2557 $image = '';
2558 if ($icon) {
2559 $image = $this->pix_icon($icon, $iconalt, $component, array('class'=>'icon iconlarge'));
2562 $help = '';
2563 if ($helpidentifier) {
2564 $help = $this->help_icon($helpidentifier, $component);
2567 return $this->heading($image.$text.$help, $level, $classnames);
2571 * Returns HTML to display a help icon.
2573 * @deprecated since Moodle 2.0
2575 public function old_help_icon($helpidentifier, $title, $component = 'moodle', $linktext = '') {
2576 throw new coding_exception('old_help_icon() can not be used any more, please see help_icon().');
2580 * Returns HTML to display a help icon.
2582 * Theme developers: DO NOT OVERRIDE! Please override function
2583 * {@link core_renderer::render_help_icon()} instead.
2585 * @param string $identifier The keyword that defines a help page
2586 * @param string $component component name
2587 * @param string|bool $linktext true means use $title as link text, string means link text value
2588 * @param string|object|array|int $a An object, string or number that can be used
2589 * within translation strings
2590 * @return string HTML fragment
2592 public function help_icon($identifier, $component = 'moodle', $linktext = '', $a = null) {
2593 $icon = new help_icon($identifier, $component, $a);
2594 $icon->diag_strings();
2595 if ($linktext === true) {
2596 $icon->linktext = get_string($icon->identifier, $icon->component, $a);
2597 } else if (!empty($linktext)) {
2598 $icon->linktext = $linktext;
2600 return $this->render($icon);
2604 * Implementation of user image rendering.
2606 * @param help_icon $helpicon A help icon instance
2607 * @return string HTML fragment
2609 protected function render_help_icon(help_icon $helpicon) {
2610 $context = $helpicon->export_for_template($this);
2611 return $this->render_from_template('core/help_icon', $context);
2615 * Returns HTML to display a scale help icon.
2617 * @param int $courseid
2618 * @param stdClass $scale instance
2619 * @return string HTML fragment
2621 public function help_icon_scale($courseid, stdClass $scale) {
2622 global $CFG;
2624 $title = get_string('helpprefix2', '', $scale->name) .' ('.get_string('newwindow').')';
2626 $icon = $this->pix_icon('help', get_string('scales'), 'moodle', array('class'=>'iconhelp'));
2628 $scaleid = abs($scale->id);
2630 $link = new moodle_url('/course/scales.php', array('id' => $courseid, 'list' => true, 'scaleid' => $scaleid));
2631 $action = new popup_action('click', $link, 'ratingscale');
2633 return html_writer::tag('span', $this->action_link($link, $icon, $action), array('class' => 'helplink'));
2637 * Creates and returns a spacer image with optional line break.
2639 * @param array $attributes Any HTML attributes to add to the spaced.
2640 * @param bool $br Include a BR after the spacer.... DON'T USE THIS. Don't be
2641 * laxy do it with CSS which is a much better solution.
2642 * @return string HTML fragment
2644 public function spacer(array $attributes = null, $br = false) {
2645 $attributes = (array)$attributes;
2646 if (empty($attributes['width'])) {
2647 $attributes['width'] = 1;
2649 if (empty($attributes['height'])) {
2650 $attributes['height'] = 1;
2652 $attributes['class'] = 'spacer';
2654 $output = $this->pix_icon('spacer', '', 'moodle', $attributes);
2656 if (!empty($br)) {
2657 $output .= '<br />';
2660 return $output;
2664 * Returns HTML to display the specified user's avatar.
2666 * User avatar may be obtained in two ways:
2667 * <pre>
2668 * // Option 1: (shortcut for simple cases, preferred way)
2669 * // $user has come from the DB and has fields id, picture, imagealt, firstname and lastname
2670 * $OUTPUT->user_picture($user, array('popup'=>true));
2672 * // Option 2:
2673 * $userpic = new user_picture($user);
2674 * // Set properties of $userpic
2675 * $userpic->popup = true;
2676 * $OUTPUT->render($userpic);
2677 * </pre>
2679 * Theme developers: DO NOT OVERRIDE! Please override function
2680 * {@link core_renderer::render_user_picture()} instead.
2682 * @param stdClass $user Object with at least fields id, picture, imagealt, firstname, lastname
2683 * If any of these are missing, the database is queried. Avoid this
2684 * if at all possible, particularly for reports. It is very bad for performance.
2685 * @param array $options associative array with user picture options, used only if not a user_picture object,
2686 * options are:
2687 * - courseid=$this->page->course->id (course id of user profile in link)
2688 * - size=35 (size of image)
2689 * - link=true (make image clickable - the link leads to user profile)
2690 * - popup=false (open in popup)
2691 * - alttext=true (add image alt attribute)
2692 * - class = image class attribute (default 'userpicture')
2693 * - visibletoscreenreaders=true (whether to be visible to screen readers)
2694 * - includefullname=false (whether to include the user's full name together with the user picture)
2695 * - includetoken = false (whether to use a token for authentication. True for current user, int value for other user id)
2696 * @return string HTML fragment
2698 public function user_picture(stdClass $user, array $options = null) {
2699 $userpicture = new user_picture($user);
2700 foreach ((array)$options as $key=>$value) {
2701 if (property_exists($userpicture, $key)) {
2702 $userpicture->$key = $value;
2705 return $this->render($userpicture);
2709 * Internal implementation of user image rendering.
2711 * @param user_picture $userpicture
2712 * @return string
2714 protected function render_user_picture(user_picture $userpicture) {
2715 global $CFG;
2717 $user = $userpicture->user;
2718 $canviewfullnames = has_capability('moodle/site:viewfullnames', $this->page->context);
2720 $alt = '';
2721 if ($userpicture->alttext) {
2722 if (!empty($user->imagealt)) {
2723 $alt = trim($user->imagealt);
2727 // If the user picture is being rendered as a link but without the full name, an empty alt text for the user picture
2728 // would mean that the link displayed will not have any discernible text. This becomes an accessibility issue,
2729 // especially to screen reader users. Use the user's full name by default for the user picture's alt-text if this is
2730 // the case.
2731 if ($userpicture->link && !$userpicture->includefullname && empty($alt)) {
2732 $alt = fullname($user);
2735 if (empty($userpicture->size)) {
2736 $size = 35;
2737 } else if ($userpicture->size === true or $userpicture->size == 1) {
2738 $size = 100;
2739 } else {
2740 $size = $userpicture->size;
2743 $class = $userpicture->class;
2745 if ($user->picture == 0) {
2746 $class .= ' defaultuserpic';
2749 $src = $userpicture->get_url($this->page, $this);
2751 $attributes = array('src' => $src, 'class' => $class, 'width' => $size, 'height' => $size);
2752 if (!$userpicture->visibletoscreenreaders) {
2753 $alt = '';
2755 $attributes['alt'] = $alt;
2757 if (!empty($alt)) {
2758 $attributes['title'] = $alt;
2761 // Get the image html output first, auto generated based on initials if one isn't already set.
2762 if ($user->picture == 0 && empty($CFG->enablegravatar) && !defined('BEHAT_SITE_RUNNING')) {
2763 $initials = \core_user::get_initials($user);
2764 $fullname = fullname($userpicture->user, $canviewfullnames);
2765 // Don't modify in corner cases where neither the firstname nor the lastname appears.
2766 $output = html_writer::tag(
2767 'span', $initials,
2769 'class' => 'userinitials size-' . $size,
2770 'title' => $fullname,
2771 'aria-label' => $fullname,
2772 'role' => 'img',
2775 } else {
2776 $output = html_writer::empty_tag('img', $attributes);
2779 // Show fullname together with the picture when desired.
2780 if ($userpicture->includefullname) {
2781 $output .= fullname($userpicture->user, $canviewfullnames);
2784 if (empty($userpicture->courseid)) {
2785 $courseid = $this->page->course->id;
2786 } else {
2787 $courseid = $userpicture->courseid;
2789 if ($courseid == SITEID) {
2790 $url = new moodle_url('/user/profile.php', array('id' => $user->id));
2791 } else {
2792 $url = new moodle_url('/user/view.php', array('id' => $user->id, 'course' => $courseid));
2795 // Then wrap it in link if needed. Also we don't wrap it in link if the link redirects to itself.
2796 if (!$userpicture->link ||
2797 ($this->page->has_set_url() && $this->page->url == $url)) { // Protect against unset page->url.
2798 return $output;
2801 $attributes = array('href' => $url, 'class' => 'd-inline-block aabtn');
2802 if (!$userpicture->visibletoscreenreaders) {
2803 $attributes['tabindex'] = '-1';
2804 $attributes['aria-hidden'] = 'true';
2807 if ($userpicture->popup) {
2808 $id = html_writer::random_id('userpicture');
2809 $attributes['id'] = $id;
2810 $this->add_action_handler(new popup_action('click', $url), $id);
2813 return html_writer::tag('a', $output, $attributes);
2817 * @deprecated since Moodle 4.3
2819 public function htmllize_file_tree() {
2820 throw new coding_exception('This function is deprecated and no longer relevant.');
2824 * Returns HTML to display the file picker
2826 * <pre>
2827 * $OUTPUT->file_picker($options);
2828 * </pre>
2830 * Theme developers: DO NOT OVERRIDE! Please override function
2831 * {@link core_renderer::render_file_picker()} instead.
2833 * @param stdClass $options file manager options
2834 * options are:
2835 * maxbytes=>-1,
2836 * itemid=>0,
2837 * client_id=>uniqid(),
2838 * acepted_types=>'*',
2839 * return_types=>FILE_INTERNAL,
2840 * context=>current page context
2841 * @return string HTML fragment
2843 public function file_picker($options) {
2844 $fp = new file_picker($options);
2845 return $this->render($fp);
2849 * Internal implementation of file picker rendering.
2851 * @param file_picker $fp
2852 * @return string
2854 public function render_file_picker(file_picker $fp) {
2855 $options = $fp->options;
2856 $client_id = $options->client_id;
2857 $strsaved = get_string('filesaved', 'repository');
2858 $straddfile = get_string('openpicker', 'repository');
2859 $strloading = get_string('loading', 'repository');
2860 $strdndenabled = get_string('dndenabled_inbox', 'moodle');
2861 $strdroptoupload = get_string('droptoupload', 'moodle');
2862 $iconprogress = $this->pix_icon('i/loading_small', $strloading).'';
2864 $currentfile = $options->currentfile;
2865 if (empty($currentfile)) {
2866 $currentfile = '';
2867 } else {
2868 $currentfile .= ' - ';
2870 if ($options->maxbytes) {
2871 $size = $options->maxbytes;
2872 } else {
2873 $size = get_max_upload_file_size();
2875 if ($size == -1) {
2876 $maxsize = '';
2877 } else {
2878 $maxsize = get_string('maxfilesize', 'moodle', display_size($size, 0));
2880 if ($options->buttonname) {
2881 $buttonname = ' name="' . $options->buttonname . '"';
2882 } else {
2883 $buttonname = '';
2885 $html = <<<EOD
2886 <div class="filemanager-loading mdl-align" id='filepicker-loading-{$client_id}'>
2887 $iconprogress
2888 </div>
2889 <div id="filepicker-wrapper-{$client_id}" class="mdl-left w-100" style="display:none">
2890 <div>
2891 <input type="button" class="btn btn-secondary fp-btn-choose" id="filepicker-button-{$client_id}" value="{$straddfile}"{$buttonname}/>
2892 <span> $maxsize </span>
2893 </div>
2894 EOD;
2895 if ($options->env != 'url') {
2896 $html .= <<<EOD
2897 <div id="file_info_{$client_id}" class="mdl-left filepicker-filelist" style="position: relative">
2898 <div class="filepicker-filename">
2899 <div class="filepicker-container">$currentfile
2900 <div class="dndupload-message">$strdndenabled <br/>
2901 <div class="dndupload-arrow d-flex"><i class="fa fa-arrow-circle-o-down fa-3x m-auto"></i></div>
2902 </div>
2903 </div>
2904 <div class="dndupload-progressbars"></div>
2905 </div>
2906 <div>
2907 <div class="dndupload-target">{$strdroptoupload}<br/>
2908 <div class="dndupload-arrow d-flex"><i class="fa fa-arrow-circle-o-down fa-3x m-auto"></i></div>
2909 </div>
2910 </div>
2911 </div>
2912 EOD;
2914 $html .= '</div>';
2915 return $html;
2919 * @deprecated since Moodle 3.2
2921 public function update_module_button() {
2922 throw new coding_exception('core_renderer::update_module_button() can not be used anymore. Activity ' .
2923 'modules should not add the edit module button, the link is already available in the Administration block. ' .
2924 'Themes can choose to display the link in the buttons row consistently for all module types.');
2928 * Returns HTML to display a "Turn editing on/off" button in a form.
2930 * @param moodle_url $url The URL + params to send through when clicking the button
2931 * @param string $method
2932 * @return ?string HTML the button
2934 public function edit_button(moodle_url $url, string $method = 'post') {
2936 if ($this->page->theme->haseditswitch == true) {
2937 return;
2939 $url->param('sesskey', sesskey());
2940 if ($this->page->user_is_editing()) {
2941 $url->param('edit', 'off');
2942 $editstring = get_string('turneditingoff');
2943 } else {
2944 $url->param('edit', 'on');
2945 $editstring = get_string('turneditingon');
2948 return $this->single_button($url, $editstring, $method);
2952 * Create a navbar switch for toggling editing mode.
2954 * @return ?string Html containing the edit switch
2956 public function edit_switch() {
2957 if ($this->page->user_allowed_editing()) {
2959 $temp = (object) [
2960 'legacyseturl' => (new moodle_url('/editmode.php'))->out(false),
2961 'pagecontextid' => $this->page->context->id,
2962 'pageurl' => $this->page->url,
2963 'sesskey' => sesskey(),
2965 if ($this->page->user_is_editing()) {
2966 $temp->checked = true;
2968 return $this->render_from_template('core/editswitch', $temp);
2973 * Returns HTML to display a simple button to close a window
2975 * @param string $text The lang string for the button's label (already output from get_string())
2976 * @return string html fragment
2978 public function close_window_button($text='') {
2979 if (empty($text)) {
2980 $text = get_string('closewindow');
2982 $button = new single_button(new moodle_url('#'), $text, 'get');
2983 $button->add_action(new component_action('click', 'close_window'));
2985 return $this->container($this->render($button), 'closewindow');
2989 * Output an error message. By default wraps the error message in <span class="error">.
2990 * If the error message is blank, nothing is output.
2992 * @param string $message the error message.
2993 * @return string the HTML to output.
2995 public function error_text($message) {
2996 if (empty($message)) {
2997 return '';
2999 $message = $this->pix_icon('i/warning', get_string('error'), '', array('class' => 'icon icon-pre', 'title'=>'')) . $message;
3000 return html_writer::tag('span', $message, array('class' => 'error'));
3004 * Do not call this function directly.
3006 * To terminate the current script with a fatal error, throw an exception.
3007 * Doing this will then call this function to display the error, before terminating the execution.
3009 * @param string $message The message to output
3010 * @param string $moreinfourl URL where more info can be found about the error
3011 * @param string $link Link for the Continue button
3012 * @param array $backtrace The execution backtrace
3013 * @param string $debuginfo Debugging information
3014 * @return string the HTML to output.
3016 public function fatal_error($message, $moreinfourl, $link, $backtrace, $debuginfo = null, $errorcode = "") {
3017 global $CFG;
3019 $output = '';
3020 $obbuffer = '';
3022 if ($this->has_started()) {
3023 // we can not always recover properly here, we have problems with output buffering,
3024 // html tables, etc.
3025 $output .= $this->opencontainers->pop_all_but_last();
3027 } else {
3028 // It is really bad if library code throws exception when output buffering is on,
3029 // because the buffered text would be printed before our start of page.
3030 // NOTE: this hack might be behave unexpectedly in case output buffering is enabled in PHP.ini
3031 error_reporting(0); // disable notices from gzip compression, etc.
3032 while (ob_get_level() > 0) {
3033 $buff = ob_get_clean();
3034 if ($buff === false) {
3035 break;
3037 $obbuffer .= $buff;
3039 error_reporting($CFG->debug);
3041 // Output not yet started.
3042 $protocol = (isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0');
3043 if (empty($_SERVER['HTTP_RANGE'])) {
3044 @header($protocol . ' 404 Not Found');
3045 } else if (core_useragent::check_safari_ios_version(602) && !empty($_SERVER['HTTP_X_PLAYBACK_SESSION_ID'])) {
3046 // Coax iOS 10 into sending the session cookie.
3047 @header($protocol . ' 403 Forbidden');
3048 } else {
3049 // Must stop byteserving attempts somehow,
3050 // this is weird but Chrome PDF viewer can be stopped only with 407!
3051 @header($protocol . ' 407 Proxy Authentication Required');
3054 $this->page->set_context(null); // ugly hack - make sure page context is set to something, we do not want bogus warnings here
3055 $this->page->set_url('/'); // no url
3056 //$this->page->set_pagelayout('base'); //TODO: MDL-20676 blocks on error pages are weird, unfortunately it somehow detect the pagelayout from URL :-(
3057 $this->page->set_title(get_string('error'));
3058 $this->page->set_heading($this->page->course->fullname);
3059 // No need to display the activity header when encountering an error.
3060 $this->page->activityheader->disable();
3061 $output .= $this->header();
3064 $message = '<p class="errormessage">' . s($message) . '</p>'.
3065 '<p class="errorcode"><a href="' . s($moreinfourl) . '">' .
3066 get_string('moreinformation') . '</a></p>';
3067 if (empty($CFG->rolesactive)) {
3068 $message .= '<p class="errormessage">' . get_string('installproblem', 'error') . '</p>';
3069 //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.
3071 $output .= $this->box($message, 'errorbox alert alert-danger', null, array('data-rel' => 'fatalerror'));
3073 if ($CFG->debugdeveloper) {
3074 $labelsep = get_string('labelsep', 'langconfig');
3075 if (!empty($debuginfo)) {
3076 $debuginfo = s($debuginfo); // removes all nasty JS
3077 $debuginfo = str_replace("\n", '<br />', $debuginfo); // keep newlines
3078 $label = get_string('debuginfo', 'debug') . $labelsep;
3079 $output .= $this->notification("<strong>$label</strong> " . $debuginfo, 'notifytiny');
3081 if (!empty($backtrace)) {
3082 $label = get_string('stacktrace', 'debug') . $labelsep;
3083 $output .= $this->notification("<strong>$label</strong> " . format_backtrace($backtrace), 'notifytiny');
3085 if ($obbuffer !== '' ) {
3086 $label = get_string('outputbuffer', 'debug') . $labelsep;
3087 $output .= $this->notification("<strong>$label</strong> " . s($obbuffer), 'notifytiny');
3091 if (empty($CFG->rolesactive)) {
3092 // continue does not make much sense if moodle is not installed yet because error is most probably not recoverable
3093 } else if (!empty($link)) {
3094 $output .= $this->continue_button($link);
3097 $output .= $this->footer();
3099 // Padding to encourage IE to display our error page, rather than its own.
3100 $output .= str_repeat(' ', 512);
3102 return $output;
3106 * Output a notification (that is, a status message about something that has just happened).
3108 * Note: \core\notification::add() may be more suitable for your usage.
3110 * @param string $message The message to print out.
3111 * @param ?string $type The type of notification. See constants on \core\output\notification.
3112 * @param bool $closebutton Whether to show a close icon to remove the notification (default true).
3113 * @return string the HTML to output.
3115 public function notification($message, $type = null, $closebutton = true) {
3116 $typemappings = [
3117 // Valid types.
3118 'success' => \core\output\notification::NOTIFY_SUCCESS,
3119 'info' => \core\output\notification::NOTIFY_INFO,
3120 'warning' => \core\output\notification::NOTIFY_WARNING,
3121 'error' => \core\output\notification::NOTIFY_ERROR,
3123 // Legacy types mapped to current types.
3124 'notifyproblem' => \core\output\notification::NOTIFY_ERROR,
3125 'notifytiny' => \core\output\notification::NOTIFY_ERROR,
3126 'notifyerror' => \core\output\notification::NOTIFY_ERROR,
3127 'notifysuccess' => \core\output\notification::NOTIFY_SUCCESS,
3128 'notifymessage' => \core\output\notification::NOTIFY_INFO,
3129 'notifyredirect' => \core\output\notification::NOTIFY_INFO,
3130 'redirectmessage' => \core\output\notification::NOTIFY_INFO,
3133 $extraclasses = [];
3135 if ($type) {
3136 if (strpos($type, ' ') === false) {
3137 // No spaces in the list of classes, therefore no need to loop over and determine the class.
3138 if (isset($typemappings[$type])) {
3139 $type = $typemappings[$type];
3140 } else {
3141 // The value provided did not match a known type. It must be an extra class.
3142 $extraclasses = [$type];
3144 } else {
3145 // Identify what type of notification this is.
3146 $classarray = explode(' ', self::prepare_classes($type));
3148 // Separate out the type of notification from the extra classes.
3149 foreach ($classarray as $class) {
3150 if (isset($typemappings[$class])) {
3151 $type = $typemappings[$class];
3152 } else {
3153 $extraclasses[] = $class;
3159 $notification = new \core\output\notification($message, $type, $closebutton);
3160 if (count($extraclasses)) {
3161 $notification->set_extra_classes($extraclasses);
3164 // Return the rendered template.
3165 return $this->render_from_template($notification->get_template_name(), $notification->export_for_template($this));
3169 * @deprecated since Moodle 3.1 MDL-30811 - please do not use this function any more.
3171 public function notify_problem() {
3172 throw new coding_exception('core_renderer::notify_problem() can not be used any more, '.
3173 'please use \core\notification::add(), or \core\output\notification as required.');
3177 * @deprecated since Moodle 3.1 MDL-30811 - please do not use this function any more.
3179 public function notify_success() {
3180 throw new coding_exception('core_renderer::notify_success() can not be used any more, '.
3181 'please use \core\notification::add(), or \core\output\notification as required.');
3185 * @deprecated since Moodle 3.1 MDL-30811 - please do not use this function any more.
3187 public function notify_message() {
3188 throw new coding_exception('core_renderer::notify_message() can not be used any more, '.
3189 'please use \core\notification::add(), or \core\output\notification as required.');
3193 * @deprecated since Moodle 3.1 MDL-30811 - please do not use this function any more.
3195 public function notify_redirect() {
3196 throw new coding_exception('core_renderer::notify_redirect() can not be used any more, '.
3197 'please use \core\notification::add(), or \core\output\notification as required.');
3201 * Render a notification (that is, a status message about something that has
3202 * just happened).
3204 * @param \core\output\notification $notification the notification to print out
3205 * @return string the HTML to output.
3207 protected function render_notification(\core\output\notification $notification) {
3208 return $this->render_from_template($notification->get_template_name(), $notification->export_for_template($this));
3212 * Returns HTML to display a continue button that goes to a particular URL.
3214 * @param string|moodle_url $url The url the button goes to.
3215 * @return string the HTML to output.
3217 public function continue_button($url) {
3218 if (!($url instanceof moodle_url)) {
3219 $url = new moodle_url($url);
3221 $button = new single_button($url, get_string('continue'), 'get', single_button::BUTTON_PRIMARY);
3222 $button->class = 'continuebutton';
3224 return $this->render($button);
3228 * Returns HTML to display a single paging bar to provide access to other pages (usually in a search)
3230 * Theme developers: DO NOT OVERRIDE! Please override function
3231 * {@link core_renderer::render_paging_bar()} instead.
3233 * @param int $totalcount The total number of entries available to be paged through
3234 * @param int $page The page you are currently viewing
3235 * @param int $perpage The number of entries that should be shown per page
3236 * @param string|moodle_url $baseurl url of the current page, the $pagevar parameter is added
3237 * @param string $pagevar name of page parameter that holds the page number
3238 * @return string the HTML to output.
3240 public function paging_bar($totalcount, $page, $perpage, $baseurl, $pagevar = 'page') {
3241 $pb = new paging_bar($totalcount, $page, $perpage, $baseurl, $pagevar);
3242 return $this->render($pb);
3246 * Returns HTML to display the paging bar.
3248 * @param paging_bar $pagingbar
3249 * @return string the HTML to output.
3251 protected function render_paging_bar(paging_bar $pagingbar) {
3252 // Any more than 10 is not usable and causes weird wrapping of the pagination.
3253 $pagingbar->maxdisplay = 10;
3254 return $this->render_from_template('core/paging_bar', $pagingbar->export_for_template($this));
3258 * Returns HTML to display initials bar to provide access to other pages (usually in a search)
3260 * @param string $current the currently selected letter.
3261 * @param string $class class name to add to this initial bar.
3262 * @param string $title the name to put in front of this initial bar.
3263 * @param string $urlvar URL parameter name for this initial.
3264 * @param string $url URL object.
3265 * @param array $alpha of letters in the alphabet.
3266 * @param bool $minirender Return a trimmed down view of the initials bar.
3267 * @return string the HTML to output.
3269 public function initials_bar($current, $class, $title, $urlvar, $url, $alpha = null, bool $minirender = false) {
3270 $ib = new initials_bar($current, $class, $title, $urlvar, $url, $alpha, $minirender);
3271 return $this->render($ib);
3275 * Internal implementation of initials bar rendering.
3277 * @param initials_bar $initialsbar
3278 * @return string
3280 protected function render_initials_bar(initials_bar $initialsbar) {
3281 return $this->render_from_template('core/initials_bar', $initialsbar->export_for_template($this));
3285 * Output the place a skip link goes to.
3287 * @param string $id The target name from the corresponding $PAGE->requires->skip_link_to($target) call.
3288 * @return string the HTML to output.
3290 public function skip_link_target($id = null) {
3291 return html_writer::span('', '', array('id' => $id));
3295 * Outputs a heading
3297 * @param string $text The text of the heading
3298 * @param int $level The level of importance of the heading. Defaulting to 2
3299 * @param string $classes A space-separated list of CSS classes. Defaulting to null
3300 * @param string $id An optional ID
3301 * @return string the HTML to output.
3303 public function heading($text, $level = 2, $classes = null, $id = null) {
3304 $level = (integer) $level;
3305 if ($level < 1 or $level > 6) {
3306 throw new coding_exception('Heading level must be an integer between 1 and 6.');
3308 return html_writer::tag('h' . $level, $text, array('id' => $id, 'class' => renderer_base::prepare_classes($classes)));
3312 * Outputs a box.
3314 * @param string $contents The contents of the box
3315 * @param string $classes A space-separated list of CSS classes
3316 * @param string $id An optional ID
3317 * @param array $attributes An array of other attributes to give the box.
3318 * @return string the HTML to output.
3320 public function box($contents, $classes = 'generalbox', $id = null, $attributes = array()) {
3321 return $this->box_start($classes, $id, $attributes) . $contents . $this->box_end();
3325 * Outputs the opening section of a box.
3327 * @param string $classes A space-separated list of CSS classes
3328 * @param string $id An optional ID
3329 * @param array $attributes An array of other attributes to give the box.
3330 * @return string the HTML to output.
3332 public function box_start($classes = 'generalbox', $id = null, $attributes = array()) {
3333 $this->opencontainers->push('box', html_writer::end_tag('div'));
3334 $attributes['id'] = $id;
3335 $attributes['class'] = 'box py-3 ' . renderer_base::prepare_classes($classes);
3336 return html_writer::start_tag('div', $attributes);
3340 * Outputs the closing section of a box.
3342 * @return string the HTML to output.
3344 public function box_end() {
3345 return $this->opencontainers->pop('box');
3349 * Outputs a paragraph.
3351 * @param string $contents The contents of the paragraph
3352 * @param string|null $classes A space-separated list of CSS classes
3353 * @param string|null $id An optional ID
3354 * @return string the HTML to output.
3356 public function paragraph(string $contents, ?string $classes = null, ?string $id = null): string {
3357 return html_writer::tag(
3358 'p',
3359 $contents,
3360 ['id' => $id, 'class' => renderer_base::prepare_classes($classes)]
3365 * Outputs a screen reader only inline text.
3367 * @param string $contents The contents of the paragraph
3368 * @return string the HTML to output.
3370 public function sr_text(string $contents): string {
3371 return html_writer::tag(
3372 'span',
3373 $contents,
3374 ['class' => 'sr-only']
3375 ) . ' ';
3379 * Outputs a container.
3381 * @param string $contents The contents of the box
3382 * @param string $classes A space-separated list of CSS classes
3383 * @param string $id An optional ID
3384 * @param array $attributes Optional other attributes as array
3385 * @return string the HTML to output.
3387 public function container($contents, $classes = null, $id = null, $attributes = []) {
3388 return $this->container_start($classes, $id, $attributes) . $contents . $this->container_end();
3392 * Outputs the opening section of a container.
3394 * @param string $classes A space-separated list of CSS classes
3395 * @param string $id An optional ID
3396 * @param array $attributes Optional other attributes as array
3397 * @return string the HTML to output.
3399 public function container_start($classes = null, $id = null, $attributes = []) {
3400 $this->opencontainers->push('container', html_writer::end_tag('div'));
3401 $attributes = array_merge(['id' => $id, 'class' => renderer_base::prepare_classes($classes)], $attributes);
3402 return html_writer::start_tag('div', $attributes);
3406 * Outputs the closing section of a container.
3408 * @return string the HTML to output.
3410 public function container_end() {
3411 return $this->opencontainers->pop('container');
3415 * Make nested HTML lists out of the items
3417 * The resulting list will look something like this:
3419 * <pre>
3420 * <<ul>>
3421 * <<li>><div class='tree_item parent'>(item contents)</div>
3422 * <<ul>
3423 * <<li>><div class='tree_item'>(item contents)</div><</li>>
3424 * <</ul>>
3425 * <</li>>
3426 * <</ul>>
3427 * </pre>
3429 * @param array $items
3430 * @param array $attrs html attributes passed to the top ofs the list
3431 * @return string HTML
3433 public function tree_block_contents($items, $attrs = array()) {
3434 // exit if empty, we don't want an empty ul element
3435 if (empty($items)) {
3436 return '';
3438 // array of nested li elements
3439 $lis = array();
3440 foreach ($items as $item) {
3441 // this applies to the li item which contains all child lists too
3442 $content = $item->content($this);
3443 $liclasses = array($item->get_css_type());
3444 if (!$item->forceopen || (!$item->forceopen && $item->collapse) || ($item->children->count()==0 && $item->nodetype==navigation_node::NODETYPE_BRANCH)) {
3445 $liclasses[] = 'collapsed';
3447 if ($item->isactive === true) {
3448 $liclasses[] = 'current_branch';
3450 $liattr = array('class'=>join(' ',$liclasses));
3451 // class attribute on the div item which only contains the item content
3452 $divclasses = array('tree_item');
3453 if ($item->children->count()>0 || $item->nodetype==navigation_node::NODETYPE_BRANCH) {
3454 $divclasses[] = 'branch';
3455 } else {
3456 $divclasses[] = 'leaf';
3458 if (!empty($item->classes) && count($item->classes)>0) {
3459 $divclasses[] = join(' ', $item->classes);
3461 $divattr = array('class'=>join(' ', $divclasses));
3462 if (!empty($item->id)) {
3463 $divattr['id'] = $item->id;
3465 $content = html_writer::tag('p', $content, $divattr) . $this->tree_block_contents($item->children);
3466 if (!empty($item->preceedwithhr) && $item->preceedwithhr===true) {
3467 $content = html_writer::empty_tag('hr') . $content;
3469 $content = html_writer::tag('li', $content, $liattr);
3470 $lis[] = $content;
3472 return html_writer::tag('ul', implode("\n", $lis), $attrs);
3476 * Returns a search box.
3478 * @param string $id The search box wrapper div id, defaults to an autogenerated one.
3479 * @return string HTML with the search form hidden by default.
3481 public function search_box($id = false) {
3482 global $CFG;
3484 // Accessing $CFG directly as using \core_search::is_global_search_enabled would
3485 // result in an extra included file for each site, even the ones where global search
3486 // is disabled.
3487 if (empty($CFG->enableglobalsearch) || !has_capability('moodle/search:query', context_system::instance())) {
3488 return '';
3491 $data = [
3492 'action' => new moodle_url('/search/index.php'),
3493 'hiddenfields' => (object) ['name' => 'context', 'value' => $this->page->context->id],
3494 'inputname' => 'q',
3495 'searchstring' => get_string('search'),
3497 return $this->render_from_template('core/search_input_navbar', $data);
3501 * Allow plugins to provide some content to be rendered in the navbar.
3502 * The plugin must define a PLUGIN_render_navbar_output function that returns
3503 * the HTML they wish to add to the navbar.
3505 * @return string HTML for the navbar
3507 public function navbar_plugin_output() {
3508 $output = '';
3510 // Give subsystems an opportunity to inject extra html content. The callback
3511 // must always return a string containing valid html.
3512 foreach (\core_component::get_core_subsystems() as $name => $path) {
3513 if ($path) {
3514 $output .= component_callback($name, 'render_navbar_output', [$this], '');
3518 if ($pluginsfunction = get_plugins_with_function('render_navbar_output')) {
3519 foreach ($pluginsfunction as $plugintype => $plugins) {
3520 foreach ($plugins as $pluginfunction) {
3521 $output .= $pluginfunction($this);
3526 return $output;
3530 * Construct a user menu, returning HTML that can be echoed out by a
3531 * layout file.
3533 * @param stdClass $user A user object, usually $USER.
3534 * @param bool $withlinks true if a dropdown should be built.
3535 * @return string HTML fragment.
3537 public function user_menu($user = null, $withlinks = null) {
3538 global $USER, $CFG;
3539 require_once($CFG->dirroot . '/user/lib.php');
3541 if (is_null($user)) {
3542 $user = $USER;
3545 // Note: this behaviour is intended to match that of core_renderer::login_info,
3546 // but should not be considered to be good practice; layout options are
3547 // intended to be theme-specific. Please don't copy this snippet anywhere else.
3548 if (is_null($withlinks)) {
3549 $withlinks = empty($this->page->layout_options['nologinlinks']);
3552 // Add a class for when $withlinks is false.
3553 $usermenuclasses = 'usermenu';
3554 if (!$withlinks) {
3555 $usermenuclasses .= ' withoutlinks';
3558 $returnstr = "";
3560 // If during initial install, return the empty return string.
3561 if (during_initial_install()) {
3562 return $returnstr;
3565 $loginpage = $this->is_login_page();
3566 $loginurl = get_login_url();
3568 // Get some navigation opts.
3569 $opts = user_get_user_navigation_info($user, $this->page);
3571 if (!empty($opts->unauthenticateduser)) {
3572 $returnstr = get_string($opts->unauthenticateduser['content'], 'moodle');
3573 // If not logged in, show the typical not-logged-in string.
3574 if (!$loginpage && (!$opts->unauthenticateduser['guest'] || $withlinks)) {
3575 $returnstr .= " (<a href=\"$loginurl\">" . get_string('login') . '</a>)';
3578 return html_writer::div(
3579 html_writer::span(
3580 $returnstr,
3581 'login nav-link'
3583 $usermenuclasses
3587 $avatarclasses = "avatars";
3588 $avatarcontents = html_writer::span($opts->metadata['useravatar'], 'avatar current');
3589 $usertextcontents = $opts->metadata['userfullname'];
3591 // Other user.
3592 if (!empty($opts->metadata['asotheruser'])) {
3593 $avatarcontents .= html_writer::span(
3594 $opts->metadata['realuseravatar'],
3595 'avatar realuser'
3597 $usertextcontents = $opts->metadata['realuserfullname'];
3598 $usertextcontents .= html_writer::tag(
3599 'span',
3600 get_string(
3601 'loggedinas',
3602 'moodle',
3603 html_writer::span(
3604 $opts->metadata['userfullname'],
3605 'value'
3608 array('class' => 'meta viewingas')
3612 // Role.
3613 if (!empty($opts->metadata['asotherrole'])) {
3614 $role = core_text::strtolower(preg_replace('#[ ]+#', '-', trim($opts->metadata['rolename'])));
3615 $usertextcontents .= html_writer::span(
3616 $opts->metadata['rolename'],
3617 'meta role role-' . $role
3621 // User login failures.
3622 if (!empty($opts->metadata['userloginfail'])) {
3623 $usertextcontents .= html_writer::span(
3624 $opts->metadata['userloginfail'],
3625 'meta loginfailures'
3629 // MNet.
3630 if (!empty($opts->metadata['asmnetuser'])) {
3631 $mnet = strtolower(preg_replace('#[ ]+#', '-', trim($opts->metadata['mnetidprovidername'])));
3632 $usertextcontents .= html_writer::span(
3633 $opts->metadata['mnetidprovidername'],
3634 'meta mnet mnet-' . $mnet
3638 $returnstr .= html_writer::span(
3639 html_writer::span($usertextcontents, 'usertext mr-1') .
3640 html_writer::span($avatarcontents, $avatarclasses),
3641 'userbutton'
3644 // Create a divider (well, a filler).
3645 $divider = new action_menu_filler();
3646 $divider->primary = false;
3648 $am = new action_menu();
3649 $am->set_menu_trigger(
3650 $returnstr,
3651 'nav-link'
3653 $am->set_action_label(get_string('usermenu'));
3654 $am->set_nowrap_on_items();
3655 if ($withlinks) {
3656 $navitemcount = count($opts->navitems);
3657 $idx = 0;
3658 foreach ($opts->navitems as $key => $value) {
3660 switch ($value->itemtype) {
3661 case 'divider':
3662 // If the nav item is a divider, add one and skip link processing.
3663 $am->add($divider);
3664 break;
3666 case 'invalid':
3667 // Silently skip invalid entries (should we post a notification?).
3668 break;
3670 case 'link':
3671 // Process this as a link item.
3672 $pix = null;
3673 if (isset($value->pix) && !empty($value->pix)) {
3674 $pix = new pix_icon($value->pix, '', null, array('class' => 'iconsmall'));
3675 } else if (isset($value->imgsrc) && !empty($value->imgsrc)) {
3676 $value->title = html_writer::img(
3677 $value->imgsrc,
3678 $value->title,
3679 array('class' => 'iconsmall')
3680 ) . $value->title;
3683 $al = new action_menu_link_secondary(
3684 $value->url,
3685 $pix,
3686 $value->title,
3687 array('class' => 'icon')
3689 if (!empty($value->titleidentifier)) {
3690 $al->attributes['data-title'] = $value->titleidentifier;
3692 $am->add($al);
3693 break;
3696 $idx++;
3698 // Add dividers after the first item and before the last item.
3699 if ($idx == 1 || $idx == $navitemcount - 1) {
3700 $am->add($divider);
3705 return html_writer::div(
3706 $this->render($am),
3707 $usermenuclasses
3712 * Secure layout login info.
3714 * @return string
3716 public function secure_layout_login_info() {
3717 if (get_config('core', 'logininfoinsecurelayout')) {
3718 return $this->login_info(false);
3719 } else {
3720 return '';
3725 * Returns the language menu in the secure layout.
3727 * No custom menu items are passed though, such that it will render only the language selection.
3729 * @return string
3731 public function secure_layout_language_menu() {
3732 if (get_config('core', 'langmenuinsecurelayout')) {
3733 $custommenu = new custom_menu('', current_language());
3734 return $this->render_custom_menu($custommenu);
3735 } else {
3736 return '';
3741 * This renders the navbar.
3742 * Uses bootstrap compatible html.
3744 public function navbar() {
3745 return $this->render_from_template('core/navbar', $this->page->navbar);
3749 * Renders a breadcrumb navigation node object.
3751 * @param breadcrumb_navigation_node $item The navigation node to render.
3752 * @return string HTML fragment
3754 protected function render_breadcrumb_navigation_node(breadcrumb_navigation_node $item) {
3756 if ($item->action instanceof moodle_url) {
3757 $content = $item->get_content();
3758 $title = $item->get_title();
3759 $attributes = array();
3760 $attributes['itemprop'] = 'url';
3761 if ($title !== '') {
3762 $attributes['title'] = $title;
3764 if ($item->hidden) {
3765 $attributes['class'] = 'dimmed_text';
3767 if ($item->is_last()) {
3768 $attributes['aria-current'] = 'page';
3770 $content = html_writer::tag('span', $content, array('itemprop' => 'title'));
3771 $content = html_writer::link($item->action, $content, $attributes);
3773 $attributes = array();
3774 $attributes['itemscope'] = '';
3775 $attributes['itemtype'] = 'http://data-vocabulary.org/Breadcrumb';
3776 $content = html_writer::tag('span', $content, $attributes);
3778 } else {
3779 $content = $this->render_navigation_node($item);
3781 return $content;
3785 * Renders a navigation node object.
3787 * @param navigation_node $item The navigation node to render.
3788 * @return string HTML fragment
3790 protected function render_navigation_node(navigation_node $item) {
3791 $content = $item->get_content();
3792 $title = $item->get_title();
3793 if ($item->icon instanceof renderable && !$item->hideicon) {
3794 $icon = $this->render($item->icon);
3795 $content = $icon.$content; // use CSS for spacing of icons
3797 if ($item->helpbutton !== null) {
3798 $content = trim($item->helpbutton).html_writer::tag('span', $content, array('class'=>'clearhelpbutton', 'tabindex'=>'0'));
3800 if ($content === '') {
3801 return '';
3803 if ($item->action instanceof action_link) {
3804 $link = $item->action;
3805 if ($item->hidden) {
3806 $link->add_class('dimmed');
3808 if (!empty($content)) {
3809 // Providing there is content we will use that for the link content.
3810 $link->text = $content;
3812 $content = $this->render($link);
3813 } else if ($item->action instanceof moodle_url) {
3814 $attributes = array();
3815 if ($title !== '') {
3816 $attributes['title'] = $title;
3818 if ($item->hidden) {
3819 $attributes['class'] = 'dimmed_text';
3821 $content = html_writer::link($item->action, $content, $attributes);
3823 } else if (is_string($item->action) || empty($item->action)) {
3824 $attributes = array('tabindex'=>'0'); //add tab support to span but still maintain character stream sequence.
3825 if ($title !== '') {
3826 $attributes['title'] = $title;
3828 if ($item->hidden) {
3829 $attributes['class'] = 'dimmed_text';
3831 $content = html_writer::tag('span', $content, $attributes);
3833 return $content;
3837 * Accessibility: Right arrow-like character is
3838 * used in the breadcrumb trail, course navigation menu
3839 * (previous/next activity), calendar, and search forum block.
3840 * If the theme does not set characters, appropriate defaults
3841 * are set automatically. Please DO NOT
3842 * use &lt; &gt; &raquo; - these are confusing for blind users.
3844 * @return string
3846 public function rarrow() {
3847 return $this->page->theme->rarrow;
3851 * Accessibility: Left arrow-like character is
3852 * used in the breadcrumb trail, course navigation menu
3853 * (previous/next activity), calendar, and search forum block.
3854 * If the theme does not set characters, appropriate defaults
3855 * are set automatically. Please DO NOT
3856 * use &lt; &gt; &raquo; - these are confusing for blind users.
3858 * @return string
3860 public function larrow() {
3861 return $this->page->theme->larrow;
3865 * Accessibility: Up arrow-like character is used in
3866 * the book heirarchical navigation.
3867 * If the theme does not set characters, appropriate defaults
3868 * are set automatically. Please DO NOT
3869 * use ^ - this is confusing for blind users.
3871 * @return string
3873 public function uarrow() {
3874 return $this->page->theme->uarrow;
3878 * Accessibility: Down arrow-like character.
3879 * If the theme does not set characters, appropriate defaults
3880 * are set automatically.
3882 * @return string
3884 public function darrow() {
3885 return $this->page->theme->darrow;
3889 * Returns the custom menu if one has been set
3891 * A custom menu can be configured by browsing to a theme's settings page
3892 * and then configuring the custommenu config setting as described.
3894 * Theme developers: DO NOT OVERRIDE! Please override function
3895 * {@link core_renderer::render_custom_menu()} instead.
3897 * @param string $custommenuitems - custom menuitems set by theme instead of global theme settings
3898 * @return string
3900 public function custom_menu($custommenuitems = '') {
3901 global $CFG;
3903 if (empty($custommenuitems) && !empty($CFG->custommenuitems)) {
3904 $custommenuitems = $CFG->custommenuitems;
3906 $custommenu = new custom_menu($custommenuitems, current_language());
3907 return $this->render_custom_menu($custommenu);
3911 * We want to show the custom menus as a list of links in the footer on small screens.
3912 * Just return the menu object exported so we can render it differently.
3914 public function custom_menu_flat() {
3915 global $CFG;
3916 $custommenuitems = '';
3918 if (empty($custommenuitems) && !empty($CFG->custommenuitems)) {
3919 $custommenuitems = $CFG->custommenuitems;
3921 $custommenu = new custom_menu($custommenuitems, current_language());
3922 $langs = get_string_manager()->get_list_of_translations();
3923 $haslangmenu = $this->lang_menu() != '';
3925 if ($haslangmenu) {
3926 $strlang = get_string('language');
3927 $currentlang = current_language();
3928 if (isset($langs[$currentlang])) {
3929 $currentlang = $langs[$currentlang];
3930 } else {
3931 $currentlang = $strlang;
3933 $this->language = $custommenu->add($currentlang, new moodle_url('#'), $strlang, 10000);
3934 foreach ($langs as $langtype => $langname) {
3935 $this->language->add($langname, new moodle_url($this->page->url, array('lang' => $langtype)), $langname);
3939 return $custommenu->export_for_template($this);
3943 * Renders a custom menu object (located in outputcomponents.php)
3945 * The custom menu this method produces makes use of the YUI3 menunav widget
3946 * and requires very specific html elements and classes.
3948 * @staticvar int $menucount
3949 * @param custom_menu $menu
3950 * @return string
3952 protected function render_custom_menu(custom_menu $menu) {
3953 global $CFG;
3955 $langs = get_string_manager()->get_list_of_translations();
3956 $haslangmenu = $this->lang_menu() != '';
3958 if (!$menu->has_children() && !$haslangmenu) {
3959 return '';
3962 if ($haslangmenu) {
3963 $strlang = get_string('language');
3964 $currentlang = current_language();
3965 if (isset($langs[$currentlang])) {
3966 $currentlangstr = $langs[$currentlang];
3967 } else {
3968 $currentlangstr = $strlang;
3970 $this->language = $menu->add($currentlangstr, new moodle_url('#'), $strlang, 10000);
3971 foreach ($langs as $langtype => $langname) {
3972 $attributes = [];
3973 // Set the lang attribute for languages different from the page's current language.
3974 if ($langtype !== $currentlang) {
3975 $attributes[] = [
3976 'key' => 'lang',
3977 'value' => get_html_lang_attribute_value($langtype),
3980 $this->language->add($langname, new moodle_url($this->page->url, ['lang' => $langtype]), null, null, $attributes);
3984 $content = '';
3985 foreach ($menu->get_children() as $item) {
3986 $context = $item->export_for_template($this);
3987 $content .= $this->render_from_template('core/custom_menu_item', $context);
3990 return $content;
3994 * Renders a custom menu node as part of a submenu
3996 * The custom menu this method produces makes use of the YUI3 menunav widget
3997 * and requires very specific html elements and classes.
3999 * @see core:renderer::render_custom_menu()
4001 * @staticvar int $submenucount
4002 * @param custom_menu_item $menunode
4003 * @return string
4005 protected function render_custom_menu_item(custom_menu_item $menunode) {
4006 // Required to ensure we get unique trackable id's
4007 static $submenucount = 0;
4008 if ($menunode->has_children()) {
4009 // If the child has menus render it as a sub menu
4010 $submenucount++;
4011 $content = html_writer::start_tag('li');
4012 if ($menunode->get_url() !== null) {
4013 $url = $menunode->get_url();
4014 } else {
4015 $url = '#cm_submenu_'.$submenucount;
4017 $content .= html_writer::link($url, $menunode->get_text(), array('class'=>'yui3-menu-label', 'title'=>$menunode->get_title()));
4018 $content .= html_writer::start_tag('div', array('id'=>'cm_submenu_'.$submenucount, 'class'=>'yui3-menu custom_menu_submenu'));
4019 $content .= html_writer::start_tag('div', array('class'=>'yui3-menu-content'));
4020 $content .= html_writer::start_tag('ul');
4021 foreach ($menunode->get_children() as $menunode) {
4022 $content .= $this->render_custom_menu_item($menunode);
4024 $content .= html_writer::end_tag('ul');
4025 $content .= html_writer::end_tag('div');
4026 $content .= html_writer::end_tag('div');
4027 $content .= html_writer::end_tag('li');
4028 } else {
4029 // The node doesn't have children so produce a final menuitem.
4030 // Also, if the node's text matches '####', add a class so we can treat it as a divider.
4031 $content = '';
4032 if (preg_match("/^#+$/", $menunode->get_text())) {
4034 // This is a divider.
4035 $content = html_writer::start_tag('li', array('class' => 'yui3-menuitem divider'));
4036 } else {
4037 $content = html_writer::start_tag(
4038 'li',
4039 array(
4040 'class' => 'yui3-menuitem'
4043 if ($menunode->get_url() !== null) {
4044 $url = $menunode->get_url();
4045 } else {
4046 $url = '#';
4048 $content .= html_writer::link(
4049 $url,
4050 $menunode->get_text(),
4051 array('class' => 'yui3-menuitem-content', 'title' => $menunode->get_title())
4054 $content .= html_writer::end_tag('li');
4056 // Return the sub menu
4057 return $content;
4061 * Renders theme links for switching between default and other themes.
4063 * @return string
4065 protected function theme_switch_links() {
4067 $actualdevice = core_useragent::get_device_type();
4068 $currentdevice = $this->page->devicetypeinuse;
4069 $switched = ($actualdevice != $currentdevice);
4071 if (!$switched && $currentdevice == 'default' && $actualdevice == 'default') {
4072 // The user is using the a default device and hasn't switched so don't shown the switch
4073 // device links.
4074 return '';
4077 if ($switched) {
4078 $linktext = get_string('switchdevicerecommended');
4079 $devicetype = $actualdevice;
4080 } else {
4081 $linktext = get_string('switchdevicedefault');
4082 $devicetype = 'default';
4084 $linkurl = new moodle_url('/theme/switchdevice.php', array('url' => $this->page->url, 'device' => $devicetype, 'sesskey' => sesskey()));
4086 $content = html_writer::start_tag('div', array('id' => 'theme_switch_link'));
4087 $content .= html_writer::link($linkurl, $linktext, array('rel' => 'nofollow'));
4088 $content .= html_writer::end_tag('div');
4090 return $content;
4094 * Renders tabs
4096 * This function replaces print_tabs() used before Moodle 2.5 but with slightly different arguments
4098 * Theme developers: In order to change how tabs are displayed please override functions
4099 * {@link core_renderer::render_tabtree()} and/or {@link core_renderer::render_tabobject()}
4101 * @param array $tabs array of tabs, each of them may have it's own ->subtree
4102 * @param string|null $selected which tab to mark as selected, all parent tabs will
4103 * automatically be marked as activated
4104 * @param array|string|null $inactive list of ids of inactive tabs, regardless of
4105 * their level. Note that you can as weel specify tabobject::$inactive for separate instances
4106 * @return string
4108 final public function tabtree($tabs, $selected = null, $inactive = null) {
4109 return $this->render(new tabtree($tabs, $selected, $inactive));
4113 * Renders tabtree
4115 * @param tabtree $tabtree
4116 * @return string
4118 protected function render_tabtree(tabtree $tabtree) {
4119 if (empty($tabtree->subtree)) {
4120 return '';
4122 $data = $tabtree->export_for_template($this);
4123 return $this->render_from_template('core/tabtree', $data);
4127 * Renders tabobject (part of tabtree)
4129 * This function is called from {@link core_renderer::render_tabtree()}
4130 * and also it calls itself when printing the $tabobject subtree recursively.
4132 * Property $tabobject->level indicates the number of row of tabs.
4134 * @param tabobject $tabobject
4135 * @return string HTML fragment
4137 protected function render_tabobject(tabobject $tabobject) {
4138 $str = '';
4140 // Print name of the current tab.
4141 if ($tabobject instanceof tabtree) {
4142 // No name for tabtree root.
4143 } else if ($tabobject->inactive || $tabobject->activated || ($tabobject->selected && !$tabobject->linkedwhenselected)) {
4144 // Tab name without a link. The <a> tag is used for styling.
4145 $str .= html_writer::tag('a', html_writer::span($tabobject->text), array('class' => 'nolink moodle-has-zindex'));
4146 } else {
4147 // Tab name with a link.
4148 if (!($tabobject->link instanceof moodle_url)) {
4149 // backward compartibility when link was passed as quoted string
4150 $str .= "<a href=\"$tabobject->link\" title=\"$tabobject->title\"><span>$tabobject->text</span></a>";
4151 } else {
4152 $str .= html_writer::link($tabobject->link, html_writer::span($tabobject->text), array('title' => $tabobject->title));
4156 if (empty($tabobject->subtree)) {
4157 if ($tabobject->selected) {
4158 $str .= html_writer::tag('div', '&nbsp;', array('class' => 'tabrow'. ($tabobject->level + 1). ' empty'));
4160 return $str;
4163 // Print subtree.
4164 if ($tabobject->level == 0 || $tabobject->selected || $tabobject->activated) {
4165 $str .= html_writer::start_tag('ul', array('class' => 'tabrow'. $tabobject->level));
4166 $cnt = 0;
4167 foreach ($tabobject->subtree as $tab) {
4168 $liclass = '';
4169 if (!$cnt) {
4170 $liclass .= ' first';
4172 if ($cnt == count($tabobject->subtree) - 1) {
4173 $liclass .= ' last';
4175 if ((empty($tab->subtree)) && (!empty($tab->selected))) {
4176 $liclass .= ' onerow';
4179 if ($tab->selected) {
4180 $liclass .= ' here selected';
4181 } else if ($tab->activated) {
4182 $liclass .= ' here active';
4185 // This will recursively call function render_tabobject() for each item in subtree.
4186 $str .= html_writer::tag('li', $this->render($tab), array('class' => trim($liclass)));
4187 $cnt++;
4189 $str .= html_writer::end_tag('ul');
4192 return $str;
4196 * Get the HTML for blocks in the given region.
4198 * @since Moodle 2.5.1 2.6
4199 * @param string $region The region to get HTML for.
4200 * @param array $classes Wrapping tag classes.
4201 * @param string $tag Wrapping tag.
4202 * @param boolean $fakeblocksonly Include fake blocks only.
4203 * @return string HTML.
4205 public function blocks($region, $classes = array(), $tag = 'aside', $fakeblocksonly = false) {
4206 $displayregion = $this->page->apply_theme_region_manipulations($region);
4207 $classes = (array)$classes;
4208 $classes[] = 'block-region';
4209 $attributes = array(
4210 'id' => 'block-region-'.preg_replace('#[^a-zA-Z0-9_\-]+#', '-', $displayregion),
4211 'class' => join(' ', $classes),
4212 'data-blockregion' => $displayregion,
4213 'data-droptarget' => '1'
4215 if ($this->page->blocks->region_has_content($displayregion, $this)) {
4216 $content = html_writer::tag('h2', get_string('blocks'), ['class' => 'sr-only']) .
4217 $this->blocks_for_region($displayregion, $fakeblocksonly);
4218 } else {
4219 $content = html_writer::tag('h2', get_string('blocks'), ['class' => 'sr-only']);
4221 return html_writer::tag($tag, $content, $attributes);
4225 * Renders a custom block region.
4227 * Use this method if you want to add an additional block region to the content of the page.
4228 * Please note this should only be used in special situations.
4229 * We want to leave the theme is control where ever possible!
4231 * This method must use the same method that the theme uses within its layout file.
4232 * As such it asks the theme what method it is using.
4233 * It can be one of two values, blocks or blocks_for_region (deprecated).
4235 * @param string $regionname The name of the custom region to add.
4236 * @return string HTML for the block region.
4238 public function custom_block_region($regionname) {
4239 if ($this->page->theme->get_block_render_method() === 'blocks') {
4240 return $this->blocks($regionname);
4241 } else {
4242 return $this->blocks_for_region($regionname);
4247 * Returns the CSS classes to apply to the body tag.
4249 * @since Moodle 2.5.1 2.6
4250 * @param array $additionalclasses Any additional classes to apply.
4251 * @return string
4253 public function body_css_classes(array $additionalclasses = array()) {
4254 return $this->page->bodyclasses . ' ' . implode(' ', $additionalclasses);
4258 * The ID attribute to apply to the body tag.
4260 * @since Moodle 2.5.1 2.6
4261 * @return string
4263 public function body_id() {
4264 return $this->page->bodyid;
4268 * Returns HTML attributes to use within the body tag. This includes an ID and classes.
4270 * @since Moodle 2.5.1 2.6
4271 * @param string|array $additionalclasses Any additional classes to give the body tag,
4272 * @return string
4274 public function body_attributes($additionalclasses = array()) {
4275 if (!is_array($additionalclasses)) {
4276 $additionalclasses = explode(' ', $additionalclasses);
4278 return ' id="'. $this->body_id().'" class="'.$this->body_css_classes($additionalclasses).'"';
4282 * Gets HTML for the page heading.
4284 * @since Moodle 2.5.1 2.6
4285 * @param string $tag The tag to encase the heading in. h1 by default.
4286 * @return string HTML.
4288 public function page_heading($tag = 'h1') {
4289 return html_writer::tag($tag, $this->page->heading);
4293 * Gets the HTML for the page heading button.
4295 * @since Moodle 2.5.1 2.6
4296 * @return string HTML.
4298 public function page_heading_button() {
4299 return $this->page->button;
4303 * Returns the Moodle docs link to use for this page.
4305 * @since Moodle 2.5.1 2.6
4306 * @param string $text
4307 * @return string
4309 public function page_doc_link($text = null) {
4310 if ($text === null) {
4311 $text = get_string('moodledocslink');
4313 $path = page_get_doc_link_path($this->page);
4314 if (!$path) {
4315 return '';
4317 return $this->doc_link($path, $text);
4321 * Returns the HTML for the site support email link
4323 * @param array $customattribs Array of custom attributes for the support email anchor tag.
4324 * @param bool $embed Set to true if you want to embed the link in other inline content.
4325 * @return string The html code for the support email link.
4327 public function supportemail(array $customattribs = [], bool $embed = false): string {
4328 global $CFG;
4330 // Do not provide a link to contact site support if it is unavailable to this user. This would be where the site has
4331 // disabled support, or limited it to authenticated users and the current user is a guest or not logged in.
4332 if (!isset($CFG->supportavailability) ||
4333 $CFG->supportavailability == CONTACT_SUPPORT_DISABLED ||
4334 ($CFG->supportavailability == CONTACT_SUPPORT_AUTHENTICATED && (!isloggedin() || isguestuser()))) {
4335 return '';
4338 $label = get_string('contactsitesupport', 'admin');
4339 $icon = $this->pix_icon('t/email', '');
4341 if (!$embed) {
4342 $content = $icon . $label;
4343 } else {
4344 $content = $label;
4347 if (!empty($CFG->supportpage)) {
4348 $attributes = ['href' => $CFG->supportpage, 'target' => 'blank'];
4349 $content .= $this->pix_icon('i/externallink', '', 'moodle', ['class' => 'ml-1']);
4350 } else {
4351 $attributes = ['href' => $CFG->wwwroot . '/user/contactsitesupport.php'];
4354 $attributes += $customattribs;
4356 return html_writer::tag('a', $content, $attributes);
4360 * Returns the services and support link for the help pop-up.
4362 * @return string
4364 public function services_support_link(): string {
4365 global $CFG;
4367 if (during_initial_install() ||
4368 (isset($CFG->showservicesandsupportcontent) && $CFG->showservicesandsupportcontent == false) ||
4369 !is_siteadmin()) {
4370 return '';
4373 $liferingicon = $this->pix_icon('t/life-ring', '', 'moodle', ['class' => 'fa fa-life-ring']);
4374 $newwindowicon = $this->pix_icon('i/externallink', get_string('opensinnewwindow'), 'moodle', ['class' => 'ml-1']);
4375 $link = !empty($CFG->servicespage)
4376 ? $CFG->servicespage
4377 : 'https://moodle.com/help/?utm_source=CTA-banner&utm_medium=platform&utm_campaign=name~Moodle4+cat~lms+mp~no';
4378 $content = $liferingicon . get_string('moodleservicesandsupport') . $newwindowicon;
4380 return html_writer::tag('a', $content, ['target' => '_blank', 'href' => $link]);
4384 * Helper function to decide whether to show the help popover header or not.
4386 * @return bool
4388 public function has_popover_links(): bool {
4389 return !empty($this->services_support_link()) || !empty($this->page_doc_link()) || !empty($this->supportemail());
4393 * Helper function to decide whether to show the communication link or not.
4395 * @return bool
4397 public function has_communication_links(): bool {
4398 if (during_initial_install() || !core_communication\api::is_available()) {
4399 return false;
4401 return !empty($this->communication_link());
4405 * Returns the communication link, complete with html.
4407 * @return string
4409 public function communication_link(): string {
4410 $link = $this->communication_url() ?? '';
4411 $commicon = $this->pix_icon('t/messages-o', '', 'moodle', ['class' => 'fa fa-comments']);
4412 $newwindowicon = $this->pix_icon('i/externallink', get_string('opensinnewwindow'), 'moodle', ['class' => 'ml-1']);
4413 $content = $commicon . get_string('communicationroomlink', 'course') . $newwindowicon;
4414 $html = html_writer::tag('a', $content, ['target' => '_blank', 'href' => $link]);
4416 return !empty($link) ? $html : '';
4420 * Returns the communication url for a given instance if it exists.
4422 * @return string
4424 public function communication_url(): string {
4425 global $COURSE;
4426 return \core_communication\helper::get_course_communication_url($COURSE);
4430 * Returns the page heading menu.
4432 * @since Moodle 2.5.1 2.6
4433 * @return string HTML.
4435 public function page_heading_menu() {
4436 return $this->page->headingmenu;
4440 * Returns the title to use on the page.
4442 * @since Moodle 2.5.1 2.6
4443 * @return string
4445 public function page_title() {
4446 return $this->page->title;
4450 * Returns the moodle_url for the favicon.
4452 * @since Moodle 2.5.1 2.6
4453 * @return moodle_url The moodle_url for the favicon
4455 public function favicon() {
4456 $logo = null;
4457 if (!during_initial_install()) {
4458 $logo = get_config('core_admin', 'favicon');
4460 if (empty($logo)) {
4461 return $this->image_url('favicon', 'theme');
4464 // Use $CFG->themerev to prevent browser caching when the file changes.
4465 return moodle_url::make_pluginfile_url(context_system::instance()->id, 'core_admin', 'favicon', '64x64/',
4466 theme_get_revision(), $logo);
4470 * Renders preferences groups.
4472 * @param preferences_groups $renderable The renderable
4473 * @return string The output.
4475 public function render_preferences_groups(preferences_groups $renderable) {
4476 return $this->render_from_template('core/preferences_groups', $renderable);
4480 * Renders preferences group.
4482 * @param preferences_group $renderable The renderable
4483 * @return string The output.
4485 public function render_preferences_group(preferences_group $renderable) {
4486 $html = '';
4487 $html .= html_writer::start_tag('div', array('class' => 'col-sm-4 preferences-group'));
4488 $html .= $this->heading($renderable->title, 3);
4489 $html .= html_writer::start_tag('ul');
4490 foreach ($renderable->nodes as $node) {
4491 if ($node->has_children()) {
4492 debugging('Preferences nodes do not support children', DEBUG_DEVELOPER);
4494 $html .= html_writer::tag('li', $this->render($node));
4496 $html .= html_writer::end_tag('ul');
4497 $html .= html_writer::end_tag('div');
4498 return $html;
4501 public function context_header($headerinfo = null, $headinglevel = 1) {
4502 global $DB, $USER, $CFG, $SITE;
4503 require_once($CFG->dirroot . '/user/lib.php');
4504 $context = $this->page->context;
4505 $heading = null;
4506 $imagedata = null;
4507 $subheader = null;
4508 $userbuttons = null;
4510 // Make sure to use the heading if it has been set.
4511 if (isset($headerinfo['heading'])) {
4512 $heading = $headerinfo['heading'];
4513 } else {
4514 $heading = $this->page->heading;
4517 // The user context currently has images and buttons. Other contexts may follow.
4518 if ((isset($headerinfo['user']) || $context->contextlevel == CONTEXT_USER) && $this->page->pagetype !== 'my-index') {
4519 if (isset($headerinfo['user'])) {
4520 $user = $headerinfo['user'];
4521 } else {
4522 // Look up the user information if it is not supplied.
4523 $user = $DB->get_record('user', array('id' => $context->instanceid));
4526 // If the user context is set, then use that for capability checks.
4527 if (isset($headerinfo['usercontext'])) {
4528 $context = $headerinfo['usercontext'];
4531 // Only provide user information if the user is the current user, or a user which the current user can view.
4532 // When checking user_can_view_profile(), either:
4533 // If the page context is course, check the course context (from the page object) or;
4534 // If page context is NOT course, then check across all courses.
4535 $course = ($this->page->context->contextlevel == CONTEXT_COURSE) ? $this->page->course : null;
4537 if (user_can_view_profile($user, $course)) {
4538 // Use the user's full name if the heading isn't set.
4539 if (empty($heading)) {
4540 $heading = fullname($user);
4543 $imagedata = $this->user_picture($user, array('size' => 100));
4545 // Check to see if we should be displaying a message button.
4546 if (!empty($CFG->messaging) && has_capability('moodle/site:sendmessage', $context)) {
4547 $userbuttons = array(
4548 'messages' => array(
4549 'buttontype' => 'message',
4550 'title' => get_string('message', 'message'),
4551 'url' => new moodle_url('/message/index.php', array('id' => $user->id)),
4552 'image' => 'message',
4553 'linkattributes' => \core_message\helper::messageuser_link_params($user->id),
4554 'page' => $this->page
4558 if ($USER->id != $user->id) {
4559 $iscontact = \core_message\api::is_contact($USER->id, $user->id);
4560 $contacttitle = $iscontact ? 'removefromyourcontacts' : 'addtoyourcontacts';
4561 $contacturlaction = $iscontact ? 'removecontact' : 'addcontact';
4562 $contactimage = $iscontact ? 'removecontact' : 'addcontact';
4563 $userbuttons['togglecontact'] = array(
4564 'buttontype' => 'togglecontact',
4565 'title' => get_string($contacttitle, 'message'),
4566 'url' => new moodle_url('/message/index.php', array(
4567 'user1' => $USER->id,
4568 'user2' => $user->id,
4569 $contacturlaction => $user->id,
4570 'sesskey' => sesskey())
4572 'image' => $contactimage,
4573 'linkattributes' => \core_message\helper::togglecontact_link_params($user, $iscontact),
4574 'page' => $this->page
4578 } else {
4579 $heading = null;
4584 $contextheader = new context_header($heading, $headinglevel, $imagedata, $userbuttons);
4585 return $this->render_context_header($contextheader);
4589 * Renders the skip links for the page.
4591 * @param array $links List of skip links.
4592 * @return string HTML for the skip links.
4594 public function render_skip_links($links) {
4595 $context = [ 'links' => []];
4597 foreach ($links as $url => $text) {
4598 $context['links'][] = [ 'url' => $url, 'text' => $text];
4601 return $this->render_from_template('core/skip_links', $context);
4605 * Renders the header bar.
4607 * @param context_header $contextheader Header bar object.
4608 * @return string HTML for the header bar.
4610 protected function render_context_header(context_header $contextheader) {
4612 // Generate the heading first and before everything else as we might have to do an early return.
4613 if (!isset($contextheader->heading)) {
4614 $heading = $this->heading($this->page->heading, $contextheader->headinglevel);
4615 } else {
4616 $heading = $this->heading($contextheader->heading, $contextheader->headinglevel);
4619 $showheader = empty($this->page->layout_options['nocontextheader']);
4620 if (!$showheader) {
4621 // Return the heading wrapped in an sr-only element so it is only visible to screen-readers.
4622 return html_writer::div($heading, 'sr-only');
4625 // All the html stuff goes here.
4626 $html = html_writer::start_div('page-context-header');
4628 // Image data.
4629 if (isset($contextheader->imagedata)) {
4630 // Header specific image.
4631 $html .= html_writer::div($contextheader->imagedata, 'page-header-image icon-size-7');
4634 // Headings.
4635 if (isset($contextheader->prefix)) {
4636 $prefix = html_writer::div($contextheader->prefix, 'text-muted');
4637 $heading = $prefix . $heading;
4639 $html .= html_writer::tag('div', $heading, array('class' => 'page-header-headings'));
4641 // Buttons.
4642 if (isset($contextheader->additionalbuttons)) {
4643 $html .= html_writer::start_div('btn-group header-button-group');
4644 foreach ($contextheader->additionalbuttons as $button) {
4645 if (!isset($button->page)) {
4646 // Include js for messaging.
4647 if ($button['buttontype'] === 'togglecontact') {
4648 \core_message\helper::togglecontact_requirejs();
4650 if ($button['buttontype'] === 'message') {
4651 \core_message\helper::messageuser_requirejs();
4653 $image = $this->pix_icon($button['formattedimage'], '', 'moodle', array(
4654 'class' => 'iconsmall',
4656 $image .= html_writer::span($button['title'], 'header-button-title');
4657 } else {
4658 $image = html_writer::empty_tag('img', array(
4659 'src' => $button['formattedimage'],
4660 'alt' => $button['title'],
4663 $html .= html_writer::link($button['url'], html_writer::tag('span', $image), $button['linkattributes']);
4665 $html .= html_writer::end_div();
4667 $html .= html_writer::end_div();
4669 return $html;
4673 * Wrapper for header elements.
4675 * @return string HTML to display the main header.
4677 public function full_header() {
4678 $pagetype = $this->page->pagetype;
4679 $homepage = get_home_page();
4680 $homepagetype = null;
4681 // Add a special case since /my/courses is a part of the /my subsystem.
4682 if ($homepage == HOMEPAGE_MY || $homepage == HOMEPAGE_MYCOURSES) {
4683 $homepagetype = 'my-index';
4684 } else if ($homepage == HOMEPAGE_SITE) {
4685 $homepagetype = 'site-index';
4687 if ($this->page->include_region_main_settings_in_header_actions() &&
4688 !$this->page->blocks->is_block_present('settings')) {
4689 // Only include the region main settings if the page has requested it and it doesn't already have
4690 // the settings block on it. The region main settings are included in the settings block and
4691 // duplicating the content causes behat failures.
4692 $this->page->add_header_action(html_writer::div(
4693 $this->region_main_settings_menu(),
4694 'd-print-none',
4695 ['id' => 'region-main-settings-menu']
4699 $header = new stdClass();
4700 $header->settingsmenu = $this->context_header_settings_menu();
4701 $header->contextheader = $this->context_header();
4702 $header->hasnavbar = empty($this->page->layout_options['nonavbar']);
4703 $header->navbar = $this->navbar();
4704 $header->pageheadingbutton = $this->page_heading_button();
4705 $header->courseheader = $this->course_header();
4706 $header->headeractions = $this->page->get_header_actions();
4707 if (!empty($pagetype) && !empty($homepagetype) && $pagetype == $homepagetype) {
4708 $header->welcomemessage = \core_user::welcome_message();
4710 return $this->render_from_template('core/full_header', $header);
4714 * This is an optional menu that can be added to a layout by a theme. It contains the
4715 * menu for the course administration, only on the course main page.
4717 * @return string
4719 public function context_header_settings_menu() {
4720 $context = $this->page->context;
4721 $menu = new action_menu();
4723 $items = $this->page->navbar->get_items();
4724 $currentnode = end($items);
4726 $showcoursemenu = false;
4727 $showfrontpagemenu = false;
4728 $showusermenu = false;
4730 // We are on the course home page.
4731 if (($context->contextlevel == CONTEXT_COURSE) &&
4732 !empty($currentnode) &&
4733 ($currentnode->type == navigation_node::TYPE_COURSE || $currentnode->type == navigation_node::TYPE_SECTION)) {
4734 $showcoursemenu = true;
4737 $courseformat = course_get_format($this->page->course);
4738 // This is a single activity course format, always show the course menu on the activity main page.
4739 if ($context->contextlevel == CONTEXT_MODULE &&
4740 !$courseformat->has_view_page()) {
4742 $this->page->navigation->initialise();
4743 $activenode = $this->page->navigation->find_active_node();
4744 // If the settings menu has been forced then show the menu.
4745 if ($this->page->is_settings_menu_forced()) {
4746 $showcoursemenu = true;
4747 } else if (!empty($activenode) && ($activenode->type == navigation_node::TYPE_ACTIVITY ||
4748 $activenode->type == navigation_node::TYPE_RESOURCE)) {
4750 // We only want to show the menu on the first page of the activity. This means
4751 // the breadcrumb has no additional nodes.
4752 if ($currentnode && ($currentnode->key == $activenode->key && $currentnode->type == $activenode->type)) {
4753 $showcoursemenu = true;
4758 // This is the site front page.
4759 if ($context->contextlevel == CONTEXT_COURSE &&
4760 !empty($currentnode) &&
4761 $currentnode->key === 'home') {
4762 $showfrontpagemenu = true;
4765 // This is the user profile page.
4766 if ($context->contextlevel == CONTEXT_USER &&
4767 !empty($currentnode) &&
4768 ($currentnode->key === 'myprofile')) {
4769 $showusermenu = true;
4772 if ($showfrontpagemenu) {
4773 $settingsnode = $this->page->settingsnav->find('frontpage', navigation_node::TYPE_SETTING);
4774 if ($settingsnode) {
4775 // Build an action menu based on the visible nodes from this navigation tree.
4776 $skipped = $this->build_action_menu_from_navigation($menu, $settingsnode, false, true);
4778 // We only add a list to the full settings menu if we didn't include every node in the short menu.
4779 if ($skipped) {
4780 $text = get_string('morenavigationlinks');
4781 $url = new moodle_url('/course/admin.php', array('courseid' => $this->page->course->id));
4782 $link = new action_link($url, $text, null, null, new pix_icon('t/edit', $text));
4783 $menu->add_secondary_action($link);
4786 } else if ($showcoursemenu) {
4787 $settingsnode = $this->page->settingsnav->find('courseadmin', navigation_node::TYPE_COURSE);
4788 if ($settingsnode) {
4789 // Build an action menu based on the visible nodes from this navigation tree.
4790 $skipped = $this->build_action_menu_from_navigation($menu, $settingsnode, false, true);
4792 // We only add a list to the full settings menu if we didn't include every node in the short menu.
4793 if ($skipped) {
4794 $text = get_string('morenavigationlinks');
4795 $url = new moodle_url('/course/admin.php', array('courseid' => $this->page->course->id));
4796 $link = new action_link($url, $text, null, null, new pix_icon('t/edit', $text));
4797 $menu->add_secondary_action($link);
4800 } else if ($showusermenu) {
4801 // Get the course admin node from the settings navigation.
4802 $settingsnode = $this->page->settingsnav->find('useraccount', navigation_node::TYPE_CONTAINER);
4803 if ($settingsnode) {
4804 // Build an action menu based on the visible nodes from this navigation tree.
4805 $this->build_action_menu_from_navigation($menu, $settingsnode);
4809 return $this->render($menu);
4813 * Take a node in the nav tree and make an action menu out of it.
4814 * The links are injected in the action menu.
4816 * @param action_menu $menu
4817 * @param navigation_node $node
4818 * @param boolean $indent
4819 * @param boolean $onlytopleafnodes
4820 * @return boolean nodesskipped - True if nodes were skipped in building the menu
4822 protected function build_action_menu_from_navigation(action_menu $menu,
4823 navigation_node $node,
4824 $indent = false,
4825 $onlytopleafnodes = false) {
4826 $skipped = false;
4827 // Build an action menu based on the visible nodes from this navigation tree.
4828 foreach ($node->children as $menuitem) {
4829 if ($menuitem->display) {
4830 if ($onlytopleafnodes && $menuitem->children->count()) {
4831 $skipped = true;
4832 continue;
4834 if ($menuitem->action) {
4835 if ($menuitem->action instanceof action_link) {
4836 $link = $menuitem->action;
4837 // Give preference to setting icon over action icon.
4838 if (!empty($menuitem->icon)) {
4839 $link->icon = $menuitem->icon;
4841 } else {
4842 $link = new action_link($menuitem->action, $menuitem->text, null, null, $menuitem->icon);
4844 } else {
4845 if ($onlytopleafnodes) {
4846 $skipped = true;
4847 continue;
4849 $link = new action_link(new moodle_url('#'), $menuitem->text, null, ['disabled' => true], $menuitem->icon);
4851 if ($indent) {
4852 $link->add_class('ml-4');
4854 if (!empty($menuitem->classes)) {
4855 $link->add_class(implode(" ", $menuitem->classes));
4858 $menu->add_secondary_action($link);
4859 $skipped = $skipped || $this->build_action_menu_from_navigation($menu, $menuitem, true);
4862 return $skipped;
4866 * This is an optional menu that can be added to a layout by a theme. It contains the
4867 * menu for the most specific thing from the settings block. E.g. Module administration.
4869 * @return string
4871 public function region_main_settings_menu() {
4872 $context = $this->page->context;
4873 $menu = new action_menu();
4875 if ($context->contextlevel == CONTEXT_MODULE) {
4877 $this->page->navigation->initialise();
4878 $node = $this->page->navigation->find_active_node();
4879 $buildmenu = false;
4880 // If the settings menu has been forced then show the menu.
4881 if ($this->page->is_settings_menu_forced()) {
4882 $buildmenu = true;
4883 } else if (!empty($node) && ($node->type == navigation_node::TYPE_ACTIVITY ||
4884 $node->type == navigation_node::TYPE_RESOURCE)) {
4886 $items = $this->page->navbar->get_items();
4887 $navbarnode = end($items);
4888 // We only want to show the menu on the first page of the activity. This means
4889 // the breadcrumb has no additional nodes.
4890 if ($navbarnode && ($navbarnode->key === $node->key && $navbarnode->type == $node->type)) {
4891 $buildmenu = true;
4894 if ($buildmenu) {
4895 // Get the course admin node from the settings navigation.
4896 $node = $this->page->settingsnav->find('modulesettings', navigation_node::TYPE_SETTING);
4897 if ($node) {
4898 // Build an action menu based on the visible nodes from this navigation tree.
4899 $this->build_action_menu_from_navigation($menu, $node);
4903 } else if ($context->contextlevel == CONTEXT_COURSECAT) {
4904 // For course category context, show category settings menu, if we're on the course category page.
4905 if ($this->page->pagetype === 'course-index-category') {
4906 $node = $this->page->settingsnav->find('categorysettings', navigation_node::TYPE_CONTAINER);
4907 if ($node) {
4908 // Build an action menu based on the visible nodes from this navigation tree.
4909 $this->build_action_menu_from_navigation($menu, $node);
4913 } else {
4914 $items = $this->page->navbar->get_items();
4915 $navbarnode = end($items);
4917 if ($navbarnode && ($navbarnode->key === 'participants')) {
4918 $node = $this->page->settingsnav->find('users', navigation_node::TYPE_CONTAINER);
4919 if ($node) {
4920 // Build an action menu based on the visible nodes from this navigation tree.
4921 $this->build_action_menu_from_navigation($menu, $node);
4926 return $this->render($menu);
4930 * Displays the list of tags associated with an entry
4932 * @param array $tags list of instances of core_tag or stdClass
4933 * @param string $label label to display in front, by default 'Tags' (get_string('tags')), set to null
4934 * to use default, set to '' (empty string) to omit the label completely
4935 * @param string $classes additional classes for the enclosing div element
4936 * @param int $limit limit the number of tags to display, if size of $tags is more than this limit the "more" link
4937 * will be appended to the end, JS will toggle the rest of the tags
4938 * @param context $pagecontext specify if needed to overwrite the current page context for the view tag link
4939 * @param bool $accesshidelabel if true, the label should have class="accesshide" added.
4940 * @return string
4942 public function tag_list($tags, $label = null, $classes = '', $limit = 10,
4943 $pagecontext = null, $accesshidelabel = false) {
4944 $list = new \core_tag\output\taglist($tags, $label, $classes, $limit, $pagecontext, $accesshidelabel);
4945 return $this->render_from_template('core_tag/taglist', $list->export_for_template($this));
4949 * Renders element for inline editing of any value
4951 * @param \core\output\inplace_editable $element
4952 * @return string
4954 public function render_inplace_editable(\core\output\inplace_editable $element) {
4955 return $this->render_from_template('core/inplace_editable', $element->export_for_template($this));
4959 * Renders a bar chart.
4961 * @param \core\chart_bar $chart The chart.
4962 * @return string
4964 public function render_chart_bar(\core\chart_bar $chart) {
4965 return $this->render_chart($chart);
4969 * Renders a line chart.
4971 * @param \core\chart_line $chart The chart.
4972 * @return string
4974 public function render_chart_line(\core\chart_line $chart) {
4975 return $this->render_chart($chart);
4979 * Renders a pie chart.
4981 * @param \core\chart_pie $chart The chart.
4982 * @return string
4984 public function render_chart_pie(\core\chart_pie $chart) {
4985 return $this->render_chart($chart);
4989 * Renders a chart.
4991 * @param \core\chart_base $chart The chart.
4992 * @param bool $withtable Whether to include a data table with the chart.
4993 * @return string
4995 public function render_chart(\core\chart_base $chart, $withtable = true) {
4996 $chartdata = json_encode($chart);
4997 return $this->render_from_template('core/chart', (object) [
4998 'chartdata' => $chartdata,
4999 'withtable' => $withtable
5004 * Renders the login form.
5006 * @param \core_auth\output\login $form The renderable.
5007 * @return string
5009 public function render_login(\core_auth\output\login $form) {
5010 global $CFG, $SITE;
5012 $context = $form->export_for_template($this);
5014 $context->errorformatted = $this->error_text($context->error);
5015 $url = $this->get_logo_url();
5016 if ($url) {
5017 $url = $url->out(false);
5019 $context->logourl = $url;
5020 $context->sitename = format_string($SITE->fullname, true,
5021 ['context' => context_course::instance(SITEID), "escape" => false]);
5023 return $this->render_from_template('core/loginform', $context);
5027 * Renders an mform element from a template.
5029 * @param HTML_QuickForm_element $element element
5030 * @param bool $required if input is required field
5031 * @param bool $advanced if input is an advanced field
5032 * @param string $error error message to display
5033 * @param bool $ingroup True if this element is rendered as part of a group
5034 * @return mixed string|bool
5036 public function mform_element($element, $required, $advanced, $error, $ingroup) {
5037 $templatename = 'core_form/element-' . $element->getType();
5038 if ($ingroup) {
5039 $templatename .= "-inline";
5041 try {
5042 // We call this to generate a file not found exception if there is no template.
5043 // We don't want to call export_for_template if there is no template.
5044 core\output\mustache_template_finder::get_template_filepath($templatename);
5046 if ($element instanceof templatable) {
5047 $elementcontext = $element->export_for_template($this);
5049 $helpbutton = '';
5050 if (method_exists($element, 'getHelpButton')) {
5051 $helpbutton = $element->getHelpButton();
5053 $label = $element->getLabel();
5054 $text = '';
5055 if (method_exists($element, 'getText')) {
5056 // There currently exists code that adds a form element with an empty label.
5057 // If this is the case then set the label to the description.
5058 if (empty($label)) {
5059 $label = $element->getText();
5060 } else {
5061 $text = $element->getText();
5065 // Generate the form element wrapper ids and names to pass to the template.
5066 // This differs between group and non-group elements.
5067 if ($element->getType() === 'group') {
5068 // Group element.
5069 // The id will be something like 'fgroup_id_NAME'. E.g. fgroup_id_mygroup.
5070 $elementcontext['wrapperid'] = $elementcontext['id'];
5072 // Ensure group elements pass through the group name as the element name.
5073 $elementcontext['name'] = $elementcontext['groupname'];
5074 } else {
5075 // Non grouped element.
5076 // Creates an id like 'fitem_id_NAME'. E.g. fitem_id_mytextelement.
5077 $elementcontext['wrapperid'] = 'fitem_' . $elementcontext['id'];
5080 $context = array(
5081 'element' => $elementcontext,
5082 'label' => $label,
5083 'text' => $text,
5084 'required' => $required,
5085 'advanced' => $advanced,
5086 'helpbutton' => $helpbutton,
5087 'error' => $error
5089 return $this->render_from_template($templatename, $context);
5091 } catch (Exception $e) {
5092 // No template for this element.
5093 return false;
5098 * Render the login signup form into a nice template for the theme.
5100 * @param moodleform $form
5101 * @return string
5103 public function render_login_signup_form($form) {
5104 global $SITE;
5106 $context = $form->export_for_template($this);
5107 $url = $this->get_logo_url();
5108 if ($url) {
5109 $url = $url->out(false);
5111 $context['logourl'] = $url;
5112 $context['sitename'] = format_string($SITE->fullname, true,
5113 ['context' => context_course::instance(SITEID), "escape" => false]);
5115 return $this->render_from_template('core/signup_form_layout', $context);
5119 * Render the verify age and location page into a nice template for the theme.
5121 * @param \core_auth\output\verify_age_location_page $page The renderable
5122 * @return string
5124 protected function render_verify_age_location_page($page) {
5125 $context = $page->export_for_template($this);
5127 return $this->render_from_template('core/auth_verify_age_location_page', $context);
5131 * Render the digital minor contact information page into a nice template for the theme.
5133 * @param \core_auth\output\digital_minor_page $page The renderable
5134 * @return string
5136 protected function render_digital_minor_page($page) {
5137 $context = $page->export_for_template($this);
5139 return $this->render_from_template('core/auth_digital_minor_page', $context);
5143 * Renders a progress bar.
5145 * Do not use $OUTPUT->render($bar), instead use progress_bar::create().
5147 * @param progress_bar $bar The bar.
5148 * @return string HTML fragment
5150 public function render_progress_bar(progress_bar $bar) {
5151 $data = $bar->export_for_template($this);
5152 return $this->render_from_template('core/progress_bar', $data);
5156 * Renders an update to a progress bar.
5158 * Note: This does not cleanly map to a renderable class and should
5159 * never be used directly.
5161 * @param string $id
5162 * @param float $percent
5163 * @param string $msg Message
5164 * @param string $estimate time remaining message
5165 * @return string ascii fragment
5167 public function render_progress_bar_update(string $id, float $percent, string $msg, string $estimate): string {
5168 return html_writer::script(js_writer::function_call('updateProgressBar', [
5169 $id,
5170 round($percent, 1),
5171 $msg,
5172 $estimate,
5173 ]));
5177 * Renders element for a toggle-all checkbox.
5179 * @param \core\output\checkbox_toggleall $element
5180 * @return string
5182 public function render_checkbox_toggleall(\core\output\checkbox_toggleall $element) {
5183 return $this->render_from_template($element->get_template(), $element->export_for_template($this));
5187 * Renders the tertiary nav for the participants page
5189 * @param object $course The course we are operating within
5190 * @param string|null $renderedbuttons Any additional buttons/content to be displayed in line with the nav
5191 * @return string
5193 public function render_participants_tertiary_nav(object $course, ?string $renderedbuttons = null) {
5194 $actionbar = new \core\output\participants_action_bar($course, $this->page, $renderedbuttons);
5195 $content = $this->render_from_template('core_course/participants_actionbar', $actionbar->export_for_template($this));
5196 return $content ?: "";
5200 * Renders release information in the footer popup
5201 * @return ?string Moodle release info.
5203 public function moodle_release() {
5204 global $CFG;
5205 if (!during_initial_install() && is_siteadmin()) {
5206 return $CFG->release;
5211 * Generate the add block button when editing mode is turned on and the user can edit blocks.
5213 * @param string $region where new blocks should be added.
5214 * @return string html for the add block button.
5216 public function addblockbutton($region = ''): string {
5217 $addblockbutton = '';
5218 $regions = $this->page->blocks->get_regions();
5219 if (count($regions) == 0) {
5220 return '';
5222 if (isset($this->page->theme->addblockposition) &&
5223 $this->page->user_is_editing() &&
5224 $this->page->user_can_edit_blocks() &&
5225 $this->page->pagelayout !== 'mycourses'
5227 $params = ['bui_addblock' => '', 'sesskey' => sesskey()];
5228 if (!empty($region)) {
5229 $params['bui_blockregion'] = $region;
5231 $url = new moodle_url($this->page->url, $params);
5232 $addblockbutton = $this->render_from_template('core/add_block_button',
5234 'link' => $url->out(false),
5235 'escapedlink' => "?{$url->get_query_string(false)}",
5236 'pagehash' => $this->page->get_edited_page_hash(),
5237 'blockregion' => $region,
5238 // The following parameters are not used since Moodle 4.2 but are
5239 // still passed for backward-compatibility.
5240 'pageType' => $this->page->pagetype,
5241 'pageLayout' => $this->page->pagelayout,
5242 'subPage' => $this->page->subpage,
5246 return $addblockbutton;
5250 * Prepares an element for streaming output
5252 * This must be used with NO_OUTPUT_BUFFERING set to true. After using this method
5253 * any subsequent prints or echos to STDOUT result in the outputted content magically
5254 * being appended inside that element rather than where the current html would be
5255 * normally. This enables pages which take some time to render incremental content to
5256 * first output a fully formed html page, including the footer, and to then stream
5257 * into an element such as the main content div. This fixes a class of page layout
5258 * bugs and reduces layout shift issues and was inspired by Facebook BigPipe.
5260 * Some use cases such as a simple page which loads content via ajax could be swapped
5261 * to this method wich saves another http request and its network latency resulting
5262 * in both lower server load and better front end performance.
5264 * You should consider giving the element you stream into a minimum height to further
5265 * reduce layout shift as the content initally streams into the element.
5267 * You can safely finish the output without closing the streamed element. You can also
5268 * call this method again to swap the target of the streaming to a new element as
5269 * often as you want.
5271 * https://www.youtube.com/watch?v=LLRig4s1_yA&t=1022s
5272 * Watch this video segment to explain how and why this 'One Weird Trick' works.
5274 * @param string $selector where new content should be appended
5275 * @param string $element which contains the streamed content
5276 * @return string html to be written
5278 public function select_element_for_append(string $selector = '#region-main [role=main]', string $element = 'div') {
5280 if (!CLI_SCRIPT && !NO_OUTPUT_BUFFERING) {
5281 throw new coding_exception('select_element_for_append used in a non-CLI script without setting NO_OUTPUT_BUFFERING.',
5282 DEBUG_DEVELOPER);
5285 // We are already streaming into this element so don't change anything.
5286 if ($this->currentselector === $selector && $this->currentelement === $element) {
5287 return;
5290 // If we have a streaming element close it before starting a new one.
5291 $html = $this->close_element_for_append();
5293 $this->currentselector = $selector;
5294 $this->currentelement = $element;
5296 // Create an unclosed element for the streamed content to append into.
5297 $id = uniqid();
5298 $html .= html_writer::start_tag($element, ['id' => $id]);
5299 $html .= html_writer::tag('script', "document.querySelector('$selector').append(document.getElementById('$id'))");
5300 $html .= "\n";
5301 return $html;
5305 * This closes any opened stream elements
5307 * @return string html to be written
5309 public function close_element_for_append() {
5310 $html = '';
5311 if ($this->currentselector !== '') {
5312 $html .= html_writer::end_tag($this->currentelement);
5313 $html .= "\n";
5314 $this->currentelement = '';
5316 return $html;
5320 * A companion method to select_element_for_append
5322 * This must be used with NO_OUTPUT_BUFFERING set to true.
5324 * This is similar but instead of appending into the element it replaces
5325 * the content in the element. Depending on the 3rd argument it can replace
5326 * the innerHTML or the outerHTML which can be useful to completely remove
5327 * the element if needed.
5329 * @param string $selector where new content should be replaced
5330 * @param string $html A chunk of well formed html
5331 * @param bool $outer Wether it replaces the innerHTML or the outerHTML
5332 * @return string html to be written
5334 public function select_element_for_replace(string $selector, string $html, bool $outer = false) {
5336 if (!CLI_SCRIPT && !NO_OUTPUT_BUFFERING) {
5337 throw new coding_exception('select_element_for_replace used in a non-CLI script without setting NO_OUTPUT_BUFFERING.',
5338 DEBUG_DEVELOPER);
5341 // Escape html for use inside a javascript string.
5342 $html = addslashes_js($html);
5343 $property = $outer ? 'outerHTML' : 'innerHTML';
5344 $output = html_writer::tag('script', "document.querySelector('$selector').$property = '$html';");
5345 $output .= "\n";
5346 return $output;
5351 * A renderer that generates output for command-line scripts.
5353 * The implementation of this renderer is probably incomplete.
5355 * @copyright 2009 Tim Hunt
5356 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
5357 * @since Moodle 2.0
5358 * @package core
5359 * @category output
5361 class core_renderer_cli extends core_renderer {
5364 * @var array $progressmaximums stores the largest percentage for a progress bar.
5365 * @return string ascii fragment
5367 private $progressmaximums = [];
5370 * Returns the page header.
5372 * @return string HTML fragment
5374 public function header() {
5375 return $this->page->heading . "\n";
5379 * Renders a Check API result
5381 * To aid in CLI consistency this status is NOT translated and the visual
5382 * width is always exactly 10 chars.
5384 * @param core\check\result $result
5385 * @return string HTML fragment
5387 protected function render_check_result(core\check\result $result) {
5388 $status = $result->get_status();
5390 $labels = [
5391 core\check\result::NA => ' ' . cli_ansi_format('<colour:darkGray>' ) . ' NA ',
5392 core\check\result::OK => ' ' . cli_ansi_format('<colour:green>') . ' OK ',
5393 core\check\result::INFO => ' ' . cli_ansi_format('<colour:blue>' ) . ' INFO ',
5394 core\check\result::UNKNOWN => ' ' . cli_ansi_format('<colour:darkGray>' ) . ' UNKNOWN ',
5395 core\check\result::WARNING => ' ' . cli_ansi_format('<colour:black><bgcolour:yellow>') . ' WARNING ',
5396 core\check\result::ERROR => ' ' . cli_ansi_format('<bgcolour:red>') . ' ERROR ',
5397 core\check\result::CRITICAL => '' . cli_ansi_format('<bgcolour:red>') . ' CRITICAL ',
5399 $string = $labels[$status] . cli_ansi_format('<colour:normal>');
5400 return $string;
5404 * Renders a Check API result
5406 * @param core\check\result $result
5407 * @return string fragment
5409 public function check_result(core\check\result $result) {
5410 return $this->render_check_result($result);
5414 * Renders a progress bar.
5416 * Do not use $OUTPUT->render($bar), instead use progress_bar::create().
5418 * @param progress_bar $bar The bar.
5419 * @return string ascii fragment
5421 public function render_progress_bar(progress_bar $bar) {
5422 global $CFG;
5424 $size = 55; // The width of the progress bar in chars.
5425 $ascii = "\n";
5427 if (stream_isatty(STDOUT)) {
5428 require_once($CFG->libdir.'/clilib.php');
5430 $ascii .= "[" . str_repeat(' ', $size) . "] 0% \n";
5431 return cli_ansi_format($ascii);
5434 $this->progressmaximums[$bar->get_id()] = 0;
5435 $ascii .= '[';
5436 return $ascii;
5440 * Renders an update to a progress bar.
5442 * Note: This does not cleanly map to a renderable class and should
5443 * never be used directly.
5445 * @param string $id
5446 * @param float $percent
5447 * @param string $msg Message
5448 * @param string $estimate time remaining message
5449 * @return string ascii fragment
5451 public function render_progress_bar_update(string $id, float $percent, string $msg, string $estimate): string {
5452 $size = 55; // The width of the progress bar in chars.
5453 $ascii = '';
5455 // If we are rendering to a terminal then we can safely use ansii codes
5456 // to move the cursor and redraw the complete progress bar each time
5457 // it is updated.
5458 if (stream_isatty(STDOUT)) {
5459 $colour = $percent == 100 ? 'green' : 'blue';
5461 $done = $percent * $size * 0.01;
5462 $whole = floor($done);
5463 $bar = "<colour:$colour>";
5464 $bar .= str_repeat('█', $whole);
5466 if ($whole < $size) {
5467 // By using unicode chars for partial blocks we can have higher
5468 // precision progress bar.
5469 $fraction = floor(($done - $whole) * 8);
5470 $bar .= core_text::substr(' ▏▎▍▌▋▊▉', $fraction, 1);
5472 // Fill the rest of the empty bar.
5473 $bar .= str_repeat(' ', $size - $whole - 1);
5476 $bar .= '<colour:normal>';
5478 if ($estimate) {
5479 $estimate = "- $estimate";
5482 $ascii .= '<cursor:up>';
5483 $ascii .= '<cursor:up>';
5484 $ascii .= sprintf("[$bar] %3.1f%% %-22s\n", $percent, $estimate);
5485 $ascii .= sprintf("%-80s\n", $msg);
5486 return cli_ansi_format($ascii);
5489 // If we are not rendering to a tty, ie when piped to another command
5490 // or on windows we need to progressively render the progress bar
5491 // which can only ever go forwards.
5492 $done = round($percent * $size * 0.01);
5493 $delta = max(0, $done - $this->progressmaximums[$id]);
5495 $ascii .= str_repeat('#', $delta);
5496 if ($percent >= 100 && $delta > 0) {
5497 $ascii .= sprintf("] %3.1f%%\n$msg\n", $percent);
5499 $this->progressmaximums[$id] += $delta;
5500 return $ascii;
5504 * Returns a template fragment representing a Heading.
5506 * @param string $text The text of the heading
5507 * @param int $level The level of importance of the heading
5508 * @param string $classes A space-separated list of CSS classes
5509 * @param string $id An optional ID
5510 * @return string A template fragment for a heading
5512 public function heading($text, $level = 2, $classes = 'main', $id = null) {
5513 $text .= "\n";
5514 switch ($level) {
5515 case 1:
5516 return '=>' . $text;
5517 case 2:
5518 return '-->' . $text;
5519 default:
5520 return $text;
5525 * Returns a template fragment representing a fatal error.
5527 * @param string $message The message to output
5528 * @param string $moreinfourl URL where more info can be found about the error
5529 * @param string $link Link for the Continue button
5530 * @param array $backtrace The execution backtrace
5531 * @param string $debuginfo Debugging information
5532 * @return string A template fragment for a fatal error
5534 public function fatal_error($message, $moreinfourl, $link, $backtrace, $debuginfo = null, $errorcode = "") {
5535 global $CFG;
5537 $output = "!!! $message !!!\n";
5539 if ($CFG->debugdeveloper) {
5540 if (!empty($debuginfo)) {
5541 $output .= $this->notification($debuginfo, 'notifytiny');
5543 if (!empty($backtrace)) {
5544 $output .= $this->notification('Stack trace: ' . format_backtrace($backtrace, true), 'notifytiny');
5548 return $output;
5552 * Returns a template fragment representing a notification.
5554 * @param string $message The message to print out.
5555 * @param string $type The type of notification. See constants on \core\output\notification.
5556 * @param bool $closebutton Whether to show a close icon to remove the notification (default true).
5557 * @return string A template fragment for a notification
5559 public function notification($message, $type = null, $closebutton = true) {
5560 $message = clean_text($message);
5561 if ($type === 'notifysuccess' || $type === 'success') {
5562 return "++ $message ++\n";
5564 return "!! $message !!\n";
5568 * There is no footer for a cli request, however we must override the
5569 * footer method to prevent the default footer.
5571 public function footer() {}
5574 * Render a notification (that is, a status message about something that has
5575 * just happened).
5577 * @param \core\output\notification $notification the notification to print out
5578 * @return string plain text output
5580 public function render_notification(\core\output\notification $notification) {
5581 return $this->notification($notification->get_message(), $notification->get_message_type());
5587 * A renderer that generates output for ajax scripts.
5589 * This renderer prevents accidental sends back only json
5590 * encoded error messages, all other output is ignored.
5592 * @copyright 2010 Petr Skoda
5593 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
5594 * @since Moodle 2.0
5595 * @package core
5596 * @category output
5598 class core_renderer_ajax extends core_renderer {
5601 * Returns a template fragment representing a fatal error.
5603 * @param string $message The message to output
5604 * @param string $moreinfourl URL where more info can be found about the error
5605 * @param string $link Link for the Continue button
5606 * @param array $backtrace The execution backtrace
5607 * @param string $debuginfo Debugging information
5608 * @return string A template fragment for a fatal error
5610 public function fatal_error($message, $moreinfourl, $link, $backtrace, $debuginfo = null, $errorcode = "") {
5611 global $CFG;
5613 $this->page->set_context(null); // ugly hack - make sure page context is set to something, we do not want bogus warnings here
5615 $e = new stdClass();
5616 $e->error = $message;
5617 $e->errorcode = $errorcode;
5618 $e->stacktrace = NULL;
5619 $e->debuginfo = NULL;
5620 $e->reproductionlink = NULL;
5621 if (!empty($CFG->debug) and $CFG->debug >= DEBUG_DEVELOPER) {
5622 $link = (string) $link;
5623 if ($link) {
5624 $e->reproductionlink = $link;
5626 if (!empty($debuginfo)) {
5627 $e->debuginfo = $debuginfo;
5629 if (!empty($backtrace)) {
5630 $e->stacktrace = format_backtrace($backtrace, true);
5633 $this->header();
5634 return json_encode($e);
5638 * Used to display a notification.
5639 * For the AJAX notifications are discarded.
5641 * @param string $message The message to print out.
5642 * @param string $type The type of notification. See constants on \core\output\notification.
5643 * @param bool $closebutton Whether to show a close icon to remove the notification (default true).
5645 public function notification($message, $type = null, $closebutton = true) {
5649 * Used to display a redirection message.
5650 * AJAX redirections should not occur and as such redirection messages
5651 * are discarded.
5653 * @param moodle_url|string $encodedurl
5654 * @param string $message
5655 * @param int $delay
5656 * @param bool $debugdisableredirect
5657 * @param string $messagetype The type of notification to show the message in.
5658 * See constants on \core\output\notification.
5660 public function redirect_message($encodedurl, $message, $delay, $debugdisableredirect,
5661 $messagetype = \core\output\notification::NOTIFY_INFO) {}
5664 * Prepares the start of an AJAX output.
5666 public function header() {
5667 // unfortunately YUI iframe upload does not support application/json
5668 if (!empty($_FILES)) {
5669 @header('Content-type: text/plain; charset=utf-8');
5670 if (!core_useragent::supports_json_contenttype()) {
5671 @header('X-Content-Type-Options: nosniff');
5673 } else if (!core_useragent::supports_json_contenttype()) {
5674 @header('Content-type: text/plain; charset=utf-8');
5675 @header('X-Content-Type-Options: nosniff');
5676 } else {
5677 @header('Content-type: application/json; charset=utf-8');
5680 // Headers to make it not cacheable and json
5681 @header('Cache-Control: no-store, no-cache, must-revalidate');
5682 @header('Cache-Control: post-check=0, pre-check=0', false);
5683 @header('Pragma: no-cache');
5684 @header('Expires: Mon, 20 Aug 1969 09:23:00 GMT');
5685 @header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
5686 @header('Accept-Ranges: none');
5690 * There is no footer for an AJAX request, however we must override the
5691 * footer method to prevent the default footer.
5693 public function footer() {}
5696 * No need for headers in an AJAX request... this should never happen.
5697 * @param string $text
5698 * @param int $level
5699 * @param string $classes
5700 * @param string $id
5702 public function heading($text, $level = 2, $classes = 'main', $id = null) {}
5708 * The maintenance renderer.
5710 * The purpose of this renderer is to block out the core renderer methods that are not usable when the site
5711 * is running a maintenance related task.
5712 * It must always extend the core_renderer as we switch from the core_renderer to this renderer in a couple of places.
5714 * @since Moodle 2.6
5715 * @package core
5716 * @category output
5717 * @copyright 2013 Sam Hemelryk
5718 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
5720 class core_renderer_maintenance extends core_renderer {
5723 * Initialises the renderer instance.
5725 * @param moodle_page $page
5726 * @param string $target
5727 * @throws coding_exception
5729 public function __construct(moodle_page $page, $target) {
5730 if ($target !== RENDERER_TARGET_MAINTENANCE || $page->pagelayout !== 'maintenance') {
5731 throw new coding_exception('Invalid request for the maintenance renderer.');
5733 parent::__construct($page, $target);
5737 * Does nothing. The maintenance renderer cannot produce blocks.
5739 * @param block_contents $bc
5740 * @param string $region
5741 * @return string
5743 public function block(block_contents $bc, $region) {
5744 return '';
5748 * Does nothing. The maintenance renderer cannot produce blocks.
5750 * @param string $region
5751 * @param array $classes
5752 * @param string $tag
5753 * @param boolean $fakeblocksonly
5754 * @return string
5756 public function blocks($region, $classes = array(), $tag = 'aside', $fakeblocksonly = false) {
5757 return '';
5761 * Does nothing. The maintenance renderer cannot produce blocks.
5763 * @param string $region
5764 * @param boolean $fakeblocksonly Output fake block only.
5765 * @return string
5767 public function blocks_for_region($region, $fakeblocksonly = false) {
5768 return '';
5772 * Does nothing. The maintenance renderer cannot produce a course content header.
5774 * @param bool $onlyifnotcalledbefore
5775 * @return string
5777 public function course_content_header($onlyifnotcalledbefore = false) {
5778 return '';
5782 * Does nothing. The maintenance renderer cannot produce a course content footer.
5784 * @param bool $onlyifnotcalledbefore
5785 * @return string
5787 public function course_content_footer($onlyifnotcalledbefore = false) {
5788 return '';
5792 * Does nothing. The maintenance renderer cannot produce a course header.
5794 * @return string
5796 public function course_header() {
5797 return '';
5801 * Does nothing. The maintenance renderer cannot produce a course footer.
5803 * @return string
5805 public function course_footer() {
5806 return '';
5810 * Does nothing. The maintenance renderer cannot produce a custom menu.
5812 * @param string $custommenuitems
5813 * @return string
5815 public function custom_menu($custommenuitems = '') {
5816 return '';
5820 * Does nothing. The maintenance renderer cannot produce a file picker.
5822 * @param array $options
5823 * @return string
5825 public function file_picker($options) {
5826 return '';
5830 * Overridden confirm message for upgrades.
5832 * @param string $message The question to ask the user
5833 * @param single_button|moodle_url|string $continue The single_button component representing the Continue answer.
5834 * @param single_button|moodle_url|string $cancel The single_button component representing the Cancel answer.
5835 * @param array $displayoptions optional extra display options
5836 * @return string HTML fragment
5838 public function confirm($message, $continue, $cancel, array $displayoptions = []) {
5839 // We need plain styling of confirm boxes on upgrade because we don't know which stylesheet we have (it could be
5840 // from any previous version of Moodle).
5841 if ($continue instanceof single_button) {
5842 $continue->type = single_button::BUTTON_PRIMARY;
5843 } else if (is_string($continue)) {
5844 $continue = new single_button(new moodle_url($continue), get_string('continue'), 'post',
5845 $displayoptions['type'] ?? single_button::BUTTON_PRIMARY);
5846 } else if ($continue instanceof moodle_url) {
5847 $continue = new single_button($continue, get_string('continue'), 'post',
5848 $displayoptions['type'] ?? single_button::BUTTON_PRIMARY);
5849 } else {
5850 throw new coding_exception('The continue param to $OUTPUT->confirm() must be either a URL' .
5851 ' (string/moodle_url) or a single_button instance.');
5854 if ($cancel instanceof single_button) {
5855 $output = '';
5856 } else if (is_string($cancel)) {
5857 $cancel = new single_button(new moodle_url($cancel), get_string('cancel'), 'get');
5858 } else if ($cancel instanceof moodle_url) {
5859 $cancel = new single_button($cancel, get_string('cancel'), 'get');
5860 } else {
5861 throw new coding_exception('The cancel param to $OUTPUT->confirm() must be either a URL' .
5862 ' (string/moodle_url) or a single_button instance.');
5865 $output = $this->box_start('generalbox', 'notice');
5866 $output .= html_writer::tag('h4', get_string('confirm'));
5867 $output .= html_writer::tag('p', $message);
5868 $output .= html_writer::tag('div', $this->render($cancel) . $this->render($continue), ['class' => 'buttons']);
5869 $output .= $this->box_end();
5870 return $output;
5874 * Does nothing. The maintenance renderer does not support JS.
5876 * @param block_contents $bc
5878 public function init_block_hider_js(block_contents $bc) {
5879 // Does nothing.
5883 * Does nothing. The maintenance renderer cannot produce language menus.
5885 * @return string
5887 public function lang_menu() {
5888 return '';
5892 * Does nothing. The maintenance renderer has no need for login information.
5894 * @param mixed $withlinks
5895 * @return string
5897 public function login_info($withlinks = null) {
5898 return '';
5902 * Secure login info.
5904 * @return string
5906 public function secure_login_info() {
5907 return $this->login_info(false);
5911 * Does nothing. The maintenance renderer cannot produce user pictures.
5913 * @param stdClass $user
5914 * @param array $options
5915 * @return string
5917 public function user_picture(stdClass $user, array $options = null) {
5918 return '';