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