Merge branch 'MDL-63137-master' of git://github.com/aanabit/moodle
[moodle.git] / lib / outputrenderers.php
blob437a1f3d81461f245aeddd951390670e1d40457e
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 defined('MOODLE_INTERNAL') || die();
40 /**
41 * Simple base class for Moodle renderers.
43 * Tracks the xhtml_container_stack to use, which is passed in in the constructor.
45 * Also has methods to facilitate generating HTML output.
47 * @copyright 2009 Tim Hunt
48 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
49 * @since Moodle 2.0
50 * @package core
51 * @category output
53 class renderer_base {
54 /**
55 * @var xhtml_container_stack The xhtml_container_stack to use.
57 protected $opencontainers;
59 /**
60 * @var moodle_page The Moodle page the renderer has been created to assist with.
62 protected $page;
64 /**
65 * @var string The requested rendering target.
67 protected $target;
69 /**
70 * @var Mustache_Engine $mustache The mustache template compiler
72 private $mustache;
74 /**
75 * Return an instance of the mustache class.
77 * @since 2.9
78 * @return Mustache_Engine
80 protected function get_mustache() {
81 global $CFG;
83 if ($this->mustache === null) {
84 require_once("{$CFG->libdir}/filelib.php");
86 $themename = $this->page->theme->name;
87 $themerev = theme_get_revision();
89 // Create new localcache directory.
90 $cachedir = make_localcache_directory("mustache/$themerev/$themename");
92 // Remove old localcache directories.
93 $mustachecachedirs = glob("{$CFG->localcachedir}/mustache/*", GLOB_ONLYDIR);
94 foreach ($mustachecachedirs as $localcachedir) {
95 $cachedrev = [];
96 preg_match("/\/mustache\/([0-9]+)$/", $localcachedir, $cachedrev);
97 $cachedrev = isset($cachedrev[1]) ? intval($cachedrev[1]) : 0;
98 if ($cachedrev > 0 && $cachedrev < $themerev) {
99 fulldelete($localcachedir);
103 $loader = new \core\output\mustache_filesystem_loader();
104 $stringhelper = new \core\output\mustache_string_helper();
105 $quotehelper = new \core\output\mustache_quote_helper();
106 $jshelper = new \core\output\mustache_javascript_helper($this->page);
107 $pixhelper = new \core\output\mustache_pix_helper($this);
108 $shortentexthelper = new \core\output\mustache_shorten_text_helper();
109 $userdatehelper = new \core\output\mustache_user_date_helper();
111 // We only expose the variables that are exposed to JS templates.
112 $safeconfig = $this->page->requires->get_config_for_javascript($this->page, $this);
114 $helpers = array('config' => $safeconfig,
115 'str' => array($stringhelper, 'str'),
116 'quote' => array($quotehelper, 'quote'),
117 'js' => array($jshelper, 'help'),
118 'pix' => array($pixhelper, 'pix'),
119 'shortentext' => array($shortentexthelper, 'shorten'),
120 'userdate' => array($userdatehelper, 'transform'),
123 $this->mustache = new Mustache_Engine(array(
124 'cache' => $cachedir,
125 'escape' => 's',
126 'loader' => $loader,
127 'helpers' => $helpers,
128 'pragmas' => [Mustache_Engine::PRAGMA_BLOCKS]));
132 return $this->mustache;
137 * Constructor
139 * The constructor takes two arguments. The first is the page that the renderer
140 * has been created to assist with, and the second is the target.
141 * The target is an additional identifier that can be used to load different
142 * renderers for different options.
144 * @param moodle_page $page the page we are doing output for.
145 * @param string $target one of rendering target constants
147 public function __construct(moodle_page $page, $target) {
148 $this->opencontainers = $page->opencontainers;
149 $this->page = $page;
150 $this->target = $target;
154 * Renders a template by name with the given context.
156 * The provided data needs to be array/stdClass made up of only simple types.
157 * Simple types are array,stdClass,bool,int,float,string
159 * @since 2.9
160 * @param array|stdClass $context Context containing data for the template.
161 * @return string|boolean
163 public function render_from_template($templatename, $context) {
164 static $templatecache = array();
165 $mustache = $this->get_mustache();
167 try {
168 // Grab a copy of the existing helper to be restored later.
169 $uniqidhelper = $mustache->getHelper('uniqid');
170 } catch (Mustache_Exception_UnknownHelperException $e) {
171 // Helper doesn't exist.
172 $uniqidhelper = null;
175 // Provide 1 random value that will not change within a template
176 // but will be different from template to template. This is useful for
177 // e.g. aria attributes that only work with id attributes and must be
178 // unique in a page.
179 $mustache->addHelper('uniqid', new \core\output\mustache_uniqid_helper());
180 if (isset($templatecache[$templatename])) {
181 $template = $templatecache[$templatename];
182 } else {
183 try {
184 $template = $mustache->loadTemplate($templatename);
185 $templatecache[$templatename] = $template;
186 } catch (Mustache_Exception_UnknownTemplateException $e) {
187 throw new moodle_exception('Unknown template: ' . $templatename);
191 $renderedtemplate = trim($template->render($context));
193 // If we had an existing uniqid helper then we need to restore it to allow
194 // handle nested calls of render_from_template.
195 if ($uniqidhelper) {
196 $mustache->addHelper('uniqid', $uniqidhelper);
199 return $renderedtemplate;
204 * Returns rendered widget.
206 * The provided widget needs to be an object that extends the renderable
207 * interface.
208 * If will then be rendered by a method based upon the classname for the widget.
209 * For instance a widget of class `crazywidget` will be rendered by a protected
210 * render_crazywidget method of this renderer.
211 * If no render_crazywidget method exists and crazywidget implements templatable,
212 * look for the 'crazywidget' template in the same component and render that.
214 * @param renderable $widget instance with renderable interface
215 * @return string
217 public function render(renderable $widget) {
218 $classparts = explode('\\', get_class($widget));
219 // Strip namespaces.
220 $classname = array_pop($classparts);
221 // Remove _renderable suffixes
222 $classname = preg_replace('/_renderable$/', '', $classname);
224 $rendermethod = 'render_'.$classname;
225 if (method_exists($this, $rendermethod)) {
226 return $this->$rendermethod($widget);
228 if ($widget instanceof templatable) {
229 $component = array_shift($classparts);
230 if (!$component) {
231 $component = 'core';
233 $template = $component . '/' . $classname;
234 $context = $widget->export_for_template($this);
235 return $this->render_from_template($template, $context);
237 throw new coding_exception('Can not render widget, renderer method ('.$rendermethod.') not found.');
241 * Adds a JS action for the element with the provided id.
243 * This method adds a JS event for the provided component action to the page
244 * and then returns the id that the event has been attached to.
245 * If no id has been provided then a new ID is generated by {@link html_writer::random_id()}
247 * @param component_action $action
248 * @param string $id
249 * @return string id of element, either original submitted or random new if not supplied
251 public function add_action_handler(component_action $action, $id = null) {
252 if (!$id) {
253 $id = html_writer::random_id($action->event);
255 $this->page->requires->event_handler("#$id", $action->event, $action->jsfunction, $action->jsfunctionargs);
256 return $id;
260 * Returns true is output has already started, and false if not.
262 * @return boolean true if the header has been printed.
264 public function has_started() {
265 return $this->page->state >= moodle_page::STATE_IN_BODY;
269 * Given an array or space-separated list of classes, prepares and returns the HTML class attribute value
271 * @param mixed $classes Space-separated string or array of classes
272 * @return string HTML class attribute value
274 public static function prepare_classes($classes) {
275 if (is_array($classes)) {
276 return implode(' ', array_unique($classes));
278 return $classes;
282 * Return the direct URL for an image from the pix folder.
284 * Use this function sparingly and never for icons. For icons use pix_icon or the pix helper in a mustache template.
286 * @deprecated since Moodle 3.3
287 * @param string $imagename the name of the icon.
288 * @param string $component specification of one plugin like in get_string()
289 * @return moodle_url
291 public function pix_url($imagename, $component = 'moodle') {
292 debugging('pix_url is deprecated. Use image_url for images and pix_icon for icons.', DEBUG_DEVELOPER);
293 return $this->page->theme->image_url($imagename, $component);
297 * Return the moodle_url for an image.
299 * The exact image location and extension is determined
300 * automatically by searching for gif|png|jpg|jpeg, please
301 * note there can not be diferent images with the different
302 * extension. The imagename is for historical reasons
303 * a relative path name, it may be changed later for core
304 * images. It is recommended to not use subdirectories
305 * in plugin and theme pix directories.
307 * There are three types of images:
308 * 1/ theme images - stored in theme/mytheme/pix/,
309 * use component 'theme'
310 * 2/ core images - stored in /pix/,
311 * overridden via theme/mytheme/pix_core/
312 * 3/ plugin images - stored in mod/mymodule/pix,
313 * overridden via theme/mytheme/pix_plugins/mod/mymodule/,
314 * example: image_url('comment', 'mod_glossary')
316 * @param string $imagename the pathname of the image
317 * @param string $component full plugin name (aka component) or 'theme'
318 * @return moodle_url
320 public function image_url($imagename, $component = 'moodle') {
321 return $this->page->theme->image_url($imagename, $component);
325 * Return the site's logo URL, if any.
327 * @param int $maxwidth The maximum width, or null when the maximum width does not matter.
328 * @param int $maxheight The maximum height, or null when the maximum height does not matter.
329 * @return moodle_url|false
331 public function get_logo_url($maxwidth = null, $maxheight = 200) {
332 global $CFG;
333 $logo = get_config('core_admin', 'logo');
334 if (empty($logo)) {
335 return false;
338 // 200px high is the default image size which should be displayed at 100px in the page to account for retina displays.
339 // It's not worth the overhead of detecting and serving 2 different images based on the device.
341 // Hide the requested size in the file path.
342 $filepath = ((int) $maxwidth . 'x' . (int) $maxheight) . '/';
344 // Use $CFG->themerev to prevent browser caching when the file changes.
345 return moodle_url::make_pluginfile_url(context_system::instance()->id, 'core_admin', 'logo', $filepath,
346 theme_get_revision(), $logo);
350 * Return the site's compact logo URL, if any.
352 * @param int $maxwidth The maximum width, or null when the maximum width does not matter.
353 * @param int $maxheight The maximum height, or null when the maximum height does not matter.
354 * @return moodle_url|false
356 public function get_compact_logo_url($maxwidth = 100, $maxheight = 100) {
357 global $CFG;
358 $logo = get_config('core_admin', 'logocompact');
359 if (empty($logo)) {
360 return false;
363 // Hide the requested size in the file path.
364 $filepath = ((int) $maxwidth . 'x' . (int) $maxheight) . '/';
366 // Use $CFG->themerev to prevent browser caching when the file changes.
367 return moodle_url::make_pluginfile_url(context_system::instance()->id, 'core_admin', 'logocompact', $filepath,
368 theme_get_revision(), $logo);
372 * Whether we should display the logo in the navbar.
374 * We will when there are no main logos, and we have compact logo.
376 * @return bool
378 public function should_display_navbar_logo() {
379 $logo = $this->get_compact_logo_url();
380 return !empty($logo) && !$this->should_display_main_logo();
384 * Whether we should display the main logo.
386 * @param int $headinglevel
387 * @return bool
389 public function should_display_main_logo($headinglevel = 1) {
390 global $PAGE;
392 // Only render the logo if we're on the front page or login page and the we have a logo.
393 $logo = $this->get_logo_url();
394 if ($headinglevel == 1 && !empty($logo)) {
395 if ($PAGE->pagelayout == 'frontpage' || $PAGE->pagelayout == 'login') {
396 return true;
400 return false;
407 * Basis for all plugin renderers.
409 * @copyright Petr Skoda (skodak)
410 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
411 * @since Moodle 2.0
412 * @package core
413 * @category output
415 class plugin_renderer_base extends renderer_base {
418 * @var renderer_base|core_renderer A reference to the current renderer.
419 * The renderer provided here will be determined by the page but will in 90%
420 * of cases by the {@link core_renderer}
422 protected $output;
425 * Constructor method, calls the parent constructor
427 * @param moodle_page $page
428 * @param string $target one of rendering target constants
430 public function __construct(moodle_page $page, $target) {
431 if (empty($target) && $page->pagelayout === 'maintenance') {
432 // If the page is using the maintenance layout then we're going to force the target to maintenance.
433 // This way we'll get a special maintenance renderer that is designed to block access to API's that are likely
434 // unavailable for this page layout.
435 $target = RENDERER_TARGET_MAINTENANCE;
437 $this->output = $page->get_renderer('core', null, $target);
438 parent::__construct($page, $target);
442 * Renders the provided widget and returns the HTML to display it.
444 * @param renderable $widget instance with renderable interface
445 * @return string
447 public function render(renderable $widget) {
448 $classname = get_class($widget);
449 // Strip namespaces.
450 $classname = preg_replace('/^.*\\\/', '', $classname);
451 // Keep a copy at this point, we may need to look for a deprecated method.
452 $deprecatedmethod = 'render_'.$classname;
453 // Remove _renderable suffixes
454 $classname = preg_replace('/_renderable$/', '', $classname);
456 $rendermethod = 'render_'.$classname;
457 if (method_exists($this, $rendermethod)) {
458 return $this->$rendermethod($widget);
460 if ($rendermethod !== $deprecatedmethod && method_exists($this, $deprecatedmethod)) {
461 // This is exactly where we don't want to be.
462 // If you have arrived here you have a renderable component within your plugin that has the name
463 // blah_renderable, and you have a render method render_blah_renderable on your plugin.
464 // In 2.8 we revamped output, as part of this change we changed slightly how renderables got rendered
465 // and the _renderable suffix now gets removed when looking for a render method.
466 // You need to change your renderers render_blah_renderable to render_blah.
467 // Until you do this it will not be possible for a theme to override the renderer to override your method.
468 // Please do it ASAP.
469 static $debugged = array();
470 if (!isset($debugged[$deprecatedmethod])) {
471 debugging(sprintf('Deprecated call. Please rename your renderables render method from %s to %s.',
472 $deprecatedmethod, $rendermethod), DEBUG_DEVELOPER);
473 $debugged[$deprecatedmethod] = true;
475 return $this->$deprecatedmethod($widget);
477 // pass to core renderer if method not found here
478 return $this->output->render($widget);
482 * Magic method used to pass calls otherwise meant for the standard renderer
483 * to it to ensure we don't go causing unnecessary grief.
485 * @param string $method
486 * @param array $arguments
487 * @return mixed
489 public function __call($method, $arguments) {
490 if (method_exists('renderer_base', $method)) {
491 throw new coding_exception('Protected method called against '.get_class($this).' :: '.$method);
493 if (method_exists($this->output, $method)) {
494 return call_user_func_array(array($this->output, $method), $arguments);
495 } else {
496 throw new coding_exception('Unknown method called against '.get_class($this).' :: '.$method);
503 * The standard implementation of the core_renderer interface.
505 * @copyright 2009 Tim Hunt
506 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
507 * @since Moodle 2.0
508 * @package core
509 * @category output
511 class core_renderer extends renderer_base {
513 * Do NOT use, please use <?php echo $OUTPUT->main_content() ?>
514 * in layout files instead.
515 * @deprecated
516 * @var string used in {@link core_renderer::header()}.
518 const MAIN_CONTENT_TOKEN = '[MAIN CONTENT GOES HERE]';
521 * @var string Used to pass information from {@link core_renderer::doctype()} to
522 * {@link core_renderer::standard_head_html()}.
524 protected $contenttype;
527 * @var string Used by {@link core_renderer::redirect_message()} method to communicate
528 * with {@link core_renderer::header()}.
530 protected $metarefreshtag = '';
533 * @var string Unique token for the closing HTML
535 protected $unique_end_html_token;
538 * @var string Unique token for performance information
540 protected $unique_performance_info_token;
543 * @var string Unique token for the main content.
545 protected $unique_main_content_token;
547 /** @var custom_menu_item language The language menu if created */
548 protected $language = null;
551 * Constructor
553 * @param moodle_page $page the page we are doing output for.
554 * @param string $target one of rendering target constants
556 public function __construct(moodle_page $page, $target) {
557 $this->opencontainers = $page->opencontainers;
558 $this->page = $page;
559 $this->target = $target;
561 $this->unique_end_html_token = '%%ENDHTML-'.sesskey().'%%';
562 $this->unique_performance_info_token = '%%PERFORMANCEINFO-'.sesskey().'%%';
563 $this->unique_main_content_token = '[MAIN CONTENT GOES HERE - '.sesskey().']';
567 * Get the DOCTYPE declaration that should be used with this page. Designed to
568 * be called in theme layout.php files.
570 * @return string the DOCTYPE declaration that should be used.
572 public function doctype() {
573 if ($this->page->theme->doctype === 'html5') {
574 $this->contenttype = 'text/html; charset=utf-8';
575 return "<!DOCTYPE html>\n";
577 } else if ($this->page->theme->doctype === 'xhtml5') {
578 $this->contenttype = 'application/xhtml+xml; charset=utf-8';
579 return "<!DOCTYPE html>\n";
581 } else {
582 // legacy xhtml 1.0
583 $this->contenttype = 'text/html; charset=utf-8';
584 return ('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">' . "\n");
589 * The attributes that should be added to the <html> tag. Designed to
590 * be called in theme layout.php files.
592 * @return string HTML fragment.
594 public function htmlattributes() {
595 $return = get_html_lang(true);
596 $attributes = array();
597 if ($this->page->theme->doctype !== 'html5') {
598 $attributes['xmlns'] = 'http://www.w3.org/1999/xhtml';
601 // Give plugins an opportunity to add things like xml namespaces to the html element.
602 // This function should return an array of html attribute names => values.
603 $pluginswithfunction = get_plugins_with_function('add_htmlattributes', 'lib.php');
604 foreach ($pluginswithfunction as $plugins) {
605 foreach ($plugins as $function) {
606 $newattrs = $function();
607 unset($newattrs['dir']);
608 unset($newattrs['lang']);
609 unset($newattrs['xmlns']);
610 unset($newattrs['xml:lang']);
611 $attributes += $newattrs;
615 foreach ($attributes as $key => $val) {
616 $val = s($val);
617 $return .= " $key=\"$val\"";
620 return $return;
624 * The standard tags (meta tags, links to stylesheets and JavaScript, etc.)
625 * that should be included in the <head> tag. Designed to be called in theme
626 * layout.php files.
628 * @return string HTML fragment.
630 public function standard_head_html() {
631 global $CFG, $SESSION, $SITE, $PAGE;
633 // Before we output any content, we need to ensure that certain
634 // page components are set up.
636 // Blocks must be set up early as they may require javascript which
637 // has to be included in the page header before output is created.
638 foreach ($this->page->blocks->get_regions() as $region) {
639 $this->page->blocks->ensure_content_created($region, $this);
642 $output = '';
644 // Give plugins an opportunity to add any head elements. The callback
645 // must always return a string containing valid html head content.
646 $pluginswithfunction = get_plugins_with_function('before_standard_html_head', 'lib.php');
647 foreach ($pluginswithfunction as $plugins) {
648 foreach ($plugins as $function) {
649 $output .= $function();
653 // Allow a url_rewrite plugin to setup any dynamic head content.
654 if (isset($CFG->urlrewriteclass) && !isset($CFG->upgraderunning)) {
655 $class = $CFG->urlrewriteclass;
656 $output .= $class::html_head_setup();
659 $output .= '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />' . "\n";
660 $output .= '<meta name="keywords" content="moodle, ' . $this->page->title . '" />' . "\n";
661 // This is only set by the {@link redirect()} method
662 $output .= $this->metarefreshtag;
664 // Check if a periodic refresh delay has been set and make sure we arn't
665 // already meta refreshing
666 if ($this->metarefreshtag=='' && $this->page->periodicrefreshdelay!==null) {
667 $output .= '<meta http-equiv="refresh" content="'.$this->page->periodicrefreshdelay.';url='.$this->page->url->out().'" />';
670 // Set up help link popups for all links with the helptooltip class
671 $this->page->requires->js_init_call('M.util.help_popups.setup');
673 $focus = $this->page->focuscontrol;
674 if (!empty($focus)) {
675 if (preg_match("#forms\['([a-zA-Z0-9]+)'\].elements\['([a-zA-Z0-9]+)'\]#", $focus, $matches)) {
676 // This is a horrifically bad way to handle focus but it is passed in
677 // through messy formslib::moodleform
678 $this->page->requires->js_function_call('old_onload_focus', array($matches[1], $matches[2]));
679 } else if (strpos($focus, '.')!==false) {
680 // Old style of focus, bad way to do it
681 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);
682 $this->page->requires->js_function_call('old_onload_focus', explode('.', $focus, 2));
683 } else {
684 // Focus element with given id
685 $this->page->requires->js_function_call('focuscontrol', array($focus));
689 // Get the theme stylesheet - this has to be always first CSS, this loads also styles.css from all plugins;
690 // any other custom CSS can not be overridden via themes and is highly discouraged
691 $urls = $this->page->theme->css_urls($this->page);
692 foreach ($urls as $url) {
693 $this->page->requires->css_theme($url);
696 // Get the theme javascript head and footer
697 if ($jsurl = $this->page->theme->javascript_url(true)) {
698 $this->page->requires->js($jsurl, true);
700 if ($jsurl = $this->page->theme->javascript_url(false)) {
701 $this->page->requires->js($jsurl);
704 // Get any HTML from the page_requirements_manager.
705 $output .= $this->page->requires->get_head_code($this->page, $this);
707 // List alternate versions.
708 foreach ($this->page->alternateversions as $type => $alt) {
709 $output .= html_writer::empty_tag('link', array('rel' => 'alternate',
710 'type' => $type, 'title' => $alt->title, 'href' => $alt->url));
713 // Add noindex tag if relevant page and setting applied.
714 $allowindexing = isset($CFG->allowindexing) ? $CFG->allowindexing : 0;
715 $loginpages = array('login-index', 'login-signup');
716 if ($allowindexing == 2 || ($allowindexing == 0 && in_array($this->page->pagetype, $loginpages))) {
717 if (!isset($CFG->additionalhtmlhead)) {
718 $CFG->additionalhtmlhead = '';
720 $CFG->additionalhtmlhead .= '<meta name="robots" content="noindex" />';
723 if (!empty($CFG->additionalhtmlhead)) {
724 $output .= "\n".$CFG->additionalhtmlhead;
727 if ($PAGE->pagelayout == 'frontpage') {
728 $summary = s(strip_tags(format_text($SITE->summary, FORMAT_HTML)));
729 if (!empty($summary)) {
730 $output .= "<meta name=\"description\" content=\"$summary\" />\n";
734 return $output;
738 * The standard tags (typically skip links) that should be output just inside
739 * the start of the <body> tag. Designed to be called in theme layout.php files.
741 * @return string HTML fragment.
743 public function standard_top_of_body_html() {
744 global $CFG;
745 $output = $this->page->requires->get_top_of_body_code($this);
746 if ($this->page->pagelayout !== 'embedded' && !empty($CFG->additionalhtmltopofbody)) {
747 $output .= "\n".$CFG->additionalhtmltopofbody;
750 // Give subsystems an opportunity to inject extra html content. The callback
751 // must always return a string containing valid html.
752 foreach (\core_component::get_core_subsystems() as $name => $path) {
753 if ($path) {
754 $output .= component_callback($name, 'before_standard_top_of_body_html', [], '');
758 // Give plugins an opportunity to inject extra html content. The callback
759 // must always return a string containing valid html.
760 $pluginswithfunction = get_plugins_with_function('before_standard_top_of_body_html', 'lib.php');
761 foreach ($pluginswithfunction as $plugins) {
762 foreach ($plugins as $function) {
763 $output .= $function();
767 $output .= $this->maintenance_warning();
769 return $output;
773 * Scheduled maintenance warning message.
775 * Note: This is a nasty hack to display maintenance notice, this should be moved
776 * to some general notification area once we have it.
778 * @return string
780 public function maintenance_warning() {
781 global $CFG;
783 $output = '';
784 if (isset($CFG->maintenance_later) and $CFG->maintenance_later > time()) {
785 $timeleft = $CFG->maintenance_later - time();
786 // If timeleft less than 30 sec, set the class on block to error to highlight.
787 $errorclass = ($timeleft < 30) ? 'alert-error alert-danger' : 'alert-warning';
788 $output .= $this->box_start($errorclass . ' moodle-has-zindex maintenancewarning m-a-1 alert');
789 $a = new stdClass();
790 $a->hour = (int)($timeleft / 3600);
791 $a->min = (int)(($timeleft / 60) % 60);
792 $a->sec = (int)($timeleft % 60);
793 if ($a->hour > 0) {
794 $output .= get_string('maintenancemodeisscheduledlong', 'admin', $a);
795 } else {
796 $output .= get_string('maintenancemodeisscheduled', 'admin', $a);
799 $output .= $this->box_end();
800 $this->page->requires->yui_module('moodle-core-maintenancemodetimer', 'M.core.maintenancemodetimer',
801 array(array('timeleftinsec' => $timeleft)));
802 $this->page->requires->strings_for_js(
803 array('maintenancemodeisscheduled', 'maintenancemodeisscheduledlong', 'sitemaintenance'),
804 'admin');
806 return $output;
810 * The standard tags (typically performance information and validation links,
811 * if we are in developer debug mode) that should be output in the footer area
812 * of the page. Designed to be called in theme layout.php files.
814 * @return string HTML fragment.
816 public function standard_footer_html() {
817 global $CFG, $SCRIPT;
819 $output = '';
820 if (during_initial_install()) {
821 // Debugging info can not work before install is finished,
822 // in any case we do not want any links during installation!
823 return $output;
826 // Give plugins an opportunity to add any footer elements.
827 // The callback must always return a string containing valid html footer content.
828 $pluginswithfunction = get_plugins_with_function('standard_footer_html', 'lib.php');
829 foreach ($pluginswithfunction as $plugins) {
830 foreach ($plugins as $function) {
831 $output .= $function();
835 // This function is normally called from a layout.php file in {@link core_renderer::header()}
836 // but some of the content won't be known until later, so we return a placeholder
837 // for now. This will be replaced with the real content in {@link core_renderer::footer()}.
838 $output .= $this->unique_performance_info_token;
839 if ($this->page->devicetypeinuse == 'legacy') {
840 // The legacy theme is in use print the notification
841 $output .= html_writer::tag('div', get_string('legacythemeinuse'), array('class'=>'legacythemeinuse'));
844 // Get links to switch device types (only shown for users not on a default device)
845 $output .= $this->theme_switch_links();
847 if (!empty($CFG->debugpageinfo)) {
848 $output .= '<div class="performanceinfo pageinfo">' . get_string('pageinfodebugsummary', 'core_admin',
849 $this->page->debug_summary()) . '</div>';
851 if (debugging(null, DEBUG_DEVELOPER) and has_capability('moodle/site:config', context_system::instance())) { // Only in developer mode
852 // Add link to profiling report if necessary
853 if (function_exists('profiling_is_running') && profiling_is_running()) {
854 $txt = get_string('profiledscript', 'admin');
855 $title = get_string('profiledscriptview', 'admin');
856 $url = $CFG->wwwroot . '/admin/tool/profiling/index.php?script=' . urlencode($SCRIPT);
857 $link= '<a title="' . $title . '" href="' . $url . '">' . $txt . '</a>';
858 $output .= '<div class="profilingfooter">' . $link . '</div>';
860 $purgeurl = new moodle_url('/admin/purgecaches.php', array('confirm' => 1,
861 'sesskey' => sesskey(), 'returnurl' => $this->page->url->out_as_local_url(false)));
862 $output .= '<div class="purgecaches">' .
863 html_writer::link($purgeurl, get_string('purgecaches', 'admin')) . '</div>';
865 if (!empty($CFG->debugvalidators)) {
866 // NOTE: this is not a nice hack, $PAGE->url is not always accurate and $FULLME neither, it is not a bug if it fails. --skodak
867 $output .= '<div class="validators"><ul class="list-unstyled ml-1">
868 <li><a href="http://validator.w3.org/check?verbose=1&amp;ss=1&amp;uri=' . urlencode(qualified_me()) . '">Validate HTML</a></li>
869 <li><a href="http://www.contentquality.com/mynewtester/cynthia.exe?rptmode=-1&amp;url1=' . urlencode(qualified_me()) . '">Section 508 Check</a></li>
870 <li><a href="http://www.contentquality.com/mynewtester/cynthia.exe?rptmode=0&amp;warnp2n3e=1&amp;url1=' . urlencode(qualified_me()) . '">WCAG 1 (2,3) Check</a></li>
871 </ul></div>';
873 return $output;
877 * Returns standard main content placeholder.
878 * Designed to be called in theme layout.php files.
880 * @return string HTML fragment.
882 public function main_content() {
883 // This is here because it is the only place we can inject the "main" role over the entire main content area
884 // without requiring all theme's to manually do it, and without creating yet another thing people need to
885 // remember in the theme.
886 // This is an unfortunate hack. DO NO EVER add anything more here.
887 // DO NOT add classes.
888 // DO NOT add an id.
889 return '<div role="main">'.$this->unique_main_content_token.'</div>';
893 * Returns standard navigation between activities in a course.
895 * @return string the navigation HTML.
897 public function activity_navigation() {
898 // First we should check if we want to add navigation.
899 $context = $this->page->context;
900 if (($this->page->pagelayout !== 'incourse' && $this->page->pagelayout !== 'frametop')
901 || $context->contextlevel != CONTEXT_MODULE) {
902 return '';
905 // If the activity is in stealth mode, show no links.
906 if ($this->page->cm->is_stealth()) {
907 return '';
910 // Get a list of all the activities in the course.
911 $course = $this->page->cm->get_course();
912 $modules = get_fast_modinfo($course->id)->get_cms();
914 // Put the modules into an array in order by the position they are shown in the course.
915 $mods = [];
916 $activitylist = [];
917 foreach ($modules as $module) {
918 // Only add activities the user can access, aren't in stealth mode and have a url (eg. mod_label does not).
919 if (!$module->uservisible || $module->is_stealth() || empty($module->url)) {
920 continue;
922 $mods[$module->id] = $module;
924 // No need to add the current module to the list for the activity dropdown menu.
925 if ($module->id == $this->page->cm->id) {
926 continue;
928 // Module name.
929 $modname = $module->get_formatted_name();
930 // Display the hidden text if necessary.
931 if (!$module->visible) {
932 $modname .= ' ' . get_string('hiddenwithbrackets');
934 // Module URL.
935 $linkurl = new moodle_url($module->url, array('forceview' => 1));
936 // Add module URL (as key) and name (as value) to the activity list array.
937 $activitylist[$linkurl->out(false)] = $modname;
940 $nummods = count($mods);
942 // If there is only one mod then do nothing.
943 if ($nummods == 1) {
944 return '';
947 // Get an array of just the course module ids used to get the cmid value based on their position in the course.
948 $modids = array_keys($mods);
950 // Get the position in the array of the course module we are viewing.
951 $position = array_search($this->page->cm->id, $modids);
953 $prevmod = null;
954 $nextmod = null;
956 // Check if we have a previous mod to show.
957 if ($position > 0) {
958 $prevmod = $mods[$modids[$position - 1]];
961 // Check if we have a next mod to show.
962 if ($position < ($nummods - 1)) {
963 $nextmod = $mods[$modids[$position + 1]];
966 $activitynav = new \core_course\output\activity_navigation($prevmod, $nextmod, $activitylist);
967 $renderer = $this->page->get_renderer('core', 'course');
968 return $renderer->render($activitynav);
972 * The standard tags (typically script tags that are not needed earlier) that
973 * should be output after everything else. Designed to be called in theme layout.php files.
975 * @return string HTML fragment.
977 public function standard_end_of_body_html() {
978 global $CFG;
980 // This function is normally called from a layout.php file in {@link core_renderer::header()}
981 // but some of the content won't be known until later, so we return a placeholder
982 // for now. This will be replaced with the real content in {@link core_renderer::footer()}.
983 $output = '';
984 if ($this->page->pagelayout !== 'embedded' && !empty($CFG->additionalhtmlfooter)) {
985 $output .= "\n".$CFG->additionalhtmlfooter;
987 $output .= $this->unique_end_html_token;
988 return $output;
992 * The standard HTML that should be output just before the <footer> tag.
993 * Designed to be called in theme layout.php files.
995 * @return string HTML fragment.
997 public function standard_after_main_region_html() {
998 global $CFG;
999 $output = '';
1000 if ($this->page->pagelayout !== 'embedded' && !empty($CFG->additionalhtmlbottomofbody)) {
1001 $output .= "\n".$CFG->additionalhtmlbottomofbody;
1004 // Give subsystems an opportunity to inject extra html content. The callback
1005 // must always return a string containing valid html.
1006 foreach (\core_component::get_core_subsystems() as $name => $path) {
1007 if ($path) {
1008 $output .= component_callback($name, 'standard_after_main_region_html', [], '');
1012 // Give plugins an opportunity to inject extra html content. The callback
1013 // must always return a string containing valid html.
1014 $pluginswithfunction = get_plugins_with_function('standard_after_main_region_html', 'lib.php');
1015 foreach ($pluginswithfunction as $plugins) {
1016 foreach ($plugins as $function) {
1017 $output .= $function();
1021 return $output;
1025 * Return the standard string that says whether you are logged in (and switched
1026 * roles/logged in as another user).
1027 * @param bool $withlinks if false, then don't include any links in the HTML produced.
1028 * If not set, the default is the nologinlinks option from the theme config.php file,
1029 * and if that is not set, then links are included.
1030 * @return string HTML fragment.
1032 public function login_info($withlinks = null) {
1033 global $USER, $CFG, $DB, $SESSION;
1035 if (during_initial_install()) {
1036 return '';
1039 if (is_null($withlinks)) {
1040 $withlinks = empty($this->page->layout_options['nologinlinks']);
1043 $course = $this->page->course;
1044 if (\core\session\manager::is_loggedinas()) {
1045 $realuser = \core\session\manager::get_realuser();
1046 $fullname = fullname($realuser, true);
1047 if ($withlinks) {
1048 $loginastitle = get_string('loginas');
1049 $realuserinfo = " [<a href=\"$CFG->wwwroot/course/loginas.php?id=$course->id&amp;sesskey=".sesskey()."\"";
1050 $realuserinfo .= "title =\"".$loginastitle."\">$fullname</a>] ";
1051 } else {
1052 $realuserinfo = " [$fullname] ";
1054 } else {
1055 $realuserinfo = '';
1058 $loginpage = $this->is_login_page();
1059 $loginurl = get_login_url();
1061 if (empty($course->id)) {
1062 // $course->id is not defined during installation
1063 return '';
1064 } else if (isloggedin()) {
1065 $context = context_course::instance($course->id);
1067 $fullname = fullname($USER, true);
1068 // Since Moodle 2.0 this link always goes to the public profile page (not the course profile page)
1069 if ($withlinks) {
1070 $linktitle = get_string('viewprofile');
1071 $username = "<a href=\"$CFG->wwwroot/user/profile.php?id=$USER->id\" title=\"$linktitle\">$fullname</a>";
1072 } else {
1073 $username = $fullname;
1075 if (is_mnet_remote_user($USER) and $idprovider = $DB->get_record('mnet_host', array('id'=>$USER->mnethostid))) {
1076 if ($withlinks) {
1077 $username .= " from <a href=\"{$idprovider->wwwroot}\">{$idprovider->name}</a>";
1078 } else {
1079 $username .= " from {$idprovider->name}";
1082 if (isguestuser()) {
1083 $loggedinas = $realuserinfo.get_string('loggedinasguest');
1084 if (!$loginpage && $withlinks) {
1085 $loggedinas .= " (<a href=\"$loginurl\">".get_string('login').'</a>)';
1087 } else if (is_role_switched($course->id)) { // Has switched roles
1088 $rolename = '';
1089 if ($role = $DB->get_record('role', array('id'=>$USER->access['rsw'][$context->path]))) {
1090 $rolename = ': '.role_get_name($role, $context);
1092 $loggedinas = get_string('loggedinas', 'moodle', $username).$rolename;
1093 if ($withlinks) {
1094 $url = new moodle_url('/course/switchrole.php', array('id'=>$course->id,'sesskey'=>sesskey(), 'switchrole'=>0, 'returnurl'=>$this->page->url->out_as_local_url(false)));
1095 $loggedinas .= ' ('.html_writer::tag('a', get_string('switchrolereturn'), array('href' => $url)).')';
1097 } else {
1098 $loggedinas = $realuserinfo.get_string('loggedinas', 'moodle', $username);
1099 if ($withlinks) {
1100 $loggedinas .= " (<a href=\"$CFG->wwwroot/login/logout.php?sesskey=".sesskey()."\">".get_string('logout').'</a>)';
1103 } else {
1104 $loggedinas = get_string('loggedinnot', 'moodle');
1105 if (!$loginpage && $withlinks) {
1106 $loggedinas .= " (<a href=\"$loginurl\">".get_string('login').'</a>)';
1110 $loggedinas = '<div class="logininfo">'.$loggedinas.'</div>';
1112 if (isset($SESSION->justloggedin)) {
1113 unset($SESSION->justloggedin);
1114 if (!empty($CFG->displayloginfailures)) {
1115 if (!isguestuser()) {
1116 // Include this file only when required.
1117 require_once($CFG->dirroot . '/user/lib.php');
1118 if ($count = user_count_login_failures($USER)) {
1119 $loggedinas .= '<div class="loginfailures">';
1120 $a = new stdClass();
1121 $a->attempts = $count;
1122 $loggedinas .= get_string('failedloginattempts', '', $a);
1123 if (file_exists("$CFG->dirroot/report/log/index.php") and has_capability('report/log:view', context_system::instance())) {
1124 $loggedinas .= ' ('.html_writer::link(new moodle_url('/report/log/index.php', array('chooselog' => 1,
1125 'id' => 0 , 'modid' => 'site_errors')), get_string('logs')).')';
1127 $loggedinas .= '</div>';
1133 return $loggedinas;
1137 * Check whether the current page is a login page.
1139 * @since Moodle 2.9
1140 * @return bool
1142 protected function is_login_page() {
1143 // This is a real bit of a hack, but its a rarety that we need to do something like this.
1144 // In fact the login pages should be only these two pages and as exposing this as an option for all pages
1145 // could lead to abuse (or at least unneedingly complex code) the hack is the way to go.
1146 return in_array(
1147 $this->page->url->out_as_local_url(false, array()),
1148 array(
1149 '/login/index.php',
1150 '/login/forgot_password.php',
1156 * Return the 'back' link that normally appears in the footer.
1158 * @return string HTML fragment.
1160 public function home_link() {
1161 global $CFG, $SITE;
1163 if ($this->page->pagetype == 'site-index') {
1164 // Special case for site home page - please do not remove
1165 return '<div class="sitelink">' .
1166 '<a title="Moodle" href="http://moodle.org/">' .
1167 '<img src="' . $this->image_url('moodlelogo') . '" alt="'.get_string('moodlelogo').'" /></a></div>';
1169 } else if (!empty($CFG->target_release) && $CFG->target_release != $CFG->release) {
1170 // Special case for during install/upgrade.
1171 return '<div class="sitelink">'.
1172 '<a title="Moodle" href="http://docs.moodle.org/en/Administrator_documentation" onclick="this.target=\'_blank\'">' .
1173 '<img src="' . $this->image_url('moodlelogo') . '" alt="'.get_string('moodlelogo').'" /></a></div>';
1175 } else if ($this->page->course->id == $SITE->id || strpos($this->page->pagetype, 'course-view') === 0) {
1176 return '<div class="homelink"><a href="' . $CFG->wwwroot . '/">' .
1177 get_string('home') . '</a></div>';
1179 } else {
1180 return '<div class="homelink"><a href="' . $CFG->wwwroot . '/course/view.php?id=' . $this->page->course->id . '">' .
1181 format_string($this->page->course->shortname, true, array('context' => $this->page->context)) . '</a></div>';
1186 * Redirects the user by any means possible given the current state
1188 * This function should not be called directly, it should always be called using
1189 * the redirect function in lib/weblib.php
1191 * The redirect function should really only be called before page output has started
1192 * however it will allow itself to be called during the state STATE_IN_BODY
1194 * @param string $encodedurl The URL to send to encoded if required
1195 * @param string $message The message to display to the user if any
1196 * @param int $delay The delay before redirecting a user, if $message has been
1197 * set this is a requirement and defaults to 3, set to 0 no delay
1198 * @param boolean $debugdisableredirect this redirect has been disabled for
1199 * debugging purposes. Display a message that explains, and don't
1200 * trigger the redirect.
1201 * @param string $messagetype The type of notification to show the message in.
1202 * See constants on \core\output\notification.
1203 * @return string The HTML to display to the user before dying, may contain
1204 * meta refresh, javascript refresh, and may have set header redirects
1206 public function redirect_message($encodedurl, $message, $delay, $debugdisableredirect,
1207 $messagetype = \core\output\notification::NOTIFY_INFO) {
1208 global $CFG;
1209 $url = str_replace('&amp;', '&', $encodedurl);
1211 switch ($this->page->state) {
1212 case moodle_page::STATE_BEFORE_HEADER :
1213 // No output yet it is safe to delivery the full arsenal of redirect methods
1214 if (!$debugdisableredirect) {
1215 // Don't use exactly the same time here, it can cause problems when both redirects fire at the same time.
1216 $this->metarefreshtag = '<meta http-equiv="refresh" content="'. $delay .'; url='. $encodedurl .'" />'."\n";
1217 $this->page->requires->js_function_call('document.location.replace', array($url), false, ($delay + 3));
1219 $output = $this->header();
1220 break;
1221 case moodle_page::STATE_PRINTING_HEADER :
1222 // We should hopefully never get here
1223 throw new coding_exception('You cannot redirect while printing the page header');
1224 break;
1225 case moodle_page::STATE_IN_BODY :
1226 // We really shouldn't be here but we can deal with this
1227 debugging("You should really redirect before you start page output");
1228 if (!$debugdisableredirect) {
1229 $this->page->requires->js_function_call('document.location.replace', array($url), false, $delay);
1231 $output = $this->opencontainers->pop_all_but_last();
1232 break;
1233 case moodle_page::STATE_DONE :
1234 // Too late to be calling redirect now
1235 throw new coding_exception('You cannot redirect after the entire page has been generated');
1236 break;
1238 $output .= $this->notification($message, $messagetype);
1239 $output .= '<div class="continuebutton">(<a href="'. $encodedurl .'">'. get_string('continue') .'</a>)</div>';
1240 if ($debugdisableredirect) {
1241 $output .= '<p><strong>'.get_string('erroroutput', 'error').'</strong></p>';
1243 $output .= $this->footer();
1244 return $output;
1248 * Start output by sending the HTTP headers, and printing the HTML <head>
1249 * and the start of the <body>.
1251 * To control what is printed, you should set properties on $PAGE. If you
1252 * are familiar with the old {@link print_header()} function from Moodle 1.9
1253 * you will find that there are properties on $PAGE that correspond to most
1254 * of the old parameters to could be passed to print_header.
1256 * Not that, in due course, the remaining $navigation, $menu parameters here
1257 * will be replaced by more properties of $PAGE, but that is still to do.
1259 * @return string HTML that you must output this, preferably immediately.
1261 public function header() {
1262 global $USER, $CFG, $SESSION;
1264 // Give plugins an opportunity touch things before the http headers are sent
1265 // such as adding additional headers. The return value is ignored.
1266 $pluginswithfunction = get_plugins_with_function('before_http_headers', 'lib.php');
1267 foreach ($pluginswithfunction as $plugins) {
1268 foreach ($plugins as $function) {
1269 $function();
1273 if (\core\session\manager::is_loggedinas()) {
1274 $this->page->add_body_class('userloggedinas');
1277 if (isset($SESSION->justloggedin) && !empty($CFG->displayloginfailures)) {
1278 require_once($CFG->dirroot . '/user/lib.php');
1279 // Set second parameter to false as we do not want reset the counter, the same message appears on footer.
1280 if ($count = user_count_login_failures($USER, false)) {
1281 $this->page->add_body_class('loginfailures');
1285 // If the user is logged in, and we're not in initial install,
1286 // check to see if the user is role-switched and add the appropriate
1287 // CSS class to the body element.
1288 if (!during_initial_install() && isloggedin() && is_role_switched($this->page->course->id)) {
1289 $this->page->add_body_class('userswitchedrole');
1292 // Give themes a chance to init/alter the page object.
1293 $this->page->theme->init_page($this->page);
1295 $this->page->set_state(moodle_page::STATE_PRINTING_HEADER);
1297 // Find the appropriate page layout file, based on $this->page->pagelayout.
1298 $layoutfile = $this->page->theme->layout_file($this->page->pagelayout);
1299 // Render the layout using the layout file.
1300 $rendered = $this->render_page_layout($layoutfile);
1302 // Slice the rendered output into header and footer.
1303 $cutpos = strpos($rendered, $this->unique_main_content_token);
1304 if ($cutpos === false) {
1305 $cutpos = strpos($rendered, self::MAIN_CONTENT_TOKEN);
1306 $token = self::MAIN_CONTENT_TOKEN;
1307 } else {
1308 $token = $this->unique_main_content_token;
1311 if ($cutpos === false) {
1312 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.');
1314 $header = substr($rendered, 0, $cutpos);
1315 $footer = substr($rendered, $cutpos + strlen($token));
1317 if (empty($this->contenttype)) {
1318 debugging('The page layout file did not call $OUTPUT->doctype()');
1319 $header = $this->doctype() . $header;
1322 // If this theme version is below 2.4 release and this is a course view page
1323 if ((!isset($this->page->theme->settings->version) || $this->page->theme->settings->version < 2012101500) &&
1324 $this->page->pagelayout === 'course' && $this->page->url->compare(new moodle_url('/course/view.php'), URL_MATCH_BASE)) {
1325 // check if course content header/footer have not been output during render of theme layout
1326 $coursecontentheader = $this->course_content_header(true);
1327 $coursecontentfooter = $this->course_content_footer(true);
1328 if (!empty($coursecontentheader)) {
1329 // display debug message and add header and footer right above and below main content
1330 // Please note that course header and footer (to be displayed above and below the whole page)
1331 // are not displayed in this case at all.
1332 // Besides the content header and footer are not displayed on any other course page
1333 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);
1334 $header .= $coursecontentheader;
1335 $footer = $coursecontentfooter. $footer;
1339 send_headers($this->contenttype, $this->page->cacheable);
1341 $this->opencontainers->push('header/footer', $footer);
1342 $this->page->set_state(moodle_page::STATE_IN_BODY);
1344 return $header . $this->skip_link_target('maincontent');
1348 * Renders and outputs the page layout file.
1350 * This is done by preparing the normal globals available to a script, and
1351 * then including the layout file provided by the current theme for the
1352 * requested layout.
1354 * @param string $layoutfile The name of the layout file
1355 * @return string HTML code
1357 protected function render_page_layout($layoutfile) {
1358 global $CFG, $SITE, $USER;
1359 // The next lines are a bit tricky. The point is, here we are in a method
1360 // of a renderer class, and this object may, or may not, be the same as
1361 // the global $OUTPUT object. When rendering the page layout file, we want to use
1362 // this object. However, people writing Moodle code expect the current
1363 // renderer to be called $OUTPUT, not $this, so define a variable called
1364 // $OUTPUT pointing at $this. The same comment applies to $PAGE and $COURSE.
1365 $OUTPUT = $this;
1366 $PAGE = $this->page;
1367 $COURSE = $this->page->course;
1369 ob_start();
1370 include($layoutfile);
1371 $rendered = ob_get_contents();
1372 ob_end_clean();
1373 return $rendered;
1377 * Outputs the page's footer
1379 * @return string HTML fragment
1381 public function footer() {
1382 global $CFG, $DB, $PAGE;
1384 // Give plugins an opportunity to touch the page before JS is finalized.
1385 $pluginswithfunction = get_plugins_with_function('before_footer', 'lib.php');
1386 foreach ($pluginswithfunction as $plugins) {
1387 foreach ($plugins as $function) {
1388 $function();
1392 $output = $this->container_end_all(true);
1394 $footer = $this->opencontainers->pop('header/footer');
1396 if (debugging() and $DB and $DB->is_transaction_started()) {
1397 // TODO: MDL-20625 print warning - transaction will be rolled back
1400 // Provide some performance info if required
1401 $performanceinfo = '';
1402 if (defined('MDL_PERF') || (!empty($CFG->perfdebug) and $CFG->perfdebug > 7)) {
1403 $perf = get_performance_info();
1404 if (defined('MDL_PERFTOFOOT') || debugging() || $CFG->perfdebug > 7) {
1405 $performanceinfo = $perf['html'];
1409 // We always want performance data when running a performance test, even if the user is redirected to another page.
1410 if (MDL_PERF_TEST && strpos($footer, $this->unique_performance_info_token) === false) {
1411 $footer = $this->unique_performance_info_token . $footer;
1413 $footer = str_replace($this->unique_performance_info_token, $performanceinfo, $footer);
1415 // Only show notifications when we have a $PAGE context id.
1416 if (!empty($PAGE->context->id)) {
1417 $this->page->requires->js_call_amd('core/notification', 'init', array(
1418 $PAGE->context->id,
1419 \core\notification::fetch_as_array($this)
1422 $footer = str_replace($this->unique_end_html_token, $this->page->requires->get_end_code(), $footer);
1424 $this->page->set_state(moodle_page::STATE_DONE);
1426 return $output . $footer;
1430 * Close all but the last open container. This is useful in places like error
1431 * handling, where you want to close all the open containers (apart from <body>)
1432 * before outputting the error message.
1434 * @param bool $shouldbenone assert that the stack should be empty now - causes a
1435 * developer debug warning if it isn't.
1436 * @return string the HTML required to close any open containers inside <body>.
1438 public function container_end_all($shouldbenone = false) {
1439 return $this->opencontainers->pop_all_but_last($shouldbenone);
1443 * Returns course-specific information to be output immediately above content on any course page
1444 * (for the current course)
1446 * @param bool $onlyifnotcalledbefore output content only if it has not been output before
1447 * @return string
1449 public function course_content_header($onlyifnotcalledbefore = false) {
1450 global $CFG;
1451 static $functioncalled = false;
1452 if ($functioncalled && $onlyifnotcalledbefore) {
1453 // we have already output the content header
1454 return '';
1457 // Output any session notification.
1458 $notifications = \core\notification::fetch();
1460 $bodynotifications = '';
1461 foreach ($notifications as $notification) {
1462 $bodynotifications .= $this->render_from_template(
1463 $notification->get_template_name(),
1464 $notification->export_for_template($this)
1468 $output = html_writer::span($bodynotifications, 'notifications', array('id' => 'user-notifications'));
1470 if ($this->page->course->id == SITEID) {
1471 // return immediately and do not include /course/lib.php if not necessary
1472 return $output;
1475 require_once($CFG->dirroot.'/course/lib.php');
1476 $functioncalled = true;
1477 $courseformat = course_get_format($this->page->course);
1478 if (($obj = $courseformat->course_content_header()) !== null) {
1479 $output .= html_writer::div($courseformat->get_renderer($this->page)->render($obj), 'course-content-header');
1481 return $output;
1485 * Returns course-specific information to be output immediately below content on any course page
1486 * (for the current course)
1488 * @param bool $onlyifnotcalledbefore output content only if it has not been output before
1489 * @return string
1491 public function course_content_footer($onlyifnotcalledbefore = false) {
1492 global $CFG;
1493 if ($this->page->course->id == SITEID) {
1494 // return immediately and do not include /course/lib.php if not necessary
1495 return '';
1497 static $functioncalled = false;
1498 if ($functioncalled && $onlyifnotcalledbefore) {
1499 // we have already output the content footer
1500 return '';
1502 $functioncalled = true;
1503 require_once($CFG->dirroot.'/course/lib.php');
1504 $courseformat = course_get_format($this->page->course);
1505 if (($obj = $courseformat->course_content_footer()) !== null) {
1506 return html_writer::div($courseformat->get_renderer($this->page)->render($obj), 'course-content-footer');
1508 return '';
1512 * Returns course-specific information to be output on any course page in the header area
1513 * (for the current course)
1515 * @return string
1517 public function course_header() {
1518 global $CFG;
1519 if ($this->page->course->id == SITEID) {
1520 // return immediately and do not include /course/lib.php if not necessary
1521 return '';
1523 require_once($CFG->dirroot.'/course/lib.php');
1524 $courseformat = course_get_format($this->page->course);
1525 if (($obj = $courseformat->course_header()) !== null) {
1526 return $courseformat->get_renderer($this->page)->render($obj);
1528 return '';
1532 * Returns course-specific information to be output on any course page in the footer area
1533 * (for the current course)
1535 * @return string
1537 public function course_footer() {
1538 global $CFG;
1539 if ($this->page->course->id == SITEID) {
1540 // return immediately and do not include /course/lib.php if not necessary
1541 return '';
1543 require_once($CFG->dirroot.'/course/lib.php');
1544 $courseformat = course_get_format($this->page->course);
1545 if (($obj = $courseformat->course_footer()) !== null) {
1546 return $courseformat->get_renderer($this->page)->render($obj);
1548 return '';
1552 * Returns lang menu or '', this method also checks forcing of languages in courses.
1554 * This function calls {@link core_renderer::render_single_select()} to actually display the language menu.
1556 * @return string The lang menu HTML or empty string
1558 public function lang_menu() {
1559 global $CFG;
1561 if (empty($CFG->langmenu)) {
1562 return '';
1565 if ($this->page->course != SITEID and !empty($this->page->course->lang)) {
1566 // do not show lang menu if language forced
1567 return '';
1570 $currlang = current_language();
1571 $langs = get_string_manager()->get_list_of_translations();
1573 if (count($langs) < 2) {
1574 return '';
1577 $s = new single_select($this->page->url, 'lang', $langs, $currlang, null);
1578 $s->label = get_accesshide(get_string('language'));
1579 $s->class = 'langmenu';
1580 return $this->render($s);
1584 * Output the row of editing icons for a block, as defined by the controls array.
1586 * @param array $controls an array like {@link block_contents::$controls}.
1587 * @param string $blockid The ID given to the block.
1588 * @return string HTML fragment.
1590 public function block_controls($actions, $blockid = null) {
1591 global $CFG;
1592 if (empty($actions)) {
1593 return '';
1595 $menu = new action_menu($actions);
1596 if ($blockid !== null) {
1597 $menu->set_owner_selector('#'.$blockid);
1599 $menu->set_constraint('.block-region');
1600 $menu->attributes['class'] .= ' block-control-actions commands';
1601 return $this->render($menu);
1605 * Returns the HTML for a basic textarea field.
1607 * @param string $name Name to use for the textarea element
1608 * @param string $id The id to use fort he textarea element
1609 * @param string $value Initial content to display in the textarea
1610 * @param int $rows Number of rows to display
1611 * @param int $cols Number of columns to display
1612 * @return string the HTML to display
1614 public function print_textarea($name, $id, $value, $rows, $cols) {
1615 global $OUTPUT;
1617 editors_head_setup();
1618 $editor = editors_get_preferred_editor(FORMAT_HTML);
1619 $editor->set_text($value);
1620 $editor->use_editor($id, []);
1622 $context = [
1623 'id' => $id,
1624 'name' => $name,
1625 'value' => $value,
1626 'rows' => $rows,
1627 'cols' => $cols
1630 return $OUTPUT->render_from_template('core_form/editor_textarea', $context);
1634 * Renders an action menu component.
1636 * @param action_menu $menu
1637 * @return string HTML
1639 public function render_action_menu(action_menu $menu) {
1641 // We don't want the class icon there!
1642 foreach ($menu->get_secondary_actions() as $action) {
1643 if ($action instanceof \action_menu_link && $action->has_class('icon')) {
1644 $action->attributes['class'] = preg_replace('/(^|\s+)icon(\s+|$)/i', '', $action->attributes['class']);
1648 if ($menu->is_empty()) {
1649 return '';
1651 $context = $menu->export_for_template($this);
1653 return $this->render_from_template('core/action_menu', $context);
1657 * Renders an action_menu_link item.
1659 * @param action_menu_link $action
1660 * @return string HTML fragment
1662 protected function render_action_menu_link(action_menu_link $action) {
1663 return $this->render_from_template('core/action_menu_link', $action->export_for_template($this));
1667 * Renders a primary action_menu_filler item.
1669 * @param action_menu_link_filler $action
1670 * @return string HTML fragment
1672 protected function render_action_menu_filler(action_menu_filler $action) {
1673 return html_writer::span('&nbsp;', 'filler');
1677 * Renders a primary action_menu_link item.
1679 * @param action_menu_link_primary $action
1680 * @return string HTML fragment
1682 protected function render_action_menu_link_primary(action_menu_link_primary $action) {
1683 return $this->render_action_menu_link($action);
1687 * Renders a secondary action_menu_link item.
1689 * @param action_menu_link_secondary $action
1690 * @return string HTML fragment
1692 protected function render_action_menu_link_secondary(action_menu_link_secondary $action) {
1693 return $this->render_action_menu_link($action);
1697 * Prints a nice side block with an optional header.
1699 * @param block_contents $bc HTML for the content
1700 * @param string $region the region the block is appearing in.
1701 * @return string the HTML to be output.
1703 public function block(block_contents $bc, $region) {
1704 $bc = clone($bc); // Avoid messing up the object passed in.
1705 if (empty($bc->blockinstanceid) || !strip_tags($bc->title)) {
1706 $bc->collapsible = block_contents::NOT_HIDEABLE;
1709 $id = !empty($bc->attributes['id']) ? $bc->attributes['id'] : uniqid('block-');
1710 $context = new stdClass();
1711 $context->skipid = $bc->skipid;
1712 $context->blockinstanceid = $bc->blockinstanceid;
1713 $context->dockable = $bc->dockable;
1714 $context->id = $id;
1715 $context->hidden = $bc->collapsible == block_contents::HIDDEN;
1716 $context->skiptitle = strip_tags($bc->title);
1717 $context->showskiplink = !empty($context->skiptitle);
1718 $context->arialabel = $bc->arialabel;
1719 $context->ariarole = !empty($bc->attributes['role']) ? $bc->attributes['role'] : 'complementary';
1720 $context->type = $bc->attributes['data-block'];
1721 $context->title = $bc->title;
1722 $context->content = $bc->content;
1723 $context->annotation = $bc->annotation;
1724 $context->footer = $bc->footer;
1725 $context->hascontrols = !empty($bc->controls);
1726 if ($context->hascontrols) {
1727 $context->controls = $this->block_controls($bc->controls, $id);
1730 return $this->render_from_template('core/block', $context);
1734 * Render the contents of a block_list.
1736 * @param array $icons the icon for each item.
1737 * @param array $items the content of each item.
1738 * @return string HTML
1740 public function list_block_contents($icons, $items) {
1741 $row = 0;
1742 $lis = array();
1743 foreach ($items as $key => $string) {
1744 $item = html_writer::start_tag('li', array('class' => 'r' . $row));
1745 if (!empty($icons[$key])) { //test if the content has an assigned icon
1746 $item .= html_writer::tag('div', $icons[$key], array('class' => 'icon column c0'));
1748 $item .= html_writer::tag('div', $string, array('class' => 'column c1'));
1749 $item .= html_writer::end_tag('li');
1750 $lis[] = $item;
1751 $row = 1 - $row; // Flip even/odd.
1753 return html_writer::tag('ul', implode("\n", $lis), array('class' => 'unlist'));
1757 * Output all the blocks in a particular region.
1759 * @param string $region the name of a region on this page.
1760 * @return string the HTML to be output.
1762 public function blocks_for_region($region) {
1763 $blockcontents = $this->page->blocks->get_content_for_region($region, $this);
1764 $blocks = $this->page->blocks->get_blocks_for_region($region);
1765 $lastblock = null;
1766 $zones = array();
1767 foreach ($blocks as $block) {
1768 $zones[] = $block->title;
1770 $output = '';
1772 foreach ($blockcontents as $bc) {
1773 if ($bc instanceof block_contents) {
1774 $output .= $this->block($bc, $region);
1775 $lastblock = $bc->title;
1776 } else if ($bc instanceof block_move_target) {
1777 $output .= $this->block_move_target($bc, $zones, $lastblock, $region);
1778 } else {
1779 throw new coding_exception('Unexpected type of thing (' . get_class($bc) . ') found in list of block contents.');
1782 return $output;
1786 * Output a place where the block that is currently being moved can be dropped.
1788 * @param block_move_target $target with the necessary details.
1789 * @param array $zones array of areas where the block can be moved to
1790 * @param string $previous the block located before the area currently being rendered.
1791 * @param string $region the name of the region
1792 * @return string the HTML to be output.
1794 public function block_move_target($target, $zones, $previous, $region) {
1795 if ($previous == null) {
1796 if (empty($zones)) {
1797 // There are no zones, probably because there are no blocks.
1798 $regions = $this->page->theme->get_all_block_regions();
1799 $position = get_string('moveblockinregion', 'block', $regions[$region]);
1800 } else {
1801 $position = get_string('moveblockbefore', 'block', $zones[0]);
1803 } else {
1804 $position = get_string('moveblockafter', 'block', $previous);
1806 return html_writer::tag('a', html_writer::tag('span', $position, array('class' => 'accesshide')), array('href' => $target->url, 'class' => 'blockmovetarget'));
1810 * Renders a special html link with attached action
1812 * Theme developers: DO NOT OVERRIDE! Please override function
1813 * {@link core_renderer::render_action_link()} instead.
1815 * @param string|moodle_url $url
1816 * @param string $text HTML fragment
1817 * @param component_action $action
1818 * @param array $attributes associative array of html link attributes + disabled
1819 * @param pix_icon optional pix icon to render with the link
1820 * @return string HTML fragment
1822 public function action_link($url, $text, component_action $action = null, array $attributes = null, $icon = null) {
1823 if (!($url instanceof moodle_url)) {
1824 $url = new moodle_url($url);
1826 $link = new action_link($url, $text, $action, $attributes, $icon);
1828 return $this->render($link);
1832 * Renders an action_link object.
1834 * The provided link is renderer and the HTML returned. At the same time the
1835 * associated actions are setup in JS by {@link core_renderer::add_action_handler()}
1837 * @param action_link $link
1838 * @return string HTML fragment
1840 protected function render_action_link(action_link $link) {
1841 return $this->render_from_template('core/action_link', $link->export_for_template($this));
1845 * Renders an action_icon.
1847 * This function uses the {@link core_renderer::action_link()} method for the
1848 * most part. What it does different is prepare the icon as HTML and use it
1849 * as the link text.
1851 * Theme developers: If you want to change how action links and/or icons are rendered,
1852 * consider overriding function {@link core_renderer::render_action_link()} and
1853 * {@link core_renderer::render_pix_icon()}.
1855 * @param string|moodle_url $url A string URL or moodel_url
1856 * @param pix_icon $pixicon
1857 * @param component_action $action
1858 * @param array $attributes associative array of html link attributes + disabled
1859 * @param bool $linktext show title next to image in link
1860 * @return string HTML fragment
1862 public function action_icon($url, pix_icon $pixicon, component_action $action = null, array $attributes = null, $linktext=false) {
1863 if (!($url instanceof moodle_url)) {
1864 $url = new moodle_url($url);
1866 $attributes = (array)$attributes;
1868 if (empty($attributes['class'])) {
1869 // let ppl override the class via $options
1870 $attributes['class'] = 'action-icon';
1873 $icon = $this->render($pixicon);
1875 if ($linktext) {
1876 $text = $pixicon->attributes['alt'];
1877 } else {
1878 $text = '';
1881 return $this->action_link($url, $text.$icon, $action, $attributes);
1885 * Print a message along with button choices for Continue/Cancel
1887 * If a string or moodle_url is given instead of a single_button, method defaults to post.
1889 * @param string $message The question to ask the user
1890 * @param single_button|moodle_url|string $continue The single_button component representing the Continue answer. Can also be a moodle_url or string URL
1891 * @param single_button|moodle_url|string $cancel The single_button component representing the Cancel answer. Can also be a moodle_url or string URL
1892 * @return string HTML fragment
1894 public function confirm($message, $continue, $cancel) {
1895 if ($continue instanceof single_button) {
1896 // ok
1897 $continue->primary = true;
1898 } else if (is_string($continue)) {
1899 $continue = new single_button(new moodle_url($continue), get_string('continue'), 'post', true);
1900 } else if ($continue instanceof moodle_url) {
1901 $continue = new single_button($continue, get_string('continue'), 'post', true);
1902 } else {
1903 throw new coding_exception('The continue param to $OUTPUT->confirm() must be either a URL (string/moodle_url) or a single_button instance.');
1906 if ($cancel instanceof single_button) {
1907 // ok
1908 } else if (is_string($cancel)) {
1909 $cancel = new single_button(new moodle_url($cancel), get_string('cancel'), 'get');
1910 } else if ($cancel instanceof moodle_url) {
1911 $cancel = new single_button($cancel, get_string('cancel'), 'get');
1912 } else {
1913 throw new coding_exception('The cancel param to $OUTPUT->confirm() must be either a URL (string/moodle_url) or a single_button instance.');
1916 $attributes = [
1917 'role'=>'alertdialog',
1918 'aria-labelledby'=>'modal-header',
1919 'aria-describedby'=>'modal-body',
1920 'aria-modal'=>'true'
1923 $output = $this->box_start('generalbox modal modal-dialog modal-in-page show', 'notice', $attributes);
1924 $output .= $this->box_start('modal-content', 'modal-content');
1925 $output .= $this->box_start('modal-header p-x-1', 'modal-header');
1926 $output .= html_writer::tag('h4', get_string('confirm'));
1927 $output .= $this->box_end();
1928 $attributes = [
1929 'role'=>'alert',
1930 'data-aria-autofocus'=>'true'
1932 $output .= $this->box_start('modal-body', 'modal-body', $attributes);
1933 $output .= html_writer::tag('p', $message);
1934 $output .= $this->box_end();
1935 $output .= $this->box_start('modal-footer', 'modal-footer');
1936 $output .= html_writer::tag('div', $this->render($continue) . $this->render($cancel), array('class' => 'buttons'));
1937 $output .= $this->box_end();
1938 $output .= $this->box_end();
1939 $output .= $this->box_end();
1940 return $output;
1944 * Returns a form with a single button.
1946 * Theme developers: DO NOT OVERRIDE! Please override function
1947 * {@link core_renderer::render_single_button()} instead.
1949 * @param string|moodle_url $url
1950 * @param string $label button text
1951 * @param string $method get or post submit method
1952 * @param array $options associative array {disabled, title, etc.}
1953 * @return string HTML fragment
1955 public function single_button($url, $label, $method='post', array $options=null) {
1956 if (!($url instanceof moodle_url)) {
1957 $url = new moodle_url($url);
1959 $button = new single_button($url, $label, $method);
1961 foreach ((array)$options as $key=>$value) {
1962 if (array_key_exists($key, $button)) {
1963 $button->$key = $value;
1967 return $this->render($button);
1971 * Renders a single button widget.
1973 * This will return HTML to display a form containing a single button.
1975 * @param single_button $button
1976 * @return string HTML fragment
1978 protected function render_single_button(single_button $button) {
1979 return $this->render_from_template('core/single_button', $button->export_for_template($this));
1983 * Returns a form with a single select widget.
1985 * Theme developers: DO NOT OVERRIDE! Please override function
1986 * {@link core_renderer::render_single_select()} instead.
1988 * @param moodle_url $url form action target, includes hidden fields
1989 * @param string $name name of selection field - the changing parameter in url
1990 * @param array $options list of options
1991 * @param string $selected selected element
1992 * @param array $nothing
1993 * @param string $formid
1994 * @param array $attributes other attributes for the single select
1995 * @return string HTML fragment
1997 public function single_select($url, $name, array $options, $selected = '',
1998 $nothing = array('' => 'choosedots'), $formid = null, $attributes = array()) {
1999 if (!($url instanceof moodle_url)) {
2000 $url = new moodle_url($url);
2002 $select = new single_select($url, $name, $options, $selected, $nothing, $formid);
2004 if (array_key_exists('label', $attributes)) {
2005 $select->set_label($attributes['label']);
2006 unset($attributes['label']);
2008 $select->attributes = $attributes;
2010 return $this->render($select);
2014 * Returns a dataformat selection and download form
2016 * @param string $label A text label
2017 * @param moodle_url|string $base The download page url
2018 * @param string $name The query param which will hold the type of the download
2019 * @param array $params Extra params sent to the download page
2020 * @return string HTML fragment
2022 public function download_dataformat_selector($label, $base, $name = 'dataformat', $params = array()) {
2024 $formats = core_plugin_manager::instance()->get_plugins_of_type('dataformat');
2025 $options = array();
2026 foreach ($formats as $format) {
2027 if ($format->is_enabled()) {
2028 $options[] = array(
2029 'value' => $format->name,
2030 'label' => get_string('dataformat', $format->component),
2034 $hiddenparams = array();
2035 foreach ($params as $key => $value) {
2036 $hiddenparams[] = array(
2037 'name' => $key,
2038 'value' => $value,
2041 $data = array(
2042 'label' => $label,
2043 'base' => $base,
2044 'name' => $name,
2045 'params' => $hiddenparams,
2046 'options' => $options,
2047 'sesskey' => sesskey(),
2048 'submit' => get_string('download'),
2051 return $this->render_from_template('core/dataformat_selector', $data);
2056 * Internal implementation of single_select rendering
2058 * @param single_select $select
2059 * @return string HTML fragment
2061 protected function render_single_select(single_select $select) {
2062 return $this->render_from_template('core/single_select', $select->export_for_template($this));
2066 * Returns a form with a url select widget.
2068 * Theme developers: DO NOT OVERRIDE! Please override function
2069 * {@link core_renderer::render_url_select()} instead.
2071 * @param array $urls list of urls - array('/course/view.php?id=1'=>'Frontpage', ....)
2072 * @param string $selected selected element
2073 * @param array $nothing
2074 * @param string $formid
2075 * @return string HTML fragment
2077 public function url_select(array $urls, $selected, $nothing = array('' => 'choosedots'), $formid = null) {
2078 $select = new url_select($urls, $selected, $nothing, $formid);
2079 return $this->render($select);
2083 * Internal implementation of url_select rendering
2085 * @param url_select $select
2086 * @return string HTML fragment
2088 protected function render_url_select(url_select $select) {
2089 return $this->render_from_template('core/url_select', $select->export_for_template($this));
2093 * Returns a string containing a link to the user documentation.
2094 * Also contains an icon by default. Shown to teachers and admin only.
2096 * @param string $path The page link after doc root and language, no leading slash.
2097 * @param string $text The text to be displayed for the link
2098 * @param boolean $forcepopup Whether to force a popup regardless of the value of $CFG->doctonewwindow
2099 * @return string
2101 public function doc_link($path, $text = '', $forcepopup = false) {
2102 global $CFG;
2104 $icon = $this->pix_icon('docs', '', 'moodle', array('class'=>'iconhelp icon-pre', 'role'=>'presentation'));
2106 $url = new moodle_url(get_docs_url($path));
2108 $attributes = array('href'=>$url);
2109 if (!empty($CFG->doctonewwindow) || $forcepopup) {
2110 $attributes['class'] = 'helplinkpopup';
2113 return html_writer::tag('a', $icon.$text, $attributes);
2117 * Return HTML for an image_icon.
2119 * Theme developers: DO NOT OVERRIDE! Please override function
2120 * {@link core_renderer::render_image_icon()} instead.
2122 * @param string $pix short pix name
2123 * @param string $alt mandatory alt attribute
2124 * @param string $component standard compoennt name like 'moodle', 'mod_forum', etc.
2125 * @param array $attributes htm lattributes
2126 * @return string HTML fragment
2128 public function image_icon($pix, $alt, $component='moodle', array $attributes = null) {
2129 $icon = new image_icon($pix, $alt, $component, $attributes);
2130 return $this->render($icon);
2134 * Renders a pix_icon widget and returns the HTML to display it.
2136 * @param image_icon $icon
2137 * @return string HTML fragment
2139 protected function render_image_icon(image_icon $icon) {
2140 $system = \core\output\icon_system::instance(\core\output\icon_system::STANDARD);
2141 return $system->render_pix_icon($this, $icon);
2145 * Return HTML for a pix_icon.
2147 * Theme developers: DO NOT OVERRIDE! Please override function
2148 * {@link core_renderer::render_pix_icon()} instead.
2150 * @param string $pix short pix name
2151 * @param string $alt mandatory alt attribute
2152 * @param string $component standard compoennt name like 'moodle', 'mod_forum', etc.
2153 * @param array $attributes htm lattributes
2154 * @return string HTML fragment
2156 public function pix_icon($pix, $alt, $component='moodle', array $attributes = null) {
2157 $icon = new pix_icon($pix, $alt, $component, $attributes);
2158 return $this->render($icon);
2162 * Renders a pix_icon widget and returns the HTML to display it.
2164 * @param pix_icon $icon
2165 * @return string HTML fragment
2167 protected function render_pix_icon(pix_icon $icon) {
2168 $system = \core\output\icon_system::instance();
2169 return $system->render_pix_icon($this, $icon);
2173 * Return HTML to display an emoticon icon.
2175 * @param pix_emoticon $emoticon
2176 * @return string HTML fragment
2178 protected function render_pix_emoticon(pix_emoticon $emoticon) {
2179 $system = \core\output\icon_system::instance(\core\output\icon_system::STANDARD);
2180 return $system->render_pix_icon($this, $emoticon);
2184 * Produces the html that represents this rating in the UI
2186 * @param rating $rating the page object on which this rating will appear
2187 * @return string
2189 function render_rating(rating $rating) {
2190 global $CFG, $USER;
2192 if ($rating->settings->aggregationmethod == RATING_AGGREGATE_NONE) {
2193 return null;//ratings are turned off
2196 $ratingmanager = new rating_manager();
2197 // Initialise the JavaScript so ratings can be done by AJAX.
2198 $ratingmanager->initialise_rating_javascript($this->page);
2200 $strrate = get_string("rate", "rating");
2201 $ratinghtml = ''; //the string we'll return
2203 // permissions check - can they view the aggregate?
2204 if ($rating->user_can_view_aggregate()) {
2206 $aggregatelabel = $ratingmanager->get_aggregate_label($rating->settings->aggregationmethod);
2207 $aggregatestr = $rating->get_aggregate_string();
2209 $aggregatehtml = html_writer::tag('span', $aggregatestr, array('id' => 'ratingaggregate'.$rating->itemid, 'class' => 'ratingaggregate')).' ';
2210 if ($rating->count > 0) {
2211 $countstr = "({$rating->count})";
2212 } else {
2213 $countstr = '-';
2215 $aggregatehtml .= html_writer::tag('span', $countstr, array('id'=>"ratingcount{$rating->itemid}", 'class' => 'ratingcount')).' ';
2217 $ratinghtml .= html_writer::tag('span', $aggregatelabel, array('class'=>'rating-aggregate-label'));
2218 if ($rating->settings->permissions->viewall && $rating->settings->pluginpermissions->viewall) {
2220 $nonpopuplink = $rating->get_view_ratings_url();
2221 $popuplink = $rating->get_view_ratings_url(true);
2223 $action = new popup_action('click', $popuplink, 'ratings', array('height' => 400, 'width' => 600));
2224 $ratinghtml .= $this->action_link($nonpopuplink, $aggregatehtml, $action);
2225 } else {
2226 $ratinghtml .= $aggregatehtml;
2230 $formstart = null;
2231 // if the item doesn't belong to the current user, the user has permission to rate
2232 // and we're within the assessable period
2233 if ($rating->user_can_rate()) {
2235 $rateurl = $rating->get_rate_url();
2236 $inputs = $rateurl->params();
2238 //start the rating form
2239 $formattrs = array(
2240 'id' => "postrating{$rating->itemid}",
2241 'class' => 'postratingform',
2242 'method' => 'post',
2243 'action' => $rateurl->out_omit_querystring()
2245 $formstart = html_writer::start_tag('form', $formattrs);
2246 $formstart .= html_writer::start_tag('div', array('class' => 'ratingform'));
2248 // add the hidden inputs
2249 foreach ($inputs as $name => $value) {
2250 $attributes = array('type' => 'hidden', 'class' => 'ratinginput', 'name' => $name, 'value' => $value);
2251 $formstart .= html_writer::empty_tag('input', $attributes);
2254 if (empty($ratinghtml)) {
2255 $ratinghtml .= $strrate.': ';
2257 $ratinghtml = $formstart.$ratinghtml;
2259 $scalearray = array(RATING_UNSET_RATING => $strrate.'...') + $rating->settings->scale->scaleitems;
2260 $scaleattrs = array('class'=>'postratingmenu ratinginput','id'=>'menurating'.$rating->itemid);
2261 $ratinghtml .= html_writer::label($rating->rating, 'menurating'.$rating->itemid, false, array('class' => 'accesshide'));
2262 $ratinghtml .= html_writer::select($scalearray, 'rating', $rating->rating, false, $scaleattrs);
2264 //output submit button
2265 $ratinghtml .= html_writer::start_tag('span', array('class'=>"ratingsubmit"));
2267 $attributes = array('type' => 'submit', 'class' => 'postratingmenusubmit', 'id' => 'postratingsubmit'.$rating->itemid, 'value' => s(get_string('rate', 'rating')));
2268 $ratinghtml .= html_writer::empty_tag('input', $attributes);
2270 if (!$rating->settings->scale->isnumeric) {
2271 // If a global scale, try to find current course ID from the context
2272 if (empty($rating->settings->scale->courseid) and $coursecontext = $rating->context->get_course_context(false)) {
2273 $courseid = $coursecontext->instanceid;
2274 } else {
2275 $courseid = $rating->settings->scale->courseid;
2277 $ratinghtml .= $this->help_icon_scale($courseid, $rating->settings->scale);
2279 $ratinghtml .= html_writer::end_tag('span');
2280 $ratinghtml .= html_writer::end_tag('div');
2281 $ratinghtml .= html_writer::end_tag('form');
2284 return $ratinghtml;
2288 * Centered heading with attached help button (same title text)
2289 * and optional icon attached.
2291 * @param string $text A heading text
2292 * @param string $helpidentifier The keyword that defines a help page
2293 * @param string $component component name
2294 * @param string|moodle_url $icon
2295 * @param string $iconalt icon alt text
2296 * @param int $level The level of importance of the heading. Defaulting to 2
2297 * @param string $classnames A space-separated list of CSS classes. Defaulting to null
2298 * @return string HTML fragment
2300 public function heading_with_help($text, $helpidentifier, $component = 'moodle', $icon = '', $iconalt = '', $level = 2, $classnames = null) {
2301 $image = '';
2302 if ($icon) {
2303 $image = $this->pix_icon($icon, $iconalt, $component, array('class'=>'icon iconlarge'));
2306 $help = '';
2307 if ($helpidentifier) {
2308 $help = $this->help_icon($helpidentifier, $component);
2311 return $this->heading($image.$text.$help, $level, $classnames);
2315 * Returns HTML to display a help icon.
2317 * @deprecated since Moodle 2.0
2319 public function old_help_icon($helpidentifier, $title, $component = 'moodle', $linktext = '') {
2320 throw new coding_exception('old_help_icon() can not be used any more, please see help_icon().');
2324 * Returns HTML to display a help icon.
2326 * Theme developers: DO NOT OVERRIDE! Please override function
2327 * {@link core_renderer::render_help_icon()} instead.
2329 * @param string $identifier The keyword that defines a help page
2330 * @param string $component component name
2331 * @param string|bool $linktext true means use $title as link text, string means link text value
2332 * @return string HTML fragment
2334 public function help_icon($identifier, $component = 'moodle', $linktext = '') {
2335 $icon = new help_icon($identifier, $component);
2336 $icon->diag_strings();
2337 if ($linktext === true) {
2338 $icon->linktext = get_string($icon->identifier, $icon->component);
2339 } else if (!empty($linktext)) {
2340 $icon->linktext = $linktext;
2342 return $this->render($icon);
2346 * Implementation of user image rendering.
2348 * @param help_icon $helpicon A help icon instance
2349 * @return string HTML fragment
2351 protected function render_help_icon(help_icon $helpicon) {
2352 $context = $helpicon->export_for_template($this);
2353 return $this->render_from_template('core/help_icon', $context);
2357 * Returns HTML to display a scale help icon.
2359 * @param int $courseid
2360 * @param stdClass $scale instance
2361 * @return string HTML fragment
2363 public function help_icon_scale($courseid, stdClass $scale) {
2364 global $CFG;
2366 $title = get_string('helpprefix2', '', $scale->name) .' ('.get_string('newwindow').')';
2368 $icon = $this->pix_icon('help', get_string('scales'), 'moodle', array('class'=>'iconhelp'));
2370 $scaleid = abs($scale->id);
2372 $link = new moodle_url('/course/scales.php', array('id' => $courseid, 'list' => true, 'scaleid' => $scaleid));
2373 $action = new popup_action('click', $link, 'ratingscale');
2375 return html_writer::tag('span', $this->action_link($link, $icon, $action), array('class' => 'helplink'));
2379 * Creates and returns a spacer image with optional line break.
2381 * @param array $attributes Any HTML attributes to add to the spaced.
2382 * @param bool $br Include a BR after the spacer.... DON'T USE THIS. Don't be
2383 * laxy do it with CSS which is a much better solution.
2384 * @return string HTML fragment
2386 public function spacer(array $attributes = null, $br = false) {
2387 $attributes = (array)$attributes;
2388 if (empty($attributes['width'])) {
2389 $attributes['width'] = 1;
2391 if (empty($attributes['height'])) {
2392 $attributes['height'] = 1;
2394 $attributes['class'] = 'spacer';
2396 $output = $this->pix_icon('spacer', '', 'moodle', $attributes);
2398 if (!empty($br)) {
2399 $output .= '<br />';
2402 return $output;
2406 * Returns HTML to display the specified user's avatar.
2408 * User avatar may be obtained in two ways:
2409 * <pre>
2410 * // Option 1: (shortcut for simple cases, preferred way)
2411 * // $user has come from the DB and has fields id, picture, imagealt, firstname and lastname
2412 * $OUTPUT->user_picture($user, array('popup'=>true));
2414 * // Option 2:
2415 * $userpic = new user_picture($user);
2416 * // Set properties of $userpic
2417 * $userpic->popup = true;
2418 * $OUTPUT->render($userpic);
2419 * </pre>
2421 * Theme developers: DO NOT OVERRIDE! Please override function
2422 * {@link core_renderer::render_user_picture()} instead.
2424 * @param stdClass $user Object with at least fields id, picture, imagealt, firstname, lastname
2425 * If any of these are missing, the database is queried. Avoid this
2426 * if at all possible, particularly for reports. It is very bad for performance.
2427 * @param array $options associative array with user picture options, used only if not a user_picture object,
2428 * options are:
2429 * - courseid=$this->page->course->id (course id of user profile in link)
2430 * - size=35 (size of image)
2431 * - link=true (make image clickable - the link leads to user profile)
2432 * - popup=false (open in popup)
2433 * - alttext=true (add image alt attribute)
2434 * - class = image class attribute (default 'userpicture')
2435 * - visibletoscreenreaders=true (whether to be visible to screen readers)
2436 * - includefullname=false (whether to include the user's full name together with the user picture)
2437 * - includetoken = false (whether to use a token for authentication. True for current user, int value for other user id)
2438 * @return string HTML fragment
2440 public function user_picture(stdClass $user, array $options = null) {
2441 $userpicture = new user_picture($user);
2442 foreach ((array)$options as $key=>$value) {
2443 if (array_key_exists($key, $userpicture)) {
2444 $userpicture->$key = $value;
2447 return $this->render($userpicture);
2451 * Internal implementation of user image rendering.
2453 * @param user_picture $userpicture
2454 * @return string
2456 protected function render_user_picture(user_picture $userpicture) {
2457 global $CFG, $DB;
2459 $user = $userpicture->user;
2460 $canviewfullnames = has_capability('moodle/site:viewfullnames', $this->page->context);
2462 if ($userpicture->alttext) {
2463 if (!empty($user->imagealt)) {
2464 $alt = $user->imagealt;
2465 } else {
2466 $alt = get_string('pictureof', '', fullname($user, $canviewfullnames));
2468 } else {
2469 $alt = '';
2472 if (empty($userpicture->size)) {
2473 $size = 35;
2474 } else if ($userpicture->size === true or $userpicture->size == 1) {
2475 $size = 100;
2476 } else {
2477 $size = $userpicture->size;
2480 $class = $userpicture->class;
2482 if ($user->picture == 0) {
2483 $class .= ' defaultuserpic';
2486 $src = $userpicture->get_url($this->page, $this);
2488 $attributes = array('src' => $src, 'class' => $class, 'width' => $size, 'height' => $size);
2489 if (!$userpicture->visibletoscreenreaders) {
2490 $attributes['role'] = 'presentation';
2491 $alt = '';
2492 $attributes['aria-hidden'] = 'true';
2495 if (!empty($alt)) {
2496 $attributes['alt'] = $alt;
2497 $attributes['title'] = $alt;
2500 // get the image html output fisrt
2501 $output = html_writer::empty_tag('img', $attributes);
2503 // Show fullname together with the picture when desired.
2504 if ($userpicture->includefullname) {
2505 $output .= fullname($userpicture->user, $canviewfullnames);
2508 // then wrap it in link if needed
2509 if (!$userpicture->link) {
2510 return $output;
2513 if (empty($userpicture->courseid)) {
2514 $courseid = $this->page->course->id;
2515 } else {
2516 $courseid = $userpicture->courseid;
2519 if ($courseid == SITEID) {
2520 $url = new moodle_url('/user/profile.php', array('id' => $user->id));
2521 } else {
2522 $url = new moodle_url('/user/view.php', array('id' => $user->id, 'course' => $courseid));
2525 $attributes = array('href'=>$url);
2526 if (!$userpicture->visibletoscreenreaders) {
2527 $attributes['tabindex'] = '-1';
2528 $attributes['aria-hidden'] = 'true';
2531 if ($userpicture->popup) {
2532 $id = html_writer::random_id('userpicture');
2533 $attributes['id'] = $id;
2534 $this->add_action_handler(new popup_action('click', $url), $id);
2537 return html_writer::tag('a', $output, $attributes);
2541 * Internal implementation of file tree viewer items rendering.
2543 * @param array $dir
2544 * @return string
2546 public function htmllize_file_tree($dir) {
2547 if (empty($dir['subdirs']) and empty($dir['files'])) {
2548 return '';
2550 $result = '<ul>';
2551 foreach ($dir['subdirs'] as $subdir) {
2552 $result .= '<li>'.s($subdir['dirname']).' '.$this->htmllize_file_tree($subdir).'</li>';
2554 foreach ($dir['files'] as $file) {
2555 $filename = $file->get_filename();
2556 $result .= '<li><span>'.html_writer::link($file->fileurl, $filename).'</span></li>';
2558 $result .= '</ul>';
2560 return $result;
2564 * Returns HTML to display the file picker
2566 * <pre>
2567 * $OUTPUT->file_picker($options);
2568 * </pre>
2570 * Theme developers: DO NOT OVERRIDE! Please override function
2571 * {@link core_renderer::render_file_picker()} instead.
2573 * @param array $options associative array with file manager options
2574 * options are:
2575 * maxbytes=>-1,
2576 * itemid=>0,
2577 * client_id=>uniqid(),
2578 * acepted_types=>'*',
2579 * return_types=>FILE_INTERNAL,
2580 * context=>$PAGE->context
2581 * @return string HTML fragment
2583 public function file_picker($options) {
2584 $fp = new file_picker($options);
2585 return $this->render($fp);
2589 * Internal implementation of file picker rendering.
2591 * @param file_picker $fp
2592 * @return string
2594 public function render_file_picker(file_picker $fp) {
2595 global $CFG, $OUTPUT, $USER;
2596 $options = $fp->options;
2597 $client_id = $options->client_id;
2598 $strsaved = get_string('filesaved', 'repository');
2599 $straddfile = get_string('openpicker', 'repository');
2600 $strloading = get_string('loading', 'repository');
2601 $strdndenabled = get_string('dndenabled_inbox', 'moodle');
2602 $strdroptoupload = get_string('droptoupload', 'moodle');
2603 $icon_progress = $OUTPUT->pix_icon('i/loading_small', $strloading).'';
2605 $currentfile = $options->currentfile;
2606 if (empty($currentfile)) {
2607 $currentfile = '';
2608 } else {
2609 $currentfile .= ' - ';
2611 if ($options->maxbytes) {
2612 $size = $options->maxbytes;
2613 } else {
2614 $size = get_max_upload_file_size();
2616 if ($size == -1) {
2617 $maxsize = '';
2618 } else {
2619 $maxsize = get_string('maxfilesize', 'moodle', display_size($size));
2621 if ($options->buttonname) {
2622 $buttonname = ' name="' . $options->buttonname . '"';
2623 } else {
2624 $buttonname = '';
2626 $html = <<<EOD
2627 <div class="filemanager-loading mdl-align" id='filepicker-loading-{$client_id}'>
2628 $icon_progress
2629 </div>
2630 <div id="filepicker-wrapper-{$client_id}" class="mdl-left w-100" style="display:none">
2631 <div>
2632 <input type="button" class="btn btn-secondary fp-btn-choose" id="filepicker-button-{$client_id}" value="{$straddfile}"{$buttonname}/>
2633 <span> $maxsize </span>
2634 </div>
2635 EOD;
2636 if ($options->env != 'url') {
2637 $html .= <<<EOD
2638 <div id="file_info_{$client_id}" class="mdl-left filepicker-filelist" style="position: relative">
2639 <div class="filepicker-filename">
2640 <div class="filepicker-container">$currentfile<div class="dndupload-message">$strdndenabled <br/><div class="dndupload-arrow"></div></div></div>
2641 <div class="dndupload-progressbars"></div>
2642 </div>
2643 <div><div class="dndupload-target">{$strdroptoupload}<br/><div class="dndupload-arrow"></div></div></div>
2644 </div>
2645 EOD;
2647 $html .= '</div>';
2648 return $html;
2652 * @deprecated since Moodle 3.2
2654 public function update_module_button() {
2655 throw new coding_exception('core_renderer::update_module_button() can not be used anymore. Activity ' .
2656 'modules should not add the edit module button, the link is already available in the Administration block. ' .
2657 'Themes can choose to display the link in the buttons row consistently for all module types.');
2661 * Returns HTML to display a "Turn editing on/off" button in a form.
2663 * @param moodle_url $url The URL + params to send through when clicking the button
2664 * @return string HTML the button
2666 public function edit_button(moodle_url $url) {
2668 $url->param('sesskey', sesskey());
2669 if ($this->page->user_is_editing()) {
2670 $url->param('edit', 'off');
2671 $editstring = get_string('turneditingoff');
2672 } else {
2673 $url->param('edit', 'on');
2674 $editstring = get_string('turneditingon');
2677 return $this->single_button($url, $editstring);
2681 * Returns HTML to display a simple button to close a window
2683 * @param string $text The lang string for the button's label (already output from get_string())
2684 * @return string html fragment
2686 public function close_window_button($text='') {
2687 if (empty($text)) {
2688 $text = get_string('closewindow');
2690 $button = new single_button(new moodle_url('#'), $text, 'get');
2691 $button->add_action(new component_action('click', 'close_window'));
2693 return $this->container($this->render($button), 'closewindow');
2697 * Output an error message. By default wraps the error message in <span class="error">.
2698 * If the error message is blank, nothing is output.
2700 * @param string $message the error message.
2701 * @return string the HTML to output.
2703 public function error_text($message) {
2704 if (empty($message)) {
2705 return '';
2707 $message = $this->pix_icon('i/warning', get_string('error'), '', array('class' => 'icon icon-pre', 'title'=>'')) . $message;
2708 return html_writer::tag('span', $message, array('class' => 'error'));
2712 * Do not call this function directly.
2714 * To terminate the current script with a fatal error, call the {@link print_error}
2715 * function, or throw an exception. Doing either of those things will then call this
2716 * function to display the error, before terminating the execution.
2718 * @param string $message The message to output
2719 * @param string $moreinfourl URL where more info can be found about the error
2720 * @param string $link Link for the Continue button
2721 * @param array $backtrace The execution backtrace
2722 * @param string $debuginfo Debugging information
2723 * @return string the HTML to output.
2725 public function fatal_error($message, $moreinfourl, $link, $backtrace, $debuginfo = null, $errorcode = "") {
2726 global $CFG;
2728 $output = '';
2729 $obbuffer = '';
2731 if ($this->has_started()) {
2732 // we can not always recover properly here, we have problems with output buffering,
2733 // html tables, etc.
2734 $output .= $this->opencontainers->pop_all_but_last();
2736 } else {
2737 // It is really bad if library code throws exception when output buffering is on,
2738 // because the buffered text would be printed before our start of page.
2739 // NOTE: this hack might be behave unexpectedly in case output buffering is enabled in PHP.ini
2740 error_reporting(0); // disable notices from gzip compression, etc.
2741 while (ob_get_level() > 0) {
2742 $buff = ob_get_clean();
2743 if ($buff === false) {
2744 break;
2746 $obbuffer .= $buff;
2748 error_reporting($CFG->debug);
2750 // Output not yet started.
2751 $protocol = (isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0');
2752 if (empty($_SERVER['HTTP_RANGE'])) {
2753 @header($protocol . ' 404 Not Found');
2754 } else if (core_useragent::check_safari_ios_version(602) && !empty($_SERVER['HTTP_X_PLAYBACK_SESSION_ID'])) {
2755 // Coax iOS 10 into sending the session cookie.
2756 @header($protocol . ' 403 Forbidden');
2757 } else {
2758 // Must stop byteserving attempts somehow,
2759 // this is weird but Chrome PDF viewer can be stopped only with 407!
2760 @header($protocol . ' 407 Proxy Authentication Required');
2763 $this->page->set_context(null); // ugly hack - make sure page context is set to something, we do not want bogus warnings here
2764 $this->page->set_url('/'); // no url
2765 //$this->page->set_pagelayout('base'); //TODO: MDL-20676 blocks on error pages are weird, unfortunately it somehow detect the pagelayout from URL :-(
2766 $this->page->set_title(get_string('error'));
2767 $this->page->set_heading($this->page->course->fullname);
2768 $output .= $this->header();
2771 $message = '<p class="errormessage">' . $message . '</p>'.
2772 '<p class="errorcode"><a href="' . $moreinfourl . '">' .
2773 get_string('moreinformation') . '</a></p>';
2774 if (empty($CFG->rolesactive)) {
2775 $message .= '<p class="errormessage">' . get_string('installproblem', 'error') . '</p>';
2776 //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.
2778 $output .= $this->box($message, 'errorbox alert alert-danger', null, array('data-rel' => 'fatalerror'));
2780 if ($CFG->debugdeveloper) {
2781 if (!empty($debuginfo)) {
2782 $debuginfo = s($debuginfo); // removes all nasty JS
2783 $debuginfo = str_replace("\n", '<br />', $debuginfo); // keep newlines
2784 $output .= $this->notification('<strong>Debug info:</strong> '.$debuginfo, 'notifytiny');
2786 if (!empty($backtrace)) {
2787 $output .= $this->notification('<strong>Stack trace:</strong> '.format_backtrace($backtrace), 'notifytiny');
2789 if ($obbuffer !== '' ) {
2790 $output .= $this->notification('<strong>Output buffer:</strong> '.s($obbuffer), 'notifytiny');
2794 if (empty($CFG->rolesactive)) {
2795 // continue does not make much sense if moodle is not installed yet because error is most probably not recoverable
2796 } else if (!empty($link)) {
2797 $output .= $this->continue_button($link);
2800 $output .= $this->footer();
2802 // Padding to encourage IE to display our error page, rather than its own.
2803 $output .= str_repeat(' ', 512);
2805 return $output;
2809 * Output a notification (that is, a status message about something that has just happened).
2811 * Note: \core\notification::add() may be more suitable for your usage.
2813 * @param string $message The message to print out.
2814 * @param string $type The type of notification. See constants on \core\output\notification.
2815 * @return string the HTML to output.
2817 public function notification($message, $type = null) {
2818 $typemappings = [
2819 // Valid types.
2820 'success' => \core\output\notification::NOTIFY_SUCCESS,
2821 'info' => \core\output\notification::NOTIFY_INFO,
2822 'warning' => \core\output\notification::NOTIFY_WARNING,
2823 'error' => \core\output\notification::NOTIFY_ERROR,
2825 // Legacy types mapped to current types.
2826 'notifyproblem' => \core\output\notification::NOTIFY_ERROR,
2827 'notifytiny' => \core\output\notification::NOTIFY_ERROR,
2828 'notifyerror' => \core\output\notification::NOTIFY_ERROR,
2829 'notifysuccess' => \core\output\notification::NOTIFY_SUCCESS,
2830 'notifymessage' => \core\output\notification::NOTIFY_INFO,
2831 'notifyredirect' => \core\output\notification::NOTIFY_INFO,
2832 'redirectmessage' => \core\output\notification::NOTIFY_INFO,
2835 $extraclasses = [];
2837 if ($type) {
2838 if (strpos($type, ' ') === false) {
2839 // No spaces in the list of classes, therefore no need to loop over and determine the class.
2840 if (isset($typemappings[$type])) {
2841 $type = $typemappings[$type];
2842 } else {
2843 // The value provided did not match a known type. It must be an extra class.
2844 $extraclasses = [$type];
2846 } else {
2847 // Identify what type of notification this is.
2848 $classarray = explode(' ', self::prepare_classes($type));
2850 // Separate out the type of notification from the extra classes.
2851 foreach ($classarray as $class) {
2852 if (isset($typemappings[$class])) {
2853 $type = $typemappings[$class];
2854 } else {
2855 $extraclasses[] = $class;
2861 $notification = new \core\output\notification($message, $type);
2862 if (count($extraclasses)) {
2863 $notification->set_extra_classes($extraclasses);
2866 // Return the rendered template.
2867 return $this->render_from_template($notification->get_template_name(), $notification->export_for_template($this));
2871 * @deprecated since Moodle 3.1 MDL-30811 - please do not use this function any more.
2873 public function notify_problem() {
2874 throw new coding_exception('core_renderer::notify_problem() can not be used any more, '.
2875 'please use \core\notification::add(), or \core\output\notification as required.');
2879 * @deprecated since Moodle 3.1 MDL-30811 - please do not use this function any more.
2881 public function notify_success() {
2882 throw new coding_exception('core_renderer::notify_success() can not be used any more, '.
2883 'please use \core\notification::add(), or \core\output\notification as required.');
2887 * @deprecated since Moodle 3.1 MDL-30811 - please do not use this function any more.
2889 public function notify_message() {
2890 throw new coding_exception('core_renderer::notify_message() can not be used any more, '.
2891 'please use \core\notification::add(), or \core\output\notification as required.');
2895 * @deprecated since Moodle 3.1 MDL-30811 - please do not use this function any more.
2897 public function notify_redirect() {
2898 throw new coding_exception('core_renderer::notify_redirect() can not be used any more, '.
2899 'please use \core\notification::add(), or \core\output\notification as required.');
2903 * Render a notification (that is, a status message about something that has
2904 * just happened).
2906 * @param \core\output\notification $notification the notification to print out
2907 * @return string the HTML to output.
2909 protected function render_notification(\core\output\notification $notification) {
2910 return $this->render_from_template($notification->get_template_name(), $notification->export_for_template($this));
2914 * Returns HTML to display a continue button that goes to a particular URL.
2916 * @param string|moodle_url $url The url the button goes to.
2917 * @return string the HTML to output.
2919 public function continue_button($url) {
2920 if (!($url instanceof moodle_url)) {
2921 $url = new moodle_url($url);
2923 $button = new single_button($url, get_string('continue'), 'get', true);
2924 $button->class = 'continuebutton';
2926 return $this->render($button);
2930 * Returns HTML to display a single paging bar to provide access to other pages (usually in a search)
2932 * Theme developers: DO NOT OVERRIDE! Please override function
2933 * {@link core_renderer::render_paging_bar()} instead.
2935 * @param int $totalcount The total number of entries available to be paged through
2936 * @param int $page The page you are currently viewing
2937 * @param int $perpage The number of entries that should be shown per page
2938 * @param string|moodle_url $baseurl url of the current page, the $pagevar parameter is added
2939 * @param string $pagevar name of page parameter that holds the page number
2940 * @return string the HTML to output.
2942 public function paging_bar($totalcount, $page, $perpage, $baseurl, $pagevar = 'page') {
2943 $pb = new paging_bar($totalcount, $page, $perpage, $baseurl, $pagevar);
2944 return $this->render($pb);
2948 * Returns HTML to display the paging bar.
2950 * @param paging_bar $pagingbar
2951 * @return string the HTML to output.
2953 protected function render_paging_bar(paging_bar $pagingbar) {
2954 // Any more than 10 is not usable and causes weird wrapping of the pagination.
2955 $pagingbar->maxdisplay = 10;
2956 return $this->render_from_template('core/paging_bar', $pagingbar->export_for_template($this));
2960 * Returns HTML to display initials bar to provide access to other pages (usually in a search)
2962 * @param string $current the currently selected letter.
2963 * @param string $class class name to add to this initial bar.
2964 * @param string $title the name to put in front of this initial bar.
2965 * @param string $urlvar URL parameter name for this initial.
2966 * @param string $url URL object.
2967 * @param array $alpha of letters in the alphabet.
2968 * @return string the HTML to output.
2970 public function initials_bar($current, $class, $title, $urlvar, $url, $alpha = null) {
2971 $ib = new initials_bar($current, $class, $title, $urlvar, $url, $alpha);
2972 return $this->render($ib);
2976 * Internal implementation of initials bar rendering.
2978 * @param initials_bar $initialsbar
2979 * @return string
2981 protected function render_initials_bar(initials_bar $initialsbar) {
2982 return $this->render_from_template('core/initials_bar', $initialsbar->export_for_template($this));
2986 * Output the place a skip link goes to.
2988 * @param string $id The target name from the corresponding $PAGE->requires->skip_link_to($target) call.
2989 * @return string the HTML to output.
2991 public function skip_link_target($id = null) {
2992 return html_writer::span('', '', array('id' => $id));
2996 * Outputs a heading
2998 * @param string $text The text of the heading
2999 * @param int $level The level of importance of the heading. Defaulting to 2
3000 * @param string $classes A space-separated list of CSS classes. Defaulting to null
3001 * @param string $id An optional ID
3002 * @return string the HTML to output.
3004 public function heading($text, $level = 2, $classes = null, $id = null) {
3005 $level = (integer) $level;
3006 if ($level < 1 or $level > 6) {
3007 throw new coding_exception('Heading level must be an integer between 1 and 6.');
3009 return html_writer::tag('h' . $level, $text, array('id' => $id, 'class' => renderer_base::prepare_classes($classes)));
3013 * Outputs a box.
3015 * @param string $contents The contents of the box
3016 * @param string $classes A space-separated list of CSS classes
3017 * @param string $id An optional ID
3018 * @param array $attributes An array of other attributes to give the box.
3019 * @return string the HTML to output.
3021 public function box($contents, $classes = 'generalbox', $id = null, $attributes = array()) {
3022 return $this->box_start($classes, $id, $attributes) . $contents . $this->box_end();
3026 * Outputs the opening section of a box.
3028 * @param string $classes A space-separated list of CSS classes
3029 * @param string $id An optional ID
3030 * @param array $attributes An array of other attributes to give the box.
3031 * @return string the HTML to output.
3033 public function box_start($classes = 'generalbox', $id = null, $attributes = array()) {
3034 $this->opencontainers->push('box', html_writer::end_tag('div'));
3035 $attributes['id'] = $id;
3036 $attributes['class'] = 'box py-3 ' . renderer_base::prepare_classes($classes);
3037 return html_writer::start_tag('div', $attributes);
3041 * Outputs the closing section of a box.
3043 * @return string the HTML to output.
3045 public function box_end() {
3046 return $this->opencontainers->pop('box');
3050 * Outputs a container.
3052 * @param string $contents The contents of the box
3053 * @param string $classes A space-separated list of CSS classes
3054 * @param string $id An optional ID
3055 * @return string the HTML to output.
3057 public function container($contents, $classes = null, $id = null) {
3058 return $this->container_start($classes, $id) . $contents . $this->container_end();
3062 * Outputs the opening section of a container.
3064 * @param string $classes A space-separated list of CSS classes
3065 * @param string $id An optional ID
3066 * @return string the HTML to output.
3068 public function container_start($classes = null, $id = null) {
3069 $this->opencontainers->push('container', html_writer::end_tag('div'));
3070 return html_writer::start_tag('div', array('id' => $id,
3071 'class' => renderer_base::prepare_classes($classes)));
3075 * Outputs the closing section of a container.
3077 * @return string the HTML to output.
3079 public function container_end() {
3080 return $this->opencontainers->pop('container');
3084 * Make nested HTML lists out of the items
3086 * The resulting list will look something like this:
3088 * <pre>
3089 * <<ul>>
3090 * <<li>><div class='tree_item parent'>(item contents)</div>
3091 * <<ul>
3092 * <<li>><div class='tree_item'>(item contents)</div><</li>>
3093 * <</ul>>
3094 * <</li>>
3095 * <</ul>>
3096 * </pre>
3098 * @param array $items
3099 * @param array $attrs html attributes passed to the top ofs the list
3100 * @return string HTML
3102 public function tree_block_contents($items, $attrs = array()) {
3103 // exit if empty, we don't want an empty ul element
3104 if (empty($items)) {
3105 return '';
3107 // array of nested li elements
3108 $lis = array();
3109 foreach ($items as $item) {
3110 // this applies to the li item which contains all child lists too
3111 $content = $item->content($this);
3112 $liclasses = array($item->get_css_type());
3113 if (!$item->forceopen || (!$item->forceopen && $item->collapse) || ($item->children->count()==0 && $item->nodetype==navigation_node::NODETYPE_BRANCH)) {
3114 $liclasses[] = 'collapsed';
3116 if ($item->isactive === true) {
3117 $liclasses[] = 'current_branch';
3119 $liattr = array('class'=>join(' ',$liclasses));
3120 // class attribute on the div item which only contains the item content
3121 $divclasses = array('tree_item');
3122 if ($item->children->count()>0 || $item->nodetype==navigation_node::NODETYPE_BRANCH) {
3123 $divclasses[] = 'branch';
3124 } else {
3125 $divclasses[] = 'leaf';
3127 if (!empty($item->classes) && count($item->classes)>0) {
3128 $divclasses[] = join(' ', $item->classes);
3130 $divattr = array('class'=>join(' ', $divclasses));
3131 if (!empty($item->id)) {
3132 $divattr['id'] = $item->id;
3134 $content = html_writer::tag('p', $content, $divattr) . $this->tree_block_contents($item->children);
3135 if (!empty($item->preceedwithhr) && $item->preceedwithhr===true) {
3136 $content = html_writer::empty_tag('hr') . $content;
3138 $content = html_writer::tag('li', $content, $liattr);
3139 $lis[] = $content;
3141 return html_writer::tag('ul', implode("\n", $lis), $attrs);
3145 * Returns a search box.
3147 * @param string $id The search box wrapper div id, defaults to an autogenerated one.
3148 * @return string HTML with the search form hidden by default.
3150 public function search_box($id = false) {
3151 global $CFG;
3153 // Accessing $CFG directly as using \core_search::is_global_search_enabled would
3154 // result in an extra included file for each site, even the ones where global search
3155 // is disabled.
3156 if (empty($CFG->enableglobalsearch) || !has_capability('moodle/search:query', context_system::instance())) {
3157 return '';
3160 if ($id == false) {
3161 $id = uniqid();
3162 } else {
3163 // Needs to be cleaned, we use it for the input id.
3164 $id = clean_param($id, PARAM_ALPHANUMEXT);
3167 // JS to animate the form.
3168 $this->page->requires->js_call_amd('core/search-input', 'init', array($id));
3170 $searchicon = html_writer::tag('div', $this->pix_icon('a/search', get_string('search', 'search'), 'moodle'),
3171 array('role' => 'button', 'tabindex' => 0));
3172 $formattrs = array('class' => 'search-input-form', 'action' => $CFG->wwwroot . '/search/index.php');
3173 $inputattrs = array('type' => 'text', 'name' => 'q', 'placeholder' => get_string('search', 'search'),
3174 'size' => 13, 'tabindex' => -1, 'id' => 'id_q_' . $id, 'class' => 'form-control');
3176 $contents = html_writer::tag('label', get_string('enteryoursearchquery', 'search'),
3177 array('for' => 'id_q_' . $id, 'class' => 'accesshide')) . html_writer::tag('input', '', $inputattrs);
3178 if ($this->page->context && $this->page->context->contextlevel !== CONTEXT_SYSTEM) {
3179 $contents .= html_writer::empty_tag('input', ['type' => 'hidden',
3180 'name' => 'context', 'value' => $this->page->context->id]);
3182 $searchinput = html_writer::tag('form', $contents, $formattrs);
3184 return html_writer::tag('div', $searchicon . $searchinput, array('class' => 'search-input-wrapper nav-link', 'id' => $id));
3188 * Allow plugins to provide some content to be rendered in the navbar.
3189 * The plugin must define a PLUGIN_render_navbar_output function that returns
3190 * the HTML they wish to add to the navbar.
3192 * @return string HTML for the navbar
3194 public function navbar_plugin_output() {
3195 $output = '';
3197 // Give subsystems an opportunity to inject extra html content. The callback
3198 // must always return a string containing valid html.
3199 foreach (\core_component::get_core_subsystems() as $name => $path) {
3200 if ($path) {
3201 $output .= component_callback($name, 'render_navbar_output', [$this], '');
3205 if ($pluginsfunction = get_plugins_with_function('render_navbar_output')) {
3206 foreach ($pluginsfunction as $plugintype => $plugins) {
3207 foreach ($plugins as $pluginfunction) {
3208 $output .= $pluginfunction($this);
3213 return $output;
3217 * Construct a user menu, returning HTML that can be echoed out by a
3218 * layout file.
3220 * @param stdClass $user A user object, usually $USER.
3221 * @param bool $withlinks true if a dropdown should be built.
3222 * @return string HTML fragment.
3224 public function user_menu($user = null, $withlinks = null) {
3225 global $USER, $CFG;
3226 require_once($CFG->dirroot . '/user/lib.php');
3228 if (is_null($user)) {
3229 $user = $USER;
3232 // Note: this behaviour is intended to match that of core_renderer::login_info,
3233 // but should not be considered to be good practice; layout options are
3234 // intended to be theme-specific. Please don't copy this snippet anywhere else.
3235 if (is_null($withlinks)) {
3236 $withlinks = empty($this->page->layout_options['nologinlinks']);
3239 // Add a class for when $withlinks is false.
3240 $usermenuclasses = 'usermenu';
3241 if (!$withlinks) {
3242 $usermenuclasses .= ' withoutlinks';
3245 $returnstr = "";
3247 // If during initial install, return the empty return string.
3248 if (during_initial_install()) {
3249 return $returnstr;
3252 $loginpage = $this->is_login_page();
3253 $loginurl = get_login_url();
3254 // If not logged in, show the typical not-logged-in string.
3255 if (!isloggedin()) {
3256 $returnstr = get_string('loggedinnot', 'moodle');
3257 if (!$loginpage) {
3258 $returnstr .= " (<a href=\"$loginurl\">" . get_string('login') . '</a>)';
3260 return html_writer::div(
3261 html_writer::span(
3262 $returnstr,
3263 'login'
3265 $usermenuclasses
3270 // If logged in as a guest user, show a string to that effect.
3271 if (isguestuser()) {
3272 $returnstr = get_string('loggedinasguest');
3273 if (!$loginpage && $withlinks) {
3274 $returnstr .= " (<a href=\"$loginurl\">".get_string('login').'</a>)';
3277 return html_writer::div(
3278 html_writer::span(
3279 $returnstr,
3280 'login'
3282 $usermenuclasses
3286 // Get some navigation opts.
3287 $opts = user_get_user_navigation_info($user, $this->page);
3289 $avatarclasses = "avatars";
3290 $avatarcontents = html_writer::span($opts->metadata['useravatar'], 'avatar current');
3291 $usertextcontents = $opts->metadata['userfullname'];
3293 // Other user.
3294 if (!empty($opts->metadata['asotheruser'])) {
3295 $avatarcontents .= html_writer::span(
3296 $opts->metadata['realuseravatar'],
3297 'avatar realuser'
3299 $usertextcontents = $opts->metadata['realuserfullname'];
3300 $usertextcontents .= html_writer::tag(
3301 'span',
3302 get_string(
3303 'loggedinas',
3304 'moodle',
3305 html_writer::span(
3306 $opts->metadata['userfullname'],
3307 'value'
3310 array('class' => 'meta viewingas')
3314 // Role.
3315 if (!empty($opts->metadata['asotherrole'])) {
3316 $role = core_text::strtolower(preg_replace('#[ ]+#', '-', trim($opts->metadata['rolename'])));
3317 $usertextcontents .= html_writer::span(
3318 $opts->metadata['rolename'],
3319 'meta role role-' . $role
3323 // User login failures.
3324 if (!empty($opts->metadata['userloginfail'])) {
3325 $usertextcontents .= html_writer::span(
3326 $opts->metadata['userloginfail'],
3327 'meta loginfailures'
3331 // MNet.
3332 if (!empty($opts->metadata['asmnetuser'])) {
3333 $mnet = strtolower(preg_replace('#[ ]+#', '-', trim($opts->metadata['mnetidprovidername'])));
3334 $usertextcontents .= html_writer::span(
3335 $opts->metadata['mnetidprovidername'],
3336 'meta mnet mnet-' . $mnet
3340 $returnstr .= html_writer::span(
3341 html_writer::span($usertextcontents, 'usertext mr-1') .
3342 html_writer::span($avatarcontents, $avatarclasses),
3343 'userbutton'
3346 // Create a divider (well, a filler).
3347 $divider = new action_menu_filler();
3348 $divider->primary = false;
3350 $am = new action_menu();
3351 $am->set_menu_trigger(
3352 $returnstr
3354 $am->set_action_label(get_string('usermenu'));
3355 $am->set_alignment(action_menu::TR, action_menu::BR);
3356 $am->set_nowrap_on_items();
3357 if ($withlinks) {
3358 $navitemcount = count($opts->navitems);
3359 $idx = 0;
3360 foreach ($opts->navitems as $key => $value) {
3362 switch ($value->itemtype) {
3363 case 'divider':
3364 // If the nav item is a divider, add one and skip link processing.
3365 $am->add($divider);
3366 break;
3368 case 'invalid':
3369 // Silently skip invalid entries (should we post a notification?).
3370 break;
3372 case 'link':
3373 // Process this as a link item.
3374 $pix = null;
3375 if (isset($value->pix) && !empty($value->pix)) {
3376 $pix = new pix_icon($value->pix, '', null, array('class' => 'iconsmall'));
3377 } else if (isset($value->imgsrc) && !empty($value->imgsrc)) {
3378 $value->title = html_writer::img(
3379 $value->imgsrc,
3380 $value->title,
3381 array('class' => 'iconsmall')
3382 ) . $value->title;
3385 $al = new action_menu_link_secondary(
3386 $value->url,
3387 $pix,
3388 $value->title,
3389 array('class' => 'icon')
3391 if (!empty($value->titleidentifier)) {
3392 $al->attributes['data-title'] = $value->titleidentifier;
3394 $am->add($al);
3395 break;
3398 $idx++;
3400 // Add dividers after the first item and before the last item.
3401 if ($idx == 1 || $idx == $navitemcount - 1) {
3402 $am->add($divider);
3407 return html_writer::div(
3408 $this->render($am),
3409 $usermenuclasses
3414 * This renders the navbar.
3415 * Uses bootstrap compatible html.
3417 public function navbar() {
3418 return $this->render_from_template('core/navbar', $this->page->navbar);
3422 * Renders a breadcrumb navigation node object.
3424 * @param breadcrumb_navigation_node $item The navigation node to render.
3425 * @return string HTML fragment
3427 protected function render_breadcrumb_navigation_node(breadcrumb_navigation_node $item) {
3429 if ($item->action instanceof moodle_url) {
3430 $content = $item->get_content();
3431 $title = $item->get_title();
3432 $attributes = array();
3433 $attributes['itemprop'] = 'url';
3434 if ($title !== '') {
3435 $attributes['title'] = $title;
3437 if ($item->hidden) {
3438 $attributes['class'] = 'dimmed_text';
3440 if ($item->is_last()) {
3441 $attributes['aria-current'] = 'page';
3443 $content = html_writer::tag('span', $content, array('itemprop' => 'title'));
3444 $content = html_writer::link($item->action, $content, $attributes);
3446 $attributes = array();
3447 $attributes['itemscope'] = '';
3448 $attributes['itemtype'] = 'http://data-vocabulary.org/Breadcrumb';
3449 $content = html_writer::tag('span', $content, $attributes);
3451 } else {
3452 $content = $this->render_navigation_node($item);
3454 return $content;
3458 * Renders a navigation node object.
3460 * @param navigation_node $item The navigation node to render.
3461 * @return string HTML fragment
3463 protected function render_navigation_node(navigation_node $item) {
3464 $content = $item->get_content();
3465 $title = $item->get_title();
3466 if ($item->icon instanceof renderable && !$item->hideicon) {
3467 $icon = $this->render($item->icon);
3468 $content = $icon.$content; // use CSS for spacing of icons
3470 if ($item->helpbutton !== null) {
3471 $content = trim($item->helpbutton).html_writer::tag('span', $content, array('class'=>'clearhelpbutton', 'tabindex'=>'0'));
3473 if ($content === '') {
3474 return '';
3476 if ($item->action instanceof action_link) {
3477 $link = $item->action;
3478 if ($item->hidden) {
3479 $link->add_class('dimmed');
3481 if (!empty($content)) {
3482 // Providing there is content we will use that for the link content.
3483 $link->text = $content;
3485 $content = $this->render($link);
3486 } else if ($item->action instanceof moodle_url) {
3487 $attributes = array();
3488 if ($title !== '') {
3489 $attributes['title'] = $title;
3491 if ($item->hidden) {
3492 $attributes['class'] = 'dimmed_text';
3494 $content = html_writer::link($item->action, $content, $attributes);
3496 } else if (is_string($item->action) || empty($item->action)) {
3497 $attributes = array('tabindex'=>'0'); //add tab support to span but still maintain character stream sequence.
3498 if ($title !== '') {
3499 $attributes['title'] = $title;
3501 if ($item->hidden) {
3502 $attributes['class'] = 'dimmed_text';
3504 $content = html_writer::tag('span', $content, $attributes);
3506 return $content;
3510 * Accessibility: Right arrow-like character is
3511 * used in the breadcrumb trail, course navigation menu
3512 * (previous/next activity), calendar, and search forum block.
3513 * If the theme does not set characters, appropriate defaults
3514 * are set automatically. Please DO NOT
3515 * use &lt; &gt; &raquo; - these are confusing for blind users.
3517 * @return string
3519 public function rarrow() {
3520 return $this->page->theme->rarrow;
3524 * Accessibility: Left arrow-like character is
3525 * used in the breadcrumb trail, course navigation menu
3526 * (previous/next activity), calendar, and search forum block.
3527 * If the theme does not set characters, appropriate defaults
3528 * are set automatically. Please DO NOT
3529 * use &lt; &gt; &raquo; - these are confusing for blind users.
3531 * @return string
3533 public function larrow() {
3534 return $this->page->theme->larrow;
3538 * Accessibility: Up arrow-like character is used in
3539 * the book heirarchical navigation.
3540 * If the theme does not set characters, appropriate defaults
3541 * are set automatically. Please DO NOT
3542 * use ^ - this is confusing for blind users.
3544 * @return string
3546 public function uarrow() {
3547 return $this->page->theme->uarrow;
3551 * Accessibility: Down arrow-like character.
3552 * If the theme does not set characters, appropriate defaults
3553 * are set automatically.
3555 * @return string
3557 public function darrow() {
3558 return $this->page->theme->darrow;
3562 * Returns the custom menu if one has been set
3564 * A custom menu can be configured by browsing to
3565 * Settings: Administration > Appearance > Themes > Theme settings
3566 * and then configuring the custommenu config setting as described.
3568 * Theme developers: DO NOT OVERRIDE! Please override function
3569 * {@link core_renderer::render_custom_menu()} instead.
3571 * @param string $custommenuitems - custom menuitems set by theme instead of global theme settings
3572 * @return string
3574 public function custom_menu($custommenuitems = '') {
3575 global $CFG;
3577 if (empty($custommenuitems) && !empty($CFG->custommenuitems)) {
3578 $custommenuitems = $CFG->custommenuitems;
3580 $custommenu = new custom_menu($custommenuitems, current_language());
3581 return $this->render_custom_menu($custommenu);
3585 * We want to show the custom menus as a list of links in the footer on small screens.
3586 * Just return the menu object exported so we can render it differently.
3588 public function custom_menu_flat() {
3589 global $CFG;
3590 $custommenuitems = '';
3592 if (empty($custommenuitems) && !empty($CFG->custommenuitems)) {
3593 $custommenuitems = $CFG->custommenuitems;
3595 $custommenu = new custom_menu($custommenuitems, current_language());
3596 $langs = get_string_manager()->get_list_of_translations();
3597 $haslangmenu = $this->lang_menu() != '';
3599 if ($haslangmenu) {
3600 $strlang = get_string('language');
3601 $currentlang = current_language();
3602 if (isset($langs[$currentlang])) {
3603 $currentlang = $langs[$currentlang];
3604 } else {
3605 $currentlang = $strlang;
3607 $this->language = $custommenu->add($currentlang, new moodle_url('#'), $strlang, 10000);
3608 foreach ($langs as $langtype => $langname) {
3609 $this->language->add($langname, new moodle_url($this->page->url, array('lang' => $langtype)), $langname);
3613 return $custommenu->export_for_template($this);
3617 * Renders a custom menu object (located in outputcomponents.php)
3619 * The custom menu this method produces makes use of the YUI3 menunav widget
3620 * and requires very specific html elements and classes.
3622 * @staticvar int $menucount
3623 * @param custom_menu $menu
3624 * @return string
3626 protected function render_custom_menu(custom_menu $menu) {
3627 global $CFG;
3629 $langs = get_string_manager()->get_list_of_translations();
3630 $haslangmenu = $this->lang_menu() != '';
3632 if (!$menu->has_children() && !$haslangmenu) {
3633 return '';
3636 if ($haslangmenu) {
3637 $strlang = get_string('language');
3638 $currentlang = current_language();
3639 if (isset($langs[$currentlang])) {
3640 $currentlang = $langs[$currentlang];
3641 } else {
3642 $currentlang = $strlang;
3644 $this->language = $menu->add($currentlang, new moodle_url('#'), $strlang, 10000);
3645 foreach ($langs as $langtype => $langname) {
3646 $this->language->add($langname, new moodle_url($this->page->url, array('lang' => $langtype)), $langname);
3650 $content = '';
3651 foreach ($menu->get_children() as $item) {
3652 $context = $item->export_for_template($this);
3653 $content .= $this->render_from_template('core/custom_menu_item', $context);
3656 return $content;
3660 * Renders a custom menu node as part of a submenu
3662 * The custom menu this method produces makes use of the YUI3 menunav widget
3663 * and requires very specific html elements and classes.
3665 * @see core:renderer::render_custom_menu()
3667 * @staticvar int $submenucount
3668 * @param custom_menu_item $menunode
3669 * @return string
3671 protected function render_custom_menu_item(custom_menu_item $menunode) {
3672 // Required to ensure we get unique trackable id's
3673 static $submenucount = 0;
3674 if ($menunode->has_children()) {
3675 // If the child has menus render it as a sub menu
3676 $submenucount++;
3677 $content = html_writer::start_tag('li');
3678 if ($menunode->get_url() !== null) {
3679 $url = $menunode->get_url();
3680 } else {
3681 $url = '#cm_submenu_'.$submenucount;
3683 $content .= html_writer::link($url, $menunode->get_text(), array('class'=>'yui3-menu-label', 'title'=>$menunode->get_title()));
3684 $content .= html_writer::start_tag('div', array('id'=>'cm_submenu_'.$submenucount, 'class'=>'yui3-menu custom_menu_submenu'));
3685 $content .= html_writer::start_tag('div', array('class'=>'yui3-menu-content'));
3686 $content .= html_writer::start_tag('ul');
3687 foreach ($menunode->get_children() as $menunode) {
3688 $content .= $this->render_custom_menu_item($menunode);
3690 $content .= html_writer::end_tag('ul');
3691 $content .= html_writer::end_tag('div');
3692 $content .= html_writer::end_tag('div');
3693 $content .= html_writer::end_tag('li');
3694 } else {
3695 // The node doesn't have children so produce a final menuitem.
3696 // Also, if the node's text matches '####', add a class so we can treat it as a divider.
3697 $content = '';
3698 if (preg_match("/^#+$/", $menunode->get_text())) {
3700 // This is a divider.
3701 $content = html_writer::start_tag('li', array('class' => 'yui3-menuitem divider'));
3702 } else {
3703 $content = html_writer::start_tag(
3704 'li',
3705 array(
3706 'class' => 'yui3-menuitem'
3709 if ($menunode->get_url() !== null) {
3710 $url = $menunode->get_url();
3711 } else {
3712 $url = '#';
3714 $content .= html_writer::link(
3715 $url,
3716 $menunode->get_text(),
3717 array('class' => 'yui3-menuitem-content', 'title' => $menunode->get_title())
3720 $content .= html_writer::end_tag('li');
3722 // Return the sub menu
3723 return $content;
3727 * Renders theme links for switching between default and other themes.
3729 * @return string
3731 protected function theme_switch_links() {
3733 $actualdevice = core_useragent::get_device_type();
3734 $currentdevice = $this->page->devicetypeinuse;
3735 $switched = ($actualdevice != $currentdevice);
3737 if (!$switched && $currentdevice == 'default' && $actualdevice == 'default') {
3738 // The user is using the a default device and hasn't switched so don't shown the switch
3739 // device links.
3740 return '';
3743 if ($switched) {
3744 $linktext = get_string('switchdevicerecommended');
3745 $devicetype = $actualdevice;
3746 } else {
3747 $linktext = get_string('switchdevicedefault');
3748 $devicetype = 'default';
3750 $linkurl = new moodle_url('/theme/switchdevice.php', array('url' => $this->page->url, 'device' => $devicetype, 'sesskey' => sesskey()));
3752 $content = html_writer::start_tag('div', array('id' => 'theme_switch_link'));
3753 $content .= html_writer::link($linkurl, $linktext, array('rel' => 'nofollow'));
3754 $content .= html_writer::end_tag('div');
3756 return $content;
3760 * Renders tabs
3762 * This function replaces print_tabs() used before Moodle 2.5 but with slightly different arguments
3764 * Theme developers: In order to change how tabs are displayed please override functions
3765 * {@link core_renderer::render_tabtree()} and/or {@link core_renderer::render_tabobject()}
3767 * @param array $tabs array of tabs, each of them may have it's own ->subtree
3768 * @param string|null $selected which tab to mark as selected, all parent tabs will
3769 * automatically be marked as activated
3770 * @param array|string|null $inactive list of ids of inactive tabs, regardless of
3771 * their level. Note that you can as weel specify tabobject::$inactive for separate instances
3772 * @return string
3774 public final function tabtree($tabs, $selected = null, $inactive = null) {
3775 return $this->render(new tabtree($tabs, $selected, $inactive));
3779 * Renders tabtree
3781 * @param tabtree $tabtree
3782 * @return string
3784 protected function render_tabtree(tabtree $tabtree) {
3785 if (empty($tabtree->subtree)) {
3786 return '';
3788 $data = $tabtree->export_for_template($this);
3789 return $this->render_from_template('core/tabtree', $data);
3793 * Renders tabobject (part of tabtree)
3795 * This function is called from {@link core_renderer::render_tabtree()}
3796 * and also it calls itself when printing the $tabobject subtree recursively.
3798 * Property $tabobject->level indicates the number of row of tabs.
3800 * @param tabobject $tabobject
3801 * @return string HTML fragment
3803 protected function render_tabobject(tabobject $tabobject) {
3804 $str = '';
3806 // Print name of the current tab.
3807 if ($tabobject instanceof tabtree) {
3808 // No name for tabtree root.
3809 } else if ($tabobject->inactive || $tabobject->activated || ($tabobject->selected && !$tabobject->linkedwhenselected)) {
3810 // Tab name without a link. The <a> tag is used for styling.
3811 $str .= html_writer::tag('a', html_writer::span($tabobject->text), array('class' => 'nolink moodle-has-zindex'));
3812 } else {
3813 // Tab name with a link.
3814 if (!($tabobject->link instanceof moodle_url)) {
3815 // backward compartibility when link was passed as quoted string
3816 $str .= "<a href=\"$tabobject->link\" title=\"$tabobject->title\"><span>$tabobject->text</span></a>";
3817 } else {
3818 $str .= html_writer::link($tabobject->link, html_writer::span($tabobject->text), array('title' => $tabobject->title));
3822 if (empty($tabobject->subtree)) {
3823 if ($tabobject->selected) {
3824 $str .= html_writer::tag('div', '&nbsp;', array('class' => 'tabrow'. ($tabobject->level + 1). ' empty'));
3826 return $str;
3829 // Print subtree.
3830 if ($tabobject->level == 0 || $tabobject->selected || $tabobject->activated) {
3831 $str .= html_writer::start_tag('ul', array('class' => 'tabrow'. $tabobject->level));
3832 $cnt = 0;
3833 foreach ($tabobject->subtree as $tab) {
3834 $liclass = '';
3835 if (!$cnt) {
3836 $liclass .= ' first';
3838 if ($cnt == count($tabobject->subtree) - 1) {
3839 $liclass .= ' last';
3841 if ((empty($tab->subtree)) && (!empty($tab->selected))) {
3842 $liclass .= ' onerow';
3845 if ($tab->selected) {
3846 $liclass .= ' here selected';
3847 } else if ($tab->activated) {
3848 $liclass .= ' here active';
3851 // This will recursively call function render_tabobject() for each item in subtree.
3852 $str .= html_writer::tag('li', $this->render($tab), array('class' => trim($liclass)));
3853 $cnt++;
3855 $str .= html_writer::end_tag('ul');
3858 return $str;
3862 * Get the HTML for blocks in the given region.
3864 * @since Moodle 2.5.1 2.6
3865 * @param string $region The region to get HTML for.
3866 * @return string HTML.
3868 public function blocks($region, $classes = array(), $tag = 'aside') {
3869 $displayregion = $this->page->apply_theme_region_manipulations($region);
3870 $classes = (array)$classes;
3871 $classes[] = 'block-region';
3872 $attributes = array(
3873 'id' => 'block-region-'.preg_replace('#[^a-zA-Z0-9_\-]+#', '-', $displayregion),
3874 'class' => join(' ', $classes),
3875 'data-blockregion' => $displayregion,
3876 'data-droptarget' => '1'
3878 if ($this->page->blocks->region_has_content($displayregion, $this)) {
3879 $content = $this->blocks_for_region($displayregion);
3880 } else {
3881 $content = '';
3883 return html_writer::tag($tag, $content, $attributes);
3887 * Renders a custom block region.
3889 * Use this method if you want to add an additional block region to the content of the page.
3890 * Please note this should only be used in special situations.
3891 * We want to leave the theme is control where ever possible!
3893 * This method must use the same method that the theme uses within its layout file.
3894 * As such it asks the theme what method it is using.
3895 * It can be one of two values, blocks or blocks_for_region (deprecated).
3897 * @param string $regionname The name of the custom region to add.
3898 * @return string HTML for the block region.
3900 public function custom_block_region($regionname) {
3901 if ($this->page->theme->get_block_render_method() === 'blocks') {
3902 return $this->blocks($regionname);
3903 } else {
3904 return $this->blocks_for_region($regionname);
3909 * Returns the CSS classes to apply to the body tag.
3911 * @since Moodle 2.5.1 2.6
3912 * @param array $additionalclasses Any additional classes to apply.
3913 * @return string
3915 public function body_css_classes(array $additionalclasses = array()) {
3916 return $this->page->bodyclasses . ' ' . implode(' ', $additionalclasses);
3920 * The ID attribute to apply to the body tag.
3922 * @since Moodle 2.5.1 2.6
3923 * @return string
3925 public function body_id() {
3926 return $this->page->bodyid;
3930 * Returns HTML attributes to use within the body tag. This includes an ID and classes.
3932 * @since Moodle 2.5.1 2.6
3933 * @param string|array $additionalclasses Any additional classes to give the body tag,
3934 * @return string
3936 public function body_attributes($additionalclasses = array()) {
3937 if (!is_array($additionalclasses)) {
3938 $additionalclasses = explode(' ', $additionalclasses);
3940 return ' id="'. $this->body_id().'" class="'.$this->body_css_classes($additionalclasses).'"';
3944 * Gets HTML for the page heading.
3946 * @since Moodle 2.5.1 2.6
3947 * @param string $tag The tag to encase the heading in. h1 by default.
3948 * @return string HTML.
3950 public function page_heading($tag = 'h1') {
3951 return html_writer::tag($tag, $this->page->heading);
3955 * Gets the HTML for the page heading button.
3957 * @since Moodle 2.5.1 2.6
3958 * @return string HTML.
3960 public function page_heading_button() {
3961 return $this->page->button;
3965 * Returns the Moodle docs link to use for this page.
3967 * @since Moodle 2.5.1 2.6
3968 * @param string $text
3969 * @return string
3971 public function page_doc_link($text = null) {
3972 if ($text === null) {
3973 $text = get_string('moodledocslink');
3975 $path = page_get_doc_link_path($this->page);
3976 if (!$path) {
3977 return '';
3979 return $this->doc_link($path, $text);
3983 * Returns the page heading menu.
3985 * @since Moodle 2.5.1 2.6
3986 * @return string HTML.
3988 public function page_heading_menu() {
3989 return $this->page->headingmenu;
3993 * Returns the title to use on the page.
3995 * @since Moodle 2.5.1 2.6
3996 * @return string
3998 public function page_title() {
3999 return $this->page->title;
4003 * Returns the URL for the favicon.
4005 * @since Moodle 2.5.1 2.6
4006 * @return string The favicon URL
4008 public function favicon() {
4009 return $this->image_url('favicon', 'theme');
4013 * Renders preferences groups.
4015 * @param preferences_groups $renderable The renderable
4016 * @return string The output.
4018 public function render_preferences_groups(preferences_groups $renderable) {
4019 return $this->render_from_template('core/preferences_groups', $renderable);
4023 * Renders preferences group.
4025 * @param preferences_group $renderable The renderable
4026 * @return string The output.
4028 public function render_preferences_group(preferences_group $renderable) {
4029 $html = '';
4030 $html .= html_writer::start_tag('div', array('class' => 'col-sm-4 preferences-group'));
4031 $html .= $this->heading($renderable->title, 3);
4032 $html .= html_writer::start_tag('ul');
4033 foreach ($renderable->nodes as $node) {
4034 if ($node->has_children()) {
4035 debugging('Preferences nodes do not support children', DEBUG_DEVELOPER);
4037 $html .= html_writer::tag('li', $this->render($node));
4039 $html .= html_writer::end_tag('ul');
4040 $html .= html_writer::end_tag('div');
4041 return $html;
4044 public function context_header($headerinfo = null, $headinglevel = 1) {
4045 global $DB, $USER, $CFG, $SITE;
4046 require_once($CFG->dirroot . '/user/lib.php');
4047 $context = $this->page->context;
4048 $heading = null;
4049 $imagedata = null;
4050 $subheader = null;
4051 $userbuttons = null;
4053 if ($this->should_display_main_logo($headinglevel)) {
4054 $sitename = format_string($SITE->fullname, true, array('context' => context_course::instance(SITEID)));
4055 return html_writer::div(html_writer::empty_tag('img', [
4056 'src' => $this->get_logo_url(null, 150), 'alt' => $sitename, 'class' => 'img-fluid']), 'logo');
4059 // Make sure to use the heading if it has been set.
4060 if (isset($headerinfo['heading'])) {
4061 $heading = $headerinfo['heading'];
4064 // The user context currently has images and buttons. Other contexts may follow.
4065 if (isset($headerinfo['user']) || $context->contextlevel == CONTEXT_USER) {
4066 if (isset($headerinfo['user'])) {
4067 $user = $headerinfo['user'];
4068 } else {
4069 // Look up the user information if it is not supplied.
4070 $user = $DB->get_record('user', array('id' => $context->instanceid));
4073 // If the user context is set, then use that for capability checks.
4074 if (isset($headerinfo['usercontext'])) {
4075 $context = $headerinfo['usercontext'];
4078 // Only provide user information if the user is the current user, or a user which the current user can view.
4079 // When checking user_can_view_profile(), either:
4080 // If the page context is course, check the course context (from the page object) or;
4081 // If page context is NOT course, then check across all courses.
4082 $course = ($this->page->context->contextlevel == CONTEXT_COURSE) ? $this->page->course : null;
4084 if (user_can_view_profile($user, $course)) {
4085 // Use the user's full name if the heading isn't set.
4086 if (!isset($heading)) {
4087 $heading = fullname($user);
4090 $imagedata = $this->user_picture($user, array('size' => 100));
4092 // Check to see if we should be displaying a message button.
4093 if (!empty($CFG->messaging) && has_capability('moodle/site:sendmessage', $context)) {
4094 $userbuttons = array(
4095 'messages' => array(
4096 'buttontype' => 'message',
4097 'title' => get_string('message', 'message'),
4098 'url' => new moodle_url('/message/index.php', array('id' => $user->id)),
4099 'image' => 'message',
4100 'linkattributes' => \core_message\helper::messageuser_link_params($user->id),
4101 'page' => $this->page
4105 if ($USER->id != $user->id) {
4106 $iscontact = \core_message\api::is_contact($USER->id, $user->id);
4107 $contacttitle = $iscontact ? 'removefromyourcontacts' : 'addtoyourcontacts';
4108 $contacturlaction = $iscontact ? 'removecontact' : 'addcontact';
4109 $contactimage = $iscontact ? 'removecontact' : 'addcontact';
4110 $userbuttons['togglecontact'] = array(
4111 'buttontype' => 'togglecontact',
4112 'title' => get_string($contacttitle, 'message'),
4113 'url' => new moodle_url('/message/index.php', array(
4114 'user1' => $USER->id,
4115 'user2' => $user->id,
4116 $contacturlaction => $user->id,
4117 'sesskey' => sesskey())
4119 'image' => $contactimage,
4120 'linkattributes' => \core_message\helper::togglecontact_link_params($user, $iscontact),
4121 'page' => $this->page
4125 $this->page->requires->string_for_js('changesmadereallygoaway', 'moodle');
4127 } else {
4128 $heading = null;
4132 $contextheader = new context_header($heading, $headinglevel, $imagedata, $userbuttons);
4133 return $this->render_context_header($contextheader);
4137 * Renders the skip links for the page.
4139 * @param array $links List of skip links.
4140 * @return string HTML for the skip links.
4142 public function render_skip_links($links) {
4143 $context = [ 'links' => []];
4145 foreach ($links as $url => $text) {
4146 $context['links'][] = [ 'url' => $url, 'text' => $text];
4149 return $this->render_from_template('core/skip_links', $context);
4153 * Renders the header bar.
4155 * @param context_header $contextheader Header bar object.
4156 * @return string HTML for the header bar.
4158 protected function render_context_header(context_header $contextheader) {
4160 $showheader = empty($this->page->layout_options['nocontextheader']);
4161 if (!$showheader) {
4162 return '';
4165 // All the html stuff goes here.
4166 $html = html_writer::start_div('page-context-header');
4168 // Image data.
4169 if (isset($contextheader->imagedata)) {
4170 // Header specific image.
4171 $html .= html_writer::div($contextheader->imagedata, 'page-header-image');
4174 // Headings.
4175 if (!isset($contextheader->heading)) {
4176 $headings = $this->heading($this->page->heading, $contextheader->headinglevel);
4177 } else {
4178 $headings = $this->heading($contextheader->heading, $contextheader->headinglevel);
4181 $html .= html_writer::tag('div', $headings, array('class' => 'page-header-headings'));
4183 // Buttons.
4184 if (isset($contextheader->additionalbuttons)) {
4185 $html .= html_writer::start_div('btn-group header-button-group');
4186 foreach ($contextheader->additionalbuttons as $button) {
4187 if (!isset($button->page)) {
4188 // Include js for messaging.
4189 if ($button['buttontype'] === 'togglecontact') {
4190 \core_message\helper::togglecontact_requirejs();
4192 if ($button['buttontype'] === 'message') {
4193 \core_message\helper::messageuser_requirejs();
4195 $image = $this->pix_icon($button['formattedimage'], $button['title'], 'moodle', array(
4196 'class' => 'iconsmall',
4197 'role' => 'presentation'
4199 $image .= html_writer::span($button['title'], 'header-button-title');
4200 } else {
4201 $image = html_writer::empty_tag('img', array(
4202 'src' => $button['formattedimage'],
4203 'role' => 'presentation'
4206 $html .= html_writer::link($button['url'], html_writer::tag('span', $image), $button['linkattributes']);
4208 $html .= html_writer::end_div();
4210 $html .= html_writer::end_div();
4212 return $html;
4216 * Wrapper for header elements.
4218 * @return string HTML to display the main header.
4220 public function full_header() {
4221 global $PAGE;
4223 $header = new stdClass();
4224 $header->settingsmenu = $this->context_header_settings_menu();
4225 $header->contextheader = $this->context_header();
4226 $header->hasnavbar = empty($PAGE->layout_options['nonavbar']);
4227 $header->navbar = $this->navbar();
4228 $header->pageheadingbutton = $this->page_heading_button();
4229 $header->courseheader = $this->course_header();
4230 return $this->render_from_template('core/full_header', $header);
4234 * This is an optional menu that can be added to a layout by a theme. It contains the
4235 * menu for the course administration, only on the course main page.
4237 * @return string
4239 public function context_header_settings_menu() {
4240 $context = $this->page->context;
4241 $menu = new action_menu();
4243 $items = $this->page->navbar->get_items();
4244 $currentnode = end($items);
4246 $showcoursemenu = false;
4247 $showfrontpagemenu = false;
4248 $showusermenu = false;
4250 // We are on the course home page.
4251 if (($context->contextlevel == CONTEXT_COURSE) &&
4252 !empty($currentnode) &&
4253 ($currentnode->type == navigation_node::TYPE_COURSE || $currentnode->type == navigation_node::TYPE_SECTION)) {
4254 $showcoursemenu = true;
4257 $courseformat = course_get_format($this->page->course);
4258 // This is a single activity course format, always show the course menu on the activity main page.
4259 if ($context->contextlevel == CONTEXT_MODULE &&
4260 !$courseformat->has_view_page()) {
4262 $this->page->navigation->initialise();
4263 $activenode = $this->page->navigation->find_active_node();
4264 // If the settings menu has been forced then show the menu.
4265 if ($this->page->is_settings_menu_forced()) {
4266 $showcoursemenu = true;
4267 } else if (!empty($activenode) && ($activenode->type == navigation_node::TYPE_ACTIVITY ||
4268 $activenode->type == navigation_node::TYPE_RESOURCE)) {
4270 // We only want to show the menu on the first page of the activity. This means
4271 // the breadcrumb has no additional nodes.
4272 if ($currentnode && ($currentnode->key == $activenode->key && $currentnode->type == $activenode->type)) {
4273 $showcoursemenu = true;
4278 // This is the site front page.
4279 if ($context->contextlevel == CONTEXT_COURSE &&
4280 !empty($currentnode) &&
4281 $currentnode->key === 'home') {
4282 $showfrontpagemenu = true;
4285 // This is the user profile page.
4286 if ($context->contextlevel == CONTEXT_USER &&
4287 !empty($currentnode) &&
4288 ($currentnode->key === 'myprofile')) {
4289 $showusermenu = true;
4292 if ($showfrontpagemenu) {
4293 $settingsnode = $this->page->settingsnav->find('frontpage', navigation_node::TYPE_SETTING);
4294 if ($settingsnode) {
4295 // Build an action menu based on the visible nodes from this navigation tree.
4296 $skipped = $this->build_action_menu_from_navigation($menu, $settingsnode, false, true);
4298 // We only add a list to the full settings menu if we didn't include every node in the short menu.
4299 if ($skipped) {
4300 $text = get_string('morenavigationlinks');
4301 $url = new moodle_url('/course/admin.php', array('courseid' => $this->page->course->id));
4302 $link = new action_link($url, $text, null, null, new pix_icon('t/edit', $text));
4303 $menu->add_secondary_action($link);
4306 } else if ($showcoursemenu) {
4307 $settingsnode = $this->page->settingsnav->find('courseadmin', navigation_node::TYPE_COURSE);
4308 if ($settingsnode) {
4309 // Build an action menu based on the visible nodes from this navigation tree.
4310 $skipped = $this->build_action_menu_from_navigation($menu, $settingsnode, false, true);
4312 // We only add a list to the full settings menu if we didn't include every node in the short menu.
4313 if ($skipped) {
4314 $text = get_string('morenavigationlinks');
4315 $url = new moodle_url('/course/admin.php', array('courseid' => $this->page->course->id));
4316 $link = new action_link($url, $text, null, null, new pix_icon('t/edit', $text));
4317 $menu->add_secondary_action($link);
4320 } else if ($showusermenu) {
4321 // Get the course admin node from the settings navigation.
4322 $settingsnode = $this->page->settingsnav->find('useraccount', navigation_node::TYPE_CONTAINER);
4323 if ($settingsnode) {
4324 // Build an action menu based on the visible nodes from this navigation tree.
4325 $this->build_action_menu_from_navigation($menu, $settingsnode);
4329 return $this->render($menu);
4333 * Take a node in the nav tree and make an action menu out of it.
4334 * The links are injected in the action menu.
4336 * @param action_menu $menu
4337 * @param navigation_node $node
4338 * @param boolean $indent
4339 * @param boolean $onlytopleafnodes
4340 * @return boolean nodesskipped - True if nodes were skipped in building the menu
4342 protected function build_action_menu_from_navigation(action_menu $menu,
4343 navigation_node $node,
4344 $indent = false,
4345 $onlytopleafnodes = false) {
4346 $skipped = false;
4347 // Build an action menu based on the visible nodes from this navigation tree.
4348 foreach ($node->children as $menuitem) {
4349 if ($menuitem->display) {
4350 if ($onlytopleafnodes && $menuitem->children->count()) {
4351 $skipped = true;
4352 continue;
4354 if ($menuitem->action) {
4355 if ($menuitem->action instanceof action_link) {
4356 $link = $menuitem->action;
4357 // Give preference to setting icon over action icon.
4358 if (!empty($menuitem->icon)) {
4359 $link->icon = $menuitem->icon;
4361 } else {
4362 $link = new action_link($menuitem->action, $menuitem->text, null, null, $menuitem->icon);
4364 } else {
4365 if ($onlytopleafnodes) {
4366 $skipped = true;
4367 continue;
4369 $link = new action_link(new moodle_url('#'), $menuitem->text, null, ['disabled' => true], $menuitem->icon);
4371 if ($indent) {
4372 $link->add_class('ml-4');
4374 if (!empty($menuitem->classes)) {
4375 $link->add_class(implode(" ", $menuitem->classes));
4378 $menu->add_secondary_action($link);
4379 $skipped = $skipped || $this->build_action_menu_from_navigation($menu, $menuitem, true);
4382 return $skipped;
4386 * This is an optional menu that can be added to a layout by a theme. It contains the
4387 * menu for the most specific thing from the settings block. E.g. Module administration.
4389 * @return string
4391 public function region_main_settings_menu() {
4392 $context = $this->page->context;
4393 $menu = new action_menu();
4395 if ($context->contextlevel == CONTEXT_MODULE) {
4397 $this->page->navigation->initialise();
4398 $node = $this->page->navigation->find_active_node();
4399 $buildmenu = false;
4400 // If the settings menu has been forced then show the menu.
4401 if ($this->page->is_settings_menu_forced()) {
4402 $buildmenu = true;
4403 } else if (!empty($node) && ($node->type == navigation_node::TYPE_ACTIVITY ||
4404 $node->type == navigation_node::TYPE_RESOURCE)) {
4406 $items = $this->page->navbar->get_items();
4407 $navbarnode = end($items);
4408 // We only want to show the menu on the first page of the activity. This means
4409 // the breadcrumb has no additional nodes.
4410 if ($navbarnode && ($navbarnode->key === $node->key && $navbarnode->type == $node->type)) {
4411 $buildmenu = true;
4414 if ($buildmenu) {
4415 // Get the course admin node from the settings navigation.
4416 $node = $this->page->settingsnav->find('modulesettings', navigation_node::TYPE_SETTING);
4417 if ($node) {
4418 // Build an action menu based on the visible nodes from this navigation tree.
4419 $this->build_action_menu_from_navigation($menu, $node);
4423 } else if ($context->contextlevel == CONTEXT_COURSECAT) {
4424 // For course category context, show category settings menu, if we're on the course category page.
4425 if ($this->page->pagetype === 'course-index-category') {
4426 $node = $this->page->settingsnav->find('categorysettings', navigation_node::TYPE_CONTAINER);
4427 if ($node) {
4428 // Build an action menu based on the visible nodes from this navigation tree.
4429 $this->build_action_menu_from_navigation($menu, $node);
4433 } else {
4434 $items = $this->page->navbar->get_items();
4435 $navbarnode = end($items);
4437 if ($navbarnode && ($navbarnode->key === 'participants')) {
4438 $node = $this->page->settingsnav->find('users', navigation_node::TYPE_CONTAINER);
4439 if ($node) {
4440 // Build an action menu based on the visible nodes from this navigation tree.
4441 $this->build_action_menu_from_navigation($menu, $node);
4446 return $this->render($menu);
4450 * Displays the list of tags associated with an entry
4452 * @param array $tags list of instances of core_tag or stdClass
4453 * @param string $label label to display in front, by default 'Tags' (get_string('tags')), set to null
4454 * to use default, set to '' (empty string) to omit the label completely
4455 * @param string $classes additional classes for the enclosing div element
4456 * @param int $limit limit the number of tags to display, if size of $tags is more than this limit the "more" link
4457 * will be appended to the end, JS will toggle the rest of the tags
4458 * @param context $pagecontext specify if needed to overwrite the current page context for the view tag link
4459 * @return string
4461 public function tag_list($tags, $label = null, $classes = '', $limit = 10, $pagecontext = null) {
4462 $list = new \core_tag\output\taglist($tags, $label, $classes, $limit, $pagecontext);
4463 return $this->render_from_template('core_tag/taglist', $list->export_for_template($this));
4467 * Renders element for inline editing of any value
4469 * @param \core\output\inplace_editable $element
4470 * @return string
4472 public function render_inplace_editable(\core\output\inplace_editable $element) {
4473 return $this->render_from_template('core/inplace_editable', $element->export_for_template($this));
4477 * Renders a bar chart.
4479 * @param \core\chart_bar $chart The chart.
4480 * @return string.
4482 public function render_chart_bar(\core\chart_bar $chart) {
4483 return $this->render_chart($chart);
4487 * Renders a line chart.
4489 * @param \core\chart_line $chart The chart.
4490 * @return string.
4492 public function render_chart_line(\core\chart_line $chart) {
4493 return $this->render_chart($chart);
4497 * Renders a pie chart.
4499 * @param \core\chart_pie $chart The chart.
4500 * @return string.
4502 public function render_chart_pie(\core\chart_pie $chart) {
4503 return $this->render_chart($chart);
4507 * Renders a chart.
4509 * @param \core\chart_base $chart The chart.
4510 * @param bool $withtable Whether to include a data table with the chart.
4511 * @return string.
4513 public function render_chart(\core\chart_base $chart, $withtable = true) {
4514 $chartdata = json_encode($chart);
4515 return $this->render_from_template('core/chart', (object) [
4516 'chartdata' => $chartdata,
4517 'withtable' => $withtable
4522 * Renders the login form.
4524 * @param \core_auth\output\login $form The renderable.
4525 * @return string
4527 public function render_login(\core_auth\output\login $form) {
4528 global $CFG, $SITE;
4530 $context = $form->export_for_template($this);
4532 // Override because rendering is not supported in template yet.
4533 if ($CFG->rememberusername == 0) {
4534 $context->cookieshelpiconformatted = $this->help_icon('cookiesenabledonlysession');
4535 } else {
4536 $context->cookieshelpiconformatted = $this->help_icon('cookiesenabled');
4538 $context->errorformatted = $this->error_text($context->error);
4539 $url = $this->get_logo_url();
4540 if ($url) {
4541 $url = $url->out(false);
4543 $context->logourl = $url;
4544 $context->sitename = format_string($SITE->fullname, true,
4545 ['context' => context_course::instance(SITEID), "escape" => false]);
4547 return $this->render_from_template('core/loginform', $context);
4551 * Renders an mform element from a template.
4553 * @param HTML_QuickForm_element $element element
4554 * @param bool $required if input is required field
4555 * @param bool $advanced if input is an advanced field
4556 * @param string $error error message to display
4557 * @param bool $ingroup True if this element is rendered as part of a group
4558 * @return mixed string|bool
4560 public function mform_element($element, $required, $advanced, $error, $ingroup) {
4561 $templatename = 'core_form/element-' . $element->getType();
4562 if ($ingroup) {
4563 $templatename .= "-inline";
4565 try {
4566 // We call this to generate a file not found exception if there is no template.
4567 // We don't want to call export_for_template if there is no template.
4568 core\output\mustache_template_finder::get_template_filepath($templatename);
4570 if ($element instanceof templatable) {
4571 $elementcontext = $element->export_for_template($this);
4573 $helpbutton = '';
4574 if (method_exists($element, 'getHelpButton')) {
4575 $helpbutton = $element->getHelpButton();
4577 $label = $element->getLabel();
4578 $text = '';
4579 if (method_exists($element, 'getText')) {
4580 // There currently exists code that adds a form element with an empty label.
4581 // If this is the case then set the label to the description.
4582 if (empty($label)) {
4583 $label = $element->getText();
4584 } else {
4585 $text = $element->getText();
4589 // Generate the form element wrapper ids and names to pass to the template.
4590 // This differs between group and non-group elements.
4591 if ($element->getType() === 'group') {
4592 // Group element.
4593 // The id will be something like 'fgroup_id_NAME'. E.g. fgroup_id_mygroup.
4594 $elementcontext['wrapperid'] = $elementcontext['id'];
4596 // Ensure group elements pass through the group name as the element name so the id_error_{{element.name}} is
4597 // properly set in the template.
4598 $elementcontext['name'] = $elementcontext['groupname'];
4599 } else {
4600 // Non grouped element.
4601 // Creates an id like 'fitem_id_NAME'. E.g. fitem_id_mytextelement.
4602 $elementcontext['wrapperid'] = 'fitem_' . $elementcontext['id'];
4605 $context = array(
4606 'element' => $elementcontext,
4607 'label' => $label,
4608 'text' => $text,
4609 'required' => $required,
4610 'advanced' => $advanced,
4611 'helpbutton' => $helpbutton,
4612 'error' => $error
4614 return $this->render_from_template($templatename, $context);
4616 } catch (Exception $e) {
4617 // No template for this element.
4618 return false;
4623 * Render the login signup form into a nice template for the theme.
4625 * @param mform $form
4626 * @return string
4628 public function render_login_signup_form($form) {
4629 global $SITE;
4631 $context = $form->export_for_template($this);
4632 $url = $this->get_logo_url();
4633 if ($url) {
4634 $url = $url->out(false);
4636 $context['logourl'] = $url;
4637 $context['sitename'] = format_string($SITE->fullname, true,
4638 ['context' => context_course::instance(SITEID), "escape" => false]);
4640 return $this->render_from_template('core/signup_form_layout', $context);
4644 * Render the verify age and location page into a nice template for the theme.
4646 * @param \core_auth\output\verify_age_location_page $page The renderable
4647 * @return string
4649 protected function render_verify_age_location_page($page) {
4650 $context = $page->export_for_template($this);
4652 return $this->render_from_template('core/auth_verify_age_location_page', $context);
4656 * Render the digital minor contact information page into a nice template for the theme.
4658 * @param \core_auth\output\digital_minor_page $page The renderable
4659 * @return string
4661 protected function render_digital_minor_page($page) {
4662 $context = $page->export_for_template($this);
4664 return $this->render_from_template('core/auth_digital_minor_page', $context);
4668 * Renders a progress bar.
4670 * Do not use $OUTPUT->render($bar), instead use progress_bar::create().
4672 * @param progress_bar $bar The bar.
4673 * @return string HTML fragment
4675 public function render_progress_bar(progress_bar $bar) {
4676 global $PAGE;
4677 $data = $bar->export_for_template($this);
4678 return $this->render_from_template('core/progress_bar', $data);
4683 * A renderer that generates output for command-line scripts.
4685 * The implementation of this renderer is probably incomplete.
4687 * @copyright 2009 Tim Hunt
4688 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4689 * @since Moodle 2.0
4690 * @package core
4691 * @category output
4693 class core_renderer_cli extends core_renderer {
4696 * Returns the page header.
4698 * @return string HTML fragment
4700 public function header() {
4701 return $this->page->heading . "\n";
4705 * Returns a template fragment representing a Heading.
4707 * @param string $text The text of the heading
4708 * @param int $level The level of importance of the heading
4709 * @param string $classes A space-separated list of CSS classes
4710 * @param string $id An optional ID
4711 * @return string A template fragment for a heading
4713 public function heading($text, $level = 2, $classes = 'main', $id = null) {
4714 $text .= "\n";
4715 switch ($level) {
4716 case 1:
4717 return '=>' . $text;
4718 case 2:
4719 return '-->' . $text;
4720 default:
4721 return $text;
4726 * Returns a template fragment representing a fatal error.
4728 * @param string $message The message to output
4729 * @param string $moreinfourl URL where more info can be found about the error
4730 * @param string $link Link for the Continue button
4731 * @param array $backtrace The execution backtrace
4732 * @param string $debuginfo Debugging information
4733 * @return string A template fragment for a fatal error
4735 public function fatal_error($message, $moreinfourl, $link, $backtrace, $debuginfo = null, $errorcode = "") {
4736 global $CFG;
4738 $output = "!!! $message !!!\n";
4740 if ($CFG->debugdeveloper) {
4741 if (!empty($debuginfo)) {
4742 $output .= $this->notification($debuginfo, 'notifytiny');
4744 if (!empty($backtrace)) {
4745 $output .= $this->notification('Stack trace: ' . format_backtrace($backtrace, true), 'notifytiny');
4749 return $output;
4753 * Returns a template fragment representing a notification.
4755 * @param string $message The message to print out.
4756 * @param string $type The type of notification. See constants on \core\output\notification.
4757 * @return string A template fragment for a notification
4759 public function notification($message, $type = null) {
4760 $message = clean_text($message);
4761 if ($type === 'notifysuccess' || $type === 'success') {
4762 return "++ $message ++\n";
4764 return "!! $message !!\n";
4768 * There is no footer for a cli request, however we must override the
4769 * footer method to prevent the default footer.
4771 public function footer() {}
4774 * Render a notification (that is, a status message about something that has
4775 * just happened).
4777 * @param \core\output\notification $notification the notification to print out
4778 * @return string plain text output
4780 public function render_notification(\core\output\notification $notification) {
4781 return $this->notification($notification->get_message(), $notification->get_message_type());
4787 * A renderer that generates output for ajax scripts.
4789 * This renderer prevents accidental sends back only json
4790 * encoded error messages, all other output is ignored.
4792 * @copyright 2010 Petr Skoda
4793 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4794 * @since Moodle 2.0
4795 * @package core
4796 * @category output
4798 class core_renderer_ajax extends core_renderer {
4801 * Returns a template fragment representing a fatal error.
4803 * @param string $message The message to output
4804 * @param string $moreinfourl URL where more info can be found about the error
4805 * @param string $link Link for the Continue button
4806 * @param array $backtrace The execution backtrace
4807 * @param string $debuginfo Debugging information
4808 * @return string A template fragment for a fatal error
4810 public function fatal_error($message, $moreinfourl, $link, $backtrace, $debuginfo = null, $errorcode = "") {
4811 global $CFG;
4813 $this->page->set_context(null); // ugly hack - make sure page context is set to something, we do not want bogus warnings here
4815 $e = new stdClass();
4816 $e->error = $message;
4817 $e->errorcode = $errorcode;
4818 $e->stacktrace = NULL;
4819 $e->debuginfo = NULL;
4820 $e->reproductionlink = NULL;
4821 if (!empty($CFG->debug) and $CFG->debug >= DEBUG_DEVELOPER) {
4822 $link = (string) $link;
4823 if ($link) {
4824 $e->reproductionlink = $link;
4826 if (!empty($debuginfo)) {
4827 $e->debuginfo = $debuginfo;
4829 if (!empty($backtrace)) {
4830 $e->stacktrace = format_backtrace($backtrace, true);
4833 $this->header();
4834 return json_encode($e);
4838 * Used to display a notification.
4839 * For the AJAX notifications are discarded.
4841 * @param string $message The message to print out.
4842 * @param string $type The type of notification. See constants on \core\output\notification.
4844 public function notification($message, $type = null) {}
4847 * Used to display a redirection message.
4848 * AJAX redirections should not occur and as such redirection messages
4849 * are discarded.
4851 * @param moodle_url|string $encodedurl
4852 * @param string $message
4853 * @param int $delay
4854 * @param bool $debugdisableredirect
4855 * @param string $messagetype The type of notification to show the message in.
4856 * See constants on \core\output\notification.
4858 public function redirect_message($encodedurl, $message, $delay, $debugdisableredirect,
4859 $messagetype = \core\output\notification::NOTIFY_INFO) {}
4862 * Prepares the start of an AJAX output.
4864 public function header() {
4865 // unfortunately YUI iframe upload does not support application/json
4866 if (!empty($_FILES)) {
4867 @header('Content-type: text/plain; charset=utf-8');
4868 if (!core_useragent::supports_json_contenttype()) {
4869 @header('X-Content-Type-Options: nosniff');
4871 } else if (!core_useragent::supports_json_contenttype()) {
4872 @header('Content-type: text/plain; charset=utf-8');
4873 @header('X-Content-Type-Options: nosniff');
4874 } else {
4875 @header('Content-type: application/json; charset=utf-8');
4878 // Headers to make it not cacheable and json
4879 @header('Cache-Control: no-store, no-cache, must-revalidate');
4880 @header('Cache-Control: post-check=0, pre-check=0', false);
4881 @header('Pragma: no-cache');
4882 @header('Expires: Mon, 20 Aug 1969 09:23:00 GMT');
4883 @header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
4884 @header('Accept-Ranges: none');
4888 * There is no footer for an AJAX request, however we must override the
4889 * footer method to prevent the default footer.
4891 public function footer() {}
4894 * No need for headers in an AJAX request... this should never happen.
4895 * @param string $text
4896 * @param int $level
4897 * @param string $classes
4898 * @param string $id
4900 public function heading($text, $level = 2, $classes = 'main', $id = null) {}
4906 * The maintenance renderer.
4908 * The purpose of this renderer is to block out the core renderer methods that are not usable when the site
4909 * is running a maintenance related task.
4910 * It must always extend the core_renderer as we switch from the core_renderer to this renderer in a couple of places.
4912 * @since Moodle 2.6
4913 * @package core
4914 * @category output
4915 * @copyright 2013 Sam Hemelryk
4916 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4918 class core_renderer_maintenance extends core_renderer {
4921 * Initialises the renderer instance.
4923 * @param moodle_page $page
4924 * @param string $target
4925 * @throws coding_exception
4927 public function __construct(moodle_page $page, $target) {
4928 if ($target !== RENDERER_TARGET_MAINTENANCE || $page->pagelayout !== 'maintenance') {
4929 throw new coding_exception('Invalid request for the maintenance renderer.');
4931 parent::__construct($page, $target);
4935 * Does nothing. The maintenance renderer cannot produce blocks.
4937 * @param block_contents $bc
4938 * @param string $region
4939 * @return string
4941 public function block(block_contents $bc, $region) {
4942 return '';
4946 * Does nothing. The maintenance renderer cannot produce blocks.
4948 * @param string $region
4949 * @param array $classes
4950 * @param string $tag
4951 * @return string
4953 public function blocks($region, $classes = array(), $tag = 'aside') {
4954 return '';
4958 * Does nothing. The maintenance renderer cannot produce blocks.
4960 * @param string $region
4961 * @return string
4963 public function blocks_for_region($region) {
4964 return '';
4968 * Does nothing. The maintenance renderer cannot produce a course content header.
4970 * @param bool $onlyifnotcalledbefore
4971 * @return string
4973 public function course_content_header($onlyifnotcalledbefore = false) {
4974 return '';
4978 * Does nothing. The maintenance renderer cannot produce a course content footer.
4980 * @param bool $onlyifnotcalledbefore
4981 * @return string
4983 public function course_content_footer($onlyifnotcalledbefore = false) {
4984 return '';
4988 * Does nothing. The maintenance renderer cannot produce a course header.
4990 * @return string
4992 public function course_header() {
4993 return '';
4997 * Does nothing. The maintenance renderer cannot produce a course footer.
4999 * @return string
5001 public function course_footer() {
5002 return '';
5006 * Does nothing. The maintenance renderer cannot produce a custom menu.
5008 * @param string $custommenuitems
5009 * @return string
5011 public function custom_menu($custommenuitems = '') {
5012 return '';
5016 * Does nothing. The maintenance renderer cannot produce a file picker.
5018 * @param array $options
5019 * @return string
5021 public function file_picker($options) {
5022 return '';
5026 * Does nothing. The maintenance renderer cannot produce and HTML file tree.
5028 * @param array $dir
5029 * @return string
5031 public function htmllize_file_tree($dir) {
5032 return '';
5037 * Overridden confirm message for upgrades.
5039 * @param string $message The question to ask the user
5040 * @param single_button|moodle_url|string $continue The single_button component representing the Continue answer.
5041 * @param single_button|moodle_url|string $cancel The single_button component representing the Cancel answer.
5042 * @return string HTML fragment
5044 public function confirm($message, $continue, $cancel) {
5045 // We need plain styling of confirm boxes on upgrade because we don't know which stylesheet we have (it could be
5046 // from any previous version of Moodle).
5047 if ($continue instanceof single_button) {
5048 $continue->primary = true;
5049 } else if (is_string($continue)) {
5050 $continue = new single_button(new moodle_url($continue), get_string('continue'), 'post', true);
5051 } else if ($continue instanceof moodle_url) {
5052 $continue = new single_button($continue, get_string('continue'), 'post', true);
5053 } else {
5054 throw new coding_exception('The continue param to $OUTPUT->confirm() must be either a URL' .
5055 ' (string/moodle_url) or a single_button instance.');
5058 if ($cancel instanceof single_button) {
5059 $output = '';
5060 } else if (is_string($cancel)) {
5061 $cancel = new single_button(new moodle_url($cancel), get_string('cancel'), 'get');
5062 } else if ($cancel instanceof moodle_url) {
5063 $cancel = new single_button($cancel, get_string('cancel'), 'get');
5064 } else {
5065 throw new coding_exception('The cancel param to $OUTPUT->confirm() must be either a URL' .
5066 ' (string/moodle_url) or a single_button instance.');
5069 $output = $this->box_start('generalbox', 'notice');
5070 $output .= html_writer::tag('h4', get_string('confirm'));
5071 $output .= html_writer::tag('p', $message);
5072 $output .= html_writer::tag('div', $this->render($continue) . $this->render($cancel), array('class' => 'buttons'));
5073 $output .= $this->box_end();
5074 return $output;
5078 * Does nothing. The maintenance renderer does not support JS.
5080 * @param block_contents $bc
5082 public function init_block_hider_js(block_contents $bc) {
5083 // Does nothing.
5087 * Does nothing. The maintenance renderer cannot produce language menus.
5089 * @return string
5091 public function lang_menu() {
5092 return '';
5096 * Does nothing. The maintenance renderer has no need for login information.
5098 * @param null $withlinks
5099 * @return string
5101 public function login_info($withlinks = null) {
5102 return '';
5106 * Secure login info.
5108 * @return string
5110 public function secure_login_info() {
5111 return $this->login_info(false);
5115 * Does nothing. The maintenance renderer cannot produce user pictures.
5117 * @param stdClass $user
5118 * @param array $options
5119 * @return string
5121 public function user_picture(stdClass $user, array $options = null) {
5122 return '';