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