PageRenderTime 66ms CodeModel.GetById 24ms RepoModel.GetById 0ms app.codeStats 1ms

/lib/outputrenderers.php

https://github.com/dongsheng/moodle
PHP | 5473 lines | 2923 code | 563 blank | 1987 comment | 611 complexity | e1717d9a56c505af49a5c0f2c2e92c7a MD5 | raw file
Possible License(s): BSD-3-Clause, MIT, GPL-3.0, Apache-2.0, LGPL-2.1

Large files files are truncated, but you can click here to view the full file

  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. * Classes for rendering HTML output for Moodle.
  18. *
  19. * Please see {@link http://docs.moodle.org/en/Developement:How_Moodle_outputs_HTML}
  20. * for an overview.
  21. *
  22. * Included in this file are the primary renderer classes:
  23. * - renderer_base: The renderer outline class that all renderers
  24. * should inherit from.
  25. * - core_renderer: The standard HTML renderer.
  26. * - core_renderer_cli: An adaption of the standard renderer for CLI scripts.
  27. * - core_renderer_ajax: An adaption of the standard renderer for AJAX scripts.
  28. * - plugin_renderer_base: A renderer class that should be extended by all
  29. * plugin renderers.
  30. *
  31. * @package core
  32. * @category output
  33. * @copyright 2009 Tim Hunt
  34. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  35. */
  36. use core_completion\cm_completion_details;
  37. use core_course\output\activity_information;
  38. defined('MOODLE_INTERNAL') || die();
  39. /**
  40. * Simple base class for Moodle renderers.
  41. *
  42. * Tracks the xhtml_container_stack to use, which is passed in in the constructor.
  43. *
  44. * Also has methods to facilitate generating HTML output.
  45. *
  46. * @copyright 2009 Tim Hunt
  47. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  48. * @since Moodle 2.0
  49. * @package core
  50. * @category output
  51. */
  52. class renderer_base {
  53. /**
  54. * @var xhtml_container_stack The xhtml_container_stack to use.
  55. */
  56. protected $opencontainers;
  57. /**
  58. * @var moodle_page The Moodle page the renderer has been created to assist with.
  59. */
  60. protected $page;
  61. /**
  62. * @var string The requested rendering target.
  63. */
  64. protected $target;
  65. /**
  66. * @var Mustache_Engine $mustache The mustache template compiler
  67. */
  68. private $mustache;
  69. /**
  70. * Return an instance of the mustache class.
  71. *
  72. * @since 2.9
  73. * @return Mustache_Engine
  74. */
  75. protected function get_mustache() {
  76. global $CFG;
  77. if ($this->mustache === null) {
  78. require_once("{$CFG->libdir}/filelib.php");
  79. $themename = $this->page->theme->name;
  80. $themerev = theme_get_revision();
  81. // Create new localcache directory.
  82. $cachedir = make_localcache_directory("mustache/$themerev/$themename");
  83. // Remove old localcache directories.
  84. $mustachecachedirs = glob("{$CFG->localcachedir}/mustache/*", GLOB_ONLYDIR);
  85. foreach ($mustachecachedirs as $localcachedir) {
  86. $cachedrev = [];
  87. preg_match("/\/mustache\/([0-9]+)$/", $localcachedir, $cachedrev);
  88. $cachedrev = isset($cachedrev[1]) ? intval($cachedrev[1]) : 0;
  89. if ($cachedrev > 0 && $cachedrev < $themerev) {
  90. fulldelete($localcachedir);
  91. }
  92. }
  93. $loader = new \core\output\mustache_filesystem_loader();
  94. $stringhelper = new \core\output\mustache_string_helper();
  95. $cleanstringhelper = new \core\output\mustache_clean_string_helper();
  96. $quotehelper = new \core\output\mustache_quote_helper();
  97. $jshelper = new \core\output\mustache_javascript_helper($this->page);
  98. $pixhelper = new \core\output\mustache_pix_helper($this);
  99. $shortentexthelper = new \core\output\mustache_shorten_text_helper();
  100. $userdatehelper = new \core\output\mustache_user_date_helper();
  101. // We only expose the variables that are exposed to JS templates.
  102. $safeconfig = $this->page->requires->get_config_for_javascript($this->page, $this);
  103. $helpers = array('config' => $safeconfig,
  104. 'str' => array($stringhelper, 'str'),
  105. 'cleanstr' => array($cleanstringhelper, 'cleanstr'),
  106. 'quote' => array($quotehelper, 'quote'),
  107. 'js' => array($jshelper, 'help'),
  108. 'pix' => array($pixhelper, 'pix'),
  109. 'shortentext' => array($shortentexthelper, 'shorten'),
  110. 'userdate' => array($userdatehelper, 'transform'),
  111. );
  112. $this->mustache = new \core\output\mustache_engine(array(
  113. 'cache' => $cachedir,
  114. 'escape' => 's',
  115. 'loader' => $loader,
  116. 'helpers' => $helpers,
  117. 'pragmas' => [Mustache_Engine::PRAGMA_BLOCKS],
  118. // Don't allow the JavaScript helper to be executed from within another
  119. // helper. If it's allowed it can be used by users to inject malicious
  120. // JS into the page.
  121. 'disallowednestedhelpers' => ['js']));
  122. }
  123. return $this->mustache;
  124. }
  125. /**
  126. * Constructor
  127. *
  128. * The constructor takes two arguments. The first is the page that the renderer
  129. * has been created to assist with, and the second is the target.
  130. * The target is an additional identifier that can be used to load different
  131. * renderers for different options.
  132. *
  133. * @param moodle_page $page the page we are doing output for.
  134. * @param string $target one of rendering target constants
  135. */
  136. public function __construct(moodle_page $page, $target) {
  137. $this->opencontainers = $page->opencontainers;
  138. $this->page = $page;
  139. $this->target = $target;
  140. }
  141. /**
  142. * Renders a template by name with the given context.
  143. *
  144. * The provided data needs to be array/stdClass made up of only simple types.
  145. * Simple types are array,stdClass,bool,int,float,string
  146. *
  147. * @since 2.9
  148. * @param array|stdClass $context Context containing data for the template.
  149. * @return string|boolean
  150. */
  151. public function render_from_template($templatename, $context) {
  152. static $templatecache = array();
  153. $mustache = $this->get_mustache();
  154. try {
  155. // Grab a copy of the existing helper to be restored later.
  156. $uniqidhelper = $mustache->getHelper('uniqid');
  157. } catch (Mustache_Exception_UnknownHelperException $e) {
  158. // Helper doesn't exist.
  159. $uniqidhelper = null;
  160. }
  161. // Provide 1 random value that will not change within a template
  162. // but will be different from template to template. This is useful for
  163. // e.g. aria attributes that only work with id attributes and must be
  164. // unique in a page.
  165. $mustache->addHelper('uniqid', new \core\output\mustache_uniqid_helper());
  166. if (isset($templatecache[$templatename])) {
  167. $template = $templatecache[$templatename];
  168. } else {
  169. try {
  170. $template = $mustache->loadTemplate($templatename);
  171. $templatecache[$templatename] = $template;
  172. } catch (Mustache_Exception_UnknownTemplateException $e) {
  173. throw new moodle_exception('Unknown template: ' . $templatename);
  174. }
  175. }
  176. $renderedtemplate = trim($template->render($context));
  177. // If we had an existing uniqid helper then we need to restore it to allow
  178. // handle nested calls of render_from_template.
  179. if ($uniqidhelper) {
  180. $mustache->addHelper('uniqid', $uniqidhelper);
  181. }
  182. return $renderedtemplate;
  183. }
  184. /**
  185. * Returns rendered widget.
  186. *
  187. * The provided widget needs to be an object that extends the renderable
  188. * interface.
  189. * If will then be rendered by a method based upon the classname for the widget.
  190. * For instance a widget of class `crazywidget` will be rendered by a protected
  191. * render_crazywidget method of this renderer.
  192. * If no render_crazywidget method exists and crazywidget implements templatable,
  193. * look for the 'crazywidget' template in the same component and render that.
  194. *
  195. * @param renderable $widget instance with renderable interface
  196. * @return string
  197. */
  198. public function render(renderable $widget) {
  199. $classparts = explode('\\', get_class($widget));
  200. // Strip namespaces.
  201. $classname = array_pop($classparts);
  202. // Remove _renderable suffixes
  203. $classname = preg_replace('/_renderable$/', '', $classname);
  204. $rendermethod = 'render_'.$classname;
  205. if (method_exists($this, $rendermethod)) {
  206. return $this->$rendermethod($widget);
  207. }
  208. if ($widget instanceof templatable) {
  209. $component = array_shift($classparts);
  210. if (!$component) {
  211. $component = 'core';
  212. }
  213. $template = $component . '/' . $classname;
  214. $context = $widget->export_for_template($this);
  215. return $this->render_from_template($template, $context);
  216. }
  217. throw new coding_exception('Can not render widget, renderer method ('.$rendermethod.') not found.');
  218. }
  219. /**
  220. * Adds a JS action for the element with the provided id.
  221. *
  222. * This method adds a JS event for the provided component action to the page
  223. * and then returns the id that the event has been attached to.
  224. * If no id has been provided then a new ID is generated by {@link html_writer::random_id()}
  225. *
  226. * @param component_action $action
  227. * @param string $id
  228. * @return string id of element, either original submitted or random new if not supplied
  229. */
  230. public function add_action_handler(component_action $action, $id = null) {
  231. if (!$id) {
  232. $id = html_writer::random_id($action->event);
  233. }
  234. $this->page->requires->event_handler("#$id", $action->event, $action->jsfunction, $action->jsfunctionargs);
  235. return $id;
  236. }
  237. /**
  238. * Returns true is output has already started, and false if not.
  239. *
  240. * @return boolean true if the header has been printed.
  241. */
  242. public function has_started() {
  243. return $this->page->state >= moodle_page::STATE_IN_BODY;
  244. }
  245. /**
  246. * Given an array or space-separated list of classes, prepares and returns the HTML class attribute value
  247. *
  248. * @param mixed $classes Space-separated string or array of classes
  249. * @return string HTML class attribute value
  250. */
  251. public static function prepare_classes($classes) {
  252. if (is_array($classes)) {
  253. return implode(' ', array_unique($classes));
  254. }
  255. return $classes;
  256. }
  257. /**
  258. * Return the direct URL for an image from the pix folder.
  259. *
  260. * Use this function sparingly and never for icons. For icons use pix_icon or the pix helper in a mustache template.
  261. *
  262. * @deprecated since Moodle 3.3
  263. * @param string $imagename the name of the icon.
  264. * @param string $component specification of one plugin like in get_string()
  265. * @return moodle_url
  266. */
  267. public function pix_url($imagename, $component = 'moodle') {
  268. debugging('pix_url is deprecated. Use image_url for images and pix_icon for icons.', DEBUG_DEVELOPER);
  269. return $this->page->theme->image_url($imagename, $component);
  270. }
  271. /**
  272. * Return the moodle_url for an image.
  273. *
  274. * The exact image location and extension is determined
  275. * automatically by searching for gif|png|jpg|jpeg, please
  276. * note there can not be diferent images with the different
  277. * extension. The imagename is for historical reasons
  278. * a relative path name, it may be changed later for core
  279. * images. It is recommended to not use subdirectories
  280. * in plugin and theme pix directories.
  281. *
  282. * There are three types of images:
  283. * 1/ theme images - stored in theme/mytheme/pix/,
  284. * use component 'theme'
  285. * 2/ core images - stored in /pix/,
  286. * overridden via theme/mytheme/pix_core/
  287. * 3/ plugin images - stored in mod/mymodule/pix,
  288. * overridden via theme/mytheme/pix_plugins/mod/mymodule/,
  289. * example: image_url('comment', 'mod_glossary')
  290. *
  291. * @param string $imagename the pathname of the image
  292. * @param string $component full plugin name (aka component) or 'theme'
  293. * @return moodle_url
  294. */
  295. public function image_url($imagename, $component = 'moodle') {
  296. return $this->page->theme->image_url($imagename, $component);
  297. }
  298. /**
  299. * Return the site's logo URL, if any.
  300. *
  301. * @param int $maxwidth The maximum width, or null when the maximum width does not matter.
  302. * @param int $maxheight The maximum height, or null when the maximum height does not matter.
  303. * @return moodle_url|false
  304. */
  305. public function get_logo_url($maxwidth = null, $maxheight = 200) {
  306. global $CFG;
  307. $logo = get_config('core_admin', 'logo');
  308. if (empty($logo)) {
  309. return false;
  310. }
  311. // 200px high is the default image size which should be displayed at 100px in the page to account for retina displays.
  312. // It's not worth the overhead of detecting and serving 2 different images based on the device.
  313. // Hide the requested size in the file path.
  314. $filepath = ((int) $maxwidth . 'x' . (int) $maxheight) . '/';
  315. // Use $CFG->themerev to prevent browser caching when the file changes.
  316. return moodle_url::make_pluginfile_url(context_system::instance()->id, 'core_admin', 'logo', $filepath,
  317. theme_get_revision(), $logo);
  318. }
  319. /**
  320. * Return the site's compact logo URL, if any.
  321. *
  322. * @param int $maxwidth The maximum width, or null when the maximum width does not matter.
  323. * @param int $maxheight The maximum height, or null when the maximum height does not matter.
  324. * @return moodle_url|false
  325. */
  326. public function get_compact_logo_url($maxwidth = 300, $maxheight = 300) {
  327. global $CFG;
  328. $logo = get_config('core_admin', 'logocompact');
  329. if (empty($logo)) {
  330. return false;
  331. }
  332. // Hide the requested size in the file path.
  333. $filepath = ((int) $maxwidth . 'x' . (int) $maxheight) . '/';
  334. // Use $CFG->themerev to prevent browser caching when the file changes.
  335. return moodle_url::make_pluginfile_url(context_system::instance()->id, 'core_admin', 'logocompact', $filepath,
  336. theme_get_revision(), $logo);
  337. }
  338. /**
  339. * Whether we should display the logo in the navbar.
  340. *
  341. * We will when there are no main logos, and we have compact logo.
  342. *
  343. * @return bool
  344. */
  345. public function should_display_navbar_logo() {
  346. $logo = $this->get_compact_logo_url();
  347. return !empty($logo) && !$this->should_display_main_logo();
  348. }
  349. /**
  350. * Whether we should display the main logo.
  351. *
  352. * @param int $headinglevel The heading level we want to check against.
  353. * @return bool
  354. */
  355. public function should_display_main_logo($headinglevel = 1) {
  356. // Only render the logo if we're on the front page or login page and the we have a logo.
  357. $logo = $this->get_logo_url();
  358. if ($headinglevel == 1 && !empty($logo)) {
  359. if ($this->page->pagelayout == 'frontpage' || $this->page->pagelayout == 'login') {
  360. return true;
  361. }
  362. }
  363. return false;
  364. }
  365. }
  366. /**
  367. * Basis for all plugin renderers.
  368. *
  369. * @copyright Petr Skoda (skodak)
  370. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  371. * @since Moodle 2.0
  372. * @package core
  373. * @category output
  374. */
  375. class plugin_renderer_base extends renderer_base {
  376. /**
  377. * @var renderer_base|core_renderer A reference to the current renderer.
  378. * The renderer provided here will be determined by the page but will in 90%
  379. * of cases by the {@link core_renderer}
  380. */
  381. protected $output;
  382. /**
  383. * Constructor method, calls the parent constructor
  384. *
  385. * @param moodle_page $page
  386. * @param string $target one of rendering target constants
  387. */
  388. public function __construct(moodle_page $page, $target) {
  389. if (empty($target) && $page->pagelayout === 'maintenance') {
  390. // If the page is using the maintenance layout then we're going to force the target to maintenance.
  391. // This way we'll get a special maintenance renderer that is designed to block access to API's that are likely
  392. // unavailable for this page layout.
  393. $target = RENDERER_TARGET_MAINTENANCE;
  394. }
  395. $this->output = $page->get_renderer('core', null, $target);
  396. parent::__construct($page, $target);
  397. }
  398. /**
  399. * Renders the provided widget and returns the HTML to display it.
  400. *
  401. * @param renderable $widget instance with renderable interface
  402. * @return string
  403. */
  404. public function render(renderable $widget) {
  405. $classname = get_class($widget);
  406. // Strip namespaces.
  407. $classname = preg_replace('/^.*\\\/', '', $classname);
  408. // Keep a copy at this point, we may need to look for a deprecated method.
  409. $deprecatedmethod = 'render_'.$classname;
  410. // Remove _renderable suffixes
  411. $classname = preg_replace('/_renderable$/', '', $classname);
  412. $rendermethod = 'render_'.$classname;
  413. if (method_exists($this, $rendermethod)) {
  414. return $this->$rendermethod($widget);
  415. }
  416. if ($rendermethod !== $deprecatedmethod && method_exists($this, $deprecatedmethod)) {
  417. // This is exactly where we don't want to be.
  418. // If you have arrived here you have a renderable component within your plugin that has the name
  419. // blah_renderable, and you have a render method render_blah_renderable on your plugin.
  420. // In 2.8 we revamped output, as part of this change we changed slightly how renderables got rendered
  421. // and the _renderable suffix now gets removed when looking for a render method.
  422. // You need to change your renderers render_blah_renderable to render_blah.
  423. // Until you do this it will not be possible for a theme to override the renderer to override your method.
  424. // Please do it ASAP.
  425. static $debugged = array();
  426. if (!isset($debugged[$deprecatedmethod])) {
  427. debugging(sprintf('Deprecated call. Please rename your renderables render method from %s to %s.',
  428. $deprecatedmethod, $rendermethod), DEBUG_DEVELOPER);
  429. $debugged[$deprecatedmethod] = true;
  430. }
  431. return $this->$deprecatedmethod($widget);
  432. }
  433. // pass to core renderer if method not found here
  434. return $this->output->render($widget);
  435. }
  436. /**
  437. * Magic method used to pass calls otherwise meant for the standard renderer
  438. * to it to ensure we don't go causing unnecessary grief.
  439. *
  440. * @param string $method
  441. * @param array $arguments
  442. * @return mixed
  443. */
  444. public function __call($method, $arguments) {
  445. if (method_exists('renderer_base', $method)) {
  446. throw new coding_exception('Protected method called against '.get_class($this).' :: '.$method);
  447. }
  448. if (method_exists($this->output, $method)) {
  449. return call_user_func_array(array($this->output, $method), $arguments);
  450. } else {
  451. throw new coding_exception('Unknown method called against '.get_class($this).' :: '.$method);
  452. }
  453. }
  454. }
  455. /**
  456. * The standard implementation of the core_renderer interface.
  457. *
  458. * @copyright 2009 Tim Hunt
  459. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  460. * @since Moodle 2.0
  461. * @package core
  462. * @category output
  463. */
  464. class core_renderer extends renderer_base {
  465. /**
  466. * Do NOT use, please use <?php echo $OUTPUT->main_content() ?>
  467. * in layout files instead.
  468. * @deprecated
  469. * @var string used in {@link core_renderer::header()}.
  470. */
  471. const MAIN_CONTENT_TOKEN = '[MAIN CONTENT GOES HERE]';
  472. /**
  473. * @var string Used to pass information from {@link core_renderer::doctype()} to
  474. * {@link core_renderer::standard_head_html()}.
  475. */
  476. protected $contenttype;
  477. /**
  478. * @var string Used by {@link core_renderer::redirect_message()} method to communicate
  479. * with {@link core_renderer::header()}.
  480. */
  481. protected $metarefreshtag = '';
  482. /**
  483. * @var string Unique token for the closing HTML
  484. */
  485. protected $unique_end_html_token;
  486. /**
  487. * @var string Unique token for performance information
  488. */
  489. protected $unique_performance_info_token;
  490. /**
  491. * @var string Unique token for the main content.
  492. */
  493. protected $unique_main_content_token;
  494. /** @var custom_menu_item language The language menu if created */
  495. protected $language = null;
  496. /**
  497. * Constructor
  498. *
  499. * @param moodle_page $page the page we are doing output for.
  500. * @param string $target one of rendering target constants
  501. */
  502. public function __construct(moodle_page $page, $target) {
  503. $this->opencontainers = $page->opencontainers;
  504. $this->page = $page;
  505. $this->target = $target;
  506. $this->unique_end_html_token = '%%ENDHTML-'.sesskey().'%%';
  507. $this->unique_performance_info_token = '%%PERFORMANCEINFO-'.sesskey().'%%';
  508. $this->unique_main_content_token = '[MAIN CONTENT GOES HERE - '.sesskey().']';
  509. }
  510. /**
  511. * Get the DOCTYPE declaration that should be used with this page. Designed to
  512. * be called in theme layout.php files.
  513. *
  514. * @return string the DOCTYPE declaration that should be used.
  515. */
  516. public function doctype() {
  517. if ($this->page->theme->doctype === 'html5') {
  518. $this->contenttype = 'text/html; charset=utf-8';
  519. return "<!DOCTYPE html>\n";
  520. } else if ($this->page->theme->doctype === 'xhtml5') {
  521. $this->contenttype = 'application/xhtml+xml; charset=utf-8';
  522. return "<!DOCTYPE html>\n";
  523. } else {
  524. // legacy xhtml 1.0
  525. $this->contenttype = 'text/html; charset=utf-8';
  526. return ('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">' . "\n");
  527. }
  528. }
  529. /**
  530. * The attributes that should be added to the <html> tag. Designed to
  531. * be called in theme layout.php files.
  532. *
  533. * @return string HTML fragment.
  534. */
  535. public function htmlattributes() {
  536. $return = get_html_lang(true);
  537. $attributes = array();
  538. if ($this->page->theme->doctype !== 'html5') {
  539. $attributes['xmlns'] = 'http://www.w3.org/1999/xhtml';
  540. }
  541. // Give plugins an opportunity to add things like xml namespaces to the html element.
  542. // This function should return an array of html attribute names => values.
  543. $pluginswithfunction = get_plugins_with_function('add_htmlattributes', 'lib.php');
  544. foreach ($pluginswithfunction as $plugins) {
  545. foreach ($plugins as $function) {
  546. $newattrs = $function();
  547. unset($newattrs['dir']);
  548. unset($newattrs['lang']);
  549. unset($newattrs['xmlns']);
  550. unset($newattrs['xml:lang']);
  551. $attributes += $newattrs;
  552. }
  553. }
  554. foreach ($attributes as $key => $val) {
  555. $val = s($val);
  556. $return .= " $key=\"$val\"";
  557. }
  558. return $return;
  559. }
  560. /**
  561. * The standard tags (meta tags, links to stylesheets and JavaScript, etc.)
  562. * that should be included in the <head> tag. Designed to be called in theme
  563. * layout.php files.
  564. *
  565. * @return string HTML fragment.
  566. */
  567. public function standard_head_html() {
  568. global $CFG, $SESSION, $SITE;
  569. // Before we output any content, we need to ensure that certain
  570. // page components are set up.
  571. // Blocks must be set up early as they may require javascript which
  572. // has to be included in the page header before output is created.
  573. foreach ($this->page->blocks->get_regions() as $region) {
  574. $this->page->blocks->ensure_content_created($region, $this);
  575. }
  576. $output = '';
  577. // Give plugins an opportunity to add any head elements. The callback
  578. // must always return a string containing valid html head content.
  579. $pluginswithfunction = get_plugins_with_function('before_standard_html_head', 'lib.php');
  580. foreach ($pluginswithfunction as $plugins) {
  581. foreach ($plugins as $function) {
  582. $output .= $function();
  583. }
  584. }
  585. // Allow a url_rewrite plugin to setup any dynamic head content.
  586. if (isset($CFG->urlrewriteclass) && !isset($CFG->upgraderunning)) {
  587. $class = $CFG->urlrewriteclass;
  588. $output .= $class::html_head_setup();
  589. }
  590. $output .= '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />' . "\n";
  591. $output .= '<meta name="keywords" content="moodle, ' . $this->page->title . '" />' . "\n";
  592. // This is only set by the {@link redirect()} method
  593. $output .= $this->metarefreshtag;
  594. // Check if a periodic refresh delay has been set and make sure we arn't
  595. // already meta refreshing
  596. if ($this->metarefreshtag=='' && $this->page->periodicrefreshdelay!==null) {
  597. $output .= '<meta http-equiv="refresh" content="'.$this->page->periodicrefreshdelay.';url='.$this->page->url->out().'" />';
  598. }
  599. // Set up help link popups for all links with the helptooltip class
  600. $this->page->requires->js_init_call('M.util.help_popups.setup');
  601. $focus = $this->page->focuscontrol;
  602. if (!empty($focus)) {
  603. if (preg_match("#forms\['([a-zA-Z0-9]+)'\].elements\['([a-zA-Z0-9]+)'\]#", $focus, $matches)) {
  604. // This is a horrifically bad way to handle focus but it is passed in
  605. // through messy formslib::moodleform
  606. $this->page->requires->js_function_call('old_onload_focus', array($matches[1], $matches[2]));
  607. } else if (strpos($focus, '.')!==false) {
  608. // Old style of focus, bad way to do it
  609. 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);
  610. $this->page->requires->js_function_call('old_onload_focus', explode('.', $focus, 2));
  611. } else {
  612. // Focus element with given id
  613. $this->page->requires->js_function_call('focuscontrol', array($focus));
  614. }
  615. }
  616. // Get the theme stylesheet - this has to be always first CSS, this loads also styles.css from all plugins;
  617. // any other custom CSS can not be overridden via themes and is highly discouraged
  618. $urls = $this->page->theme->css_urls($this->page);
  619. foreach ($urls as $url) {
  620. $this->page->requires->css_theme($url);
  621. }
  622. // Get the theme javascript head and footer
  623. if ($jsurl = $this->page->theme->javascript_url(true)) {
  624. $this->page->requires->js($jsurl, true);
  625. }
  626. if ($jsurl = $this->page->theme->javascript_url(false)) {
  627. $this->page->requires->js($jsurl);
  628. }
  629. // Get any HTML from the page_requirements_manager.
  630. $output .= $this->page->requires->get_head_code($this->page, $this);
  631. // List alternate versions.
  632. foreach ($this->page->alternateversions as $type => $alt) {
  633. $output .= html_writer::empty_tag('link', array('rel' => 'alternate',
  634. 'type' => $type, 'title' => $alt->title, 'href' => $alt->url));
  635. }
  636. // Add noindex tag if relevant page and setting applied.
  637. $allowindexing = isset($CFG->allowindexing) ? $CFG->allowindexing : 0;
  638. $loginpages = array('login-index', 'login-signup');
  639. if ($allowindexing == 2 || ($allowindexing == 0 && in_array($this->page->pagetype, $loginpages))) {
  640. if (!isset($CFG->additionalhtmlhead)) {
  641. $CFG->additionalhtmlhead = '';
  642. }
  643. $CFG->additionalhtmlhead .= '<meta name="robots" content="noindex" />';
  644. }
  645. if (!empty($CFG->additionalhtmlhead)) {
  646. $output .= "\n".$CFG->additionalhtmlhead;
  647. }
  648. if ($this->page->pagelayout == 'frontpage') {
  649. $summary = s(strip_tags(format_text($SITE->summary, FORMAT_HTML)));
  650. if (!empty($summary)) {
  651. $output .= "<meta name=\"description\" content=\"$summary\" />\n";
  652. }
  653. }
  654. return $output;
  655. }
  656. /**
  657. * The standard tags (typically skip links) that should be output just inside
  658. * the start of the <body> tag. Designed to be called in theme layout.php files.
  659. *
  660. * @return string HTML fragment.
  661. */
  662. public function standard_top_of_body_html() {
  663. global $CFG;
  664. $output = $this->page->requires->get_top_of_body_code($this);
  665. if ($this->page->pagelayout !== 'embedded' && !empty($CFG->additionalhtmltopofbody)) {
  666. $output .= "\n".$CFG->additionalhtmltopofbody;
  667. }
  668. // Give subsystems an opportunity to inject extra html content. The callback
  669. // must always return a string containing valid html.
  670. foreach (\core_component::get_core_subsystems() as $name => $path) {
  671. if ($path) {
  672. $output .= component_callback($name, 'before_standard_top_of_body_html', [], '');
  673. }
  674. }
  675. // Give plugins an opportunity to inject extra html content. The callback
  676. // must always return a string containing valid html.
  677. $pluginswithfunction = get_plugins_with_function('before_standard_top_of_body_html', 'lib.php');
  678. foreach ($pluginswithfunction as $plugins) {
  679. foreach ($plugins as $function) {
  680. $output .= $function();
  681. }
  682. }
  683. $output .= $this->maintenance_warning();
  684. return $output;
  685. }
  686. /**
  687. * Scheduled maintenance warning message.
  688. *
  689. * Note: This is a nasty hack to display maintenance notice, this should be moved
  690. * to some general notification area once we have it.
  691. *
  692. * @return string
  693. */
  694. public function maintenance_warning() {
  695. global $CFG;
  696. $output = '';
  697. if (isset($CFG->maintenance_later) and $CFG->maintenance_later > time()) {
  698. $timeleft = $CFG->maintenance_later - time();
  699. // If timeleft less than 30 sec, set the class on block to error to highlight.
  700. $errorclass = ($timeleft < 30) ? 'alert-error alert-danger' : 'alert-warning';
  701. $output .= $this->box_start($errorclass . ' moodle-has-zindex maintenancewarning m-3 alert');
  702. $a = new stdClass();
  703. $a->hour = (int)($timeleft / 3600);
  704. $a->min = (int)(($timeleft / 60) % 60);
  705. $a->sec = (int)($timeleft % 60);
  706. if ($a->hour > 0) {
  707. $output .= get_string('maintenancemodeisscheduledlong', 'admin', $a);
  708. } else {
  709. $output .= get_string('maintenancemodeisscheduled', 'admin', $a);
  710. }
  711. $output .= $this->box_end();
  712. $this->page->requires->yui_module('moodle-core-maintenancemodetimer', 'M.core.maintenancemodetimer',
  713. array(array('timeleftinsec' => $timeleft)));
  714. $this->page->requires->strings_for_js(
  715. array('maintenancemodeisscheduled', 'maintenancemodeisscheduledlong', 'sitemaintenance'),
  716. 'admin');
  717. }
  718. return $output;
  719. }
  720. /**
  721. * content that should be output in the footer area
  722. * of the page. Designed to be called in theme layout.php files.
  723. *
  724. * @return string HTML fragment.
  725. */
  726. public function standard_footer_html() {
  727. global $CFG, $SCRIPT;
  728. $output = '';
  729. if (during_initial_install()) {
  730. // Debugging info can not work before install is finished,
  731. // in any case we do not want any links during installation!
  732. return $output;
  733. }
  734. // Give plugins an opportunity to add any footer elements.
  735. // The callback must always return a string containing valid html footer content.
  736. $pluginswithfunction = get_plugins_with_function('standard_footer_html', 'lib.php');
  737. foreach ($pluginswithfunction as $plugins) {
  738. foreach ($plugins as $function) {
  739. $output .= $function();
  740. }
  741. }
  742. if (core_userfeedback::can_give_feedback()) {
  743. $output .= html_writer::div(
  744. $this->render_from_template('core/userfeedback_footer_link', ['url' => core_userfeedback::make_link()->out(false)])
  745. );
  746. }
  747. if ($this->page->devicetypeinuse == 'legacy') {
  748. // The legacy theme is in use print the notification
  749. $output .= html_writer::tag('div', get_string('legacythemeinuse'), array('class'=>'legacythemeinuse'));
  750. }
  751. // Get links to switch device types (only shown for users not on a default device)
  752. $output .= $this->theme_switch_links();
  753. return $output;
  754. }
  755. /**
  756. * Performance information and validation links for debugging.
  757. *
  758. * @return string HTML fragment.
  759. */
  760. public function debug_footer_html() {
  761. global $CFG;
  762. $output = '';
  763. if (during_initial_install()) {
  764. // Debugging info can not work before install is finished.
  765. return $output;
  766. }
  767. // This function is normally called from a layout.php file
  768. // but some of the content won't be known until later, so we return a placeholder
  769. // for now. This will be replaced with the real content in the footer.
  770. $output .= $this->unique_performance_info_token;
  771. if (!empty($CFG->debugpageinfo)) {
  772. $output .= '<div class="performanceinfo pageinfo">' . get_string('pageinfodebugsummary', 'core_admin',
  773. $this->page->debug_summary()) . '</div>';
  774. }
  775. if (debugging(null, DEBUG_DEVELOPER) and has_capability('moodle/site:config', context_system::instance())) { // Only in developer mode
  776. // Add link to profiling report if necessary
  777. if (function_exists('profiling_is_running') && profiling_is_running()) {
  778. $txt = get_string('profiledscript', 'admin');
  779. $title = get_string('profiledscriptview', 'admin');
  780. $url = $CFG->wwwroot . '/admin/tool/profiling/index.php?script=' . urlencode($SCRIPT);
  781. $link= '<a title="' . $title . '" href="' . $url . '">' . $txt . '</a>';
  782. $output .= '<div class="profilingfooter">' . $link . '</div>';
  783. }
  784. $purgeurl = new moodle_url('/admin/purgecaches.php', array('confirm' => 1,
  785. 'sesskey' => sesskey(), 'returnurl' => $this->page->url->out_as_local_url(false)));
  786. $output .= '<div class="purgecaches">' .
  787. html_writer::link($purgeurl, get_string('purgecaches', 'admin')) . '</div>';
  788. // Reactive module debug panel.
  789. $output .= $this->render_from_template('core/local/reactive/debugpanel', []);
  790. }
  791. if (!empty($CFG->debugvalidators)) {
  792. // NOTE: this is not a nice hack, $this->page->url is not always accurate and
  793. // $FULLME neither, it is not a bug if it fails. --skodak.
  794. $output .= '<div class="validators"><ul class="list-unstyled ml-1">
  795. <li><a href="http://validator.w3.org/check?verbose=1&amp;ss=1&amp;uri=' . urlencode(qualified_me()) . '">Validate HTML</a></li>
  796. <li><a href="http://www.contentquality.com/mynewtester/cynthia.exe?rptmode=-1&amp;url1=' . urlencode(qualified_me()) . '">Section 508 Check</a></li>
  797. <li><a href="http://www.contentquality.com/mynewtester/cynthia.exe?rptmode=0&amp;warnp2n3e=1&amp;url1=' . urlencode(qualified_me()) . '">WCAG 1 (2,3) Check</a></li>
  798. </ul></div>';
  799. }
  800. return $output;
  801. }
  802. /**
  803. * Returns standard main content placeholder.
  804. * Designed to be called in theme layout.php files.
  805. *
  806. * @return string HTML fragment.
  807. */
  808. public function main_content() {
  809. // This is here because it is the only place we can inject the "main" role over the entire main content area
  810. // without requiring all theme's to manually do it, and without creating yet another thing people need to
  811. // remember in the theme.
  812. // This is an unfortunate hack. DO NO EVER add anything more here.
  813. // DO NOT add classes.
  814. // DO NOT add an id.
  815. return '<div role="main">'.$this->unique_main_content_token.'</div>';
  816. }
  817. /**
  818. * Returns information about an activity.
  819. *
  820. * @param cm_info $cminfo The course module information.
  821. * @param cm_completion_details $completiondetails The completion details for this activity module.
  822. * @param array $activitydates The dates for this activity module.
  823. * @return string the activity information HTML.
  824. * @throws coding_exception
  825. */
  826. public function activity_information(cm_info $cminfo, cm_completion_details $completiondetails, array $activitydates): string {
  827. if (!$completiondetails->has_completion() && empty($activitydates)) {
  828. // No need to render the activity information when there's no completion info and activity dates to show.
  829. return '';
  830. }
  831. $activityinfo = new activity_information($cminfo, $completiondetails, $activitydates);
  832. $renderer = $this->page->get_renderer('core', 'course');
  833. return $renderer->render($activityinfo);
  834. }
  835. /**
  836. * Returns standard navigation between activities in a course.
  837. *
  838. * @return string the navigation HTML.
  839. */
  840. public function activity_navigation() {
  841. // First we should check if we want to add navigation.
  842. $context = $this->page->context;
  843. if (($this->page->pagelayout !== 'incourse' && $this->page->pagelayout !== 'frametop')
  844. || $context->contextlevel != CONTEXT_MODULE) {
  845. return '';
  846. }
  847. // If the activity is in stealth mode, show no links.
  848. if ($this->page->cm->is_stealth()) {
  849. return '';
  850. }
  851. $course = $this->page->cm->get_course();
  852. $courseformat = course_get_format($course);
  853. // If the theme implements course index and the current course format uses course index and the current
  854. // page layout is not 'frametop' (this layout does not support course index), show no links.
  855. if ($this->page->theme->usescourseindex && $courseformat->uses_course_index() &&
  856. $this->page->pagelayout !== 'frametop') {
  857. return '';
  858. }
  859. // Get a list of all the activities in the course.
  860. $modules = get_fast_modinfo($course->id)->get_cms();
  861. // Put the modules into an array in order by the position they are shown in the course.
  862. $mods = [];
  863. $activitylist = [];
  864. foreach ($modules as $module) {
  865. // Only add activities the user can access, aren't in stealth mode and have a url (eg. mod_label does not).
  866. if (!$module->uservisible || $module->is_stealth() || empty($module->url)) {
  867. continue;
  868. }
  869. $mods[$module->id] = $module;
  870. // No need to add the current module to the list for the activity dropdown menu.
  871. if ($module->id == $this->page->cm->id) {
  872. continue;
  873. }
  874. // Module name.
  875. $modname = $module->get_formatted_name();
  876. // Display the hidden text if necessary.
  877. if (!$module->visible) {
  878. $modname .= ' ' . get_string('hiddenwithbrackets');
  879. }
  880. // Module URL.
  881. $linkurl = new moodle_url($module->url, array('forceview' => 1));
  882. // Add module URL (as key) and name (as value) to the activity list array.
  883. $activitylist[$linkurl->out(false)] = $modname;
  884. }
  885. $nummods = count($mods);
  886. // If there is only one mod then do nothing.
  887. if ($nummods == 1) {
  888. return '';
  889. }
  890. // Get an array of just the course module ids used to get the cmid value based on their position in the course.
  891. $modids = array_keys($mods);
  892. // Get the position in the array of the course module we are viewing.
  893. $position = array_search($this->page->cm->id, $modids);
  894. $prevmod = null;
  895. $nextmod = null;
  896. // Check if we have a previous mod to show.
  897. if ($position > 0) {
  898. $prevmod = $mods[$modids[$position - 1]];
  899. }
  900. // Check if we have a next mod to show.
  901. if ($position < ($nummods - 1)) {
  902. $nextmod = $mods[$modids[$position + 1]];
  903. }
  904. $activitynav = new \core_course\output\activity_navigation($prevmod, $nextmod, $activitylist);
  905. $renderer = $this->page->get_renderer('core', 'course');
  906. return $renderer->render($activitynav);
  907. }
  908. /**
  909. * The standard tags (typically script tags that are not needed earlier) that
  910. * should be output after everything else. Designed to be called in theme layout.php files.
  911. *
  912. * @return string HTML fragment.
  913. */
  914. public function standard_end_of_body_html() {
  915. global $CFG;
  916. // This function is normally called from a layout.php file in {@link core_renderer::header()}
  917. // but some of the content won't be known until later, so we return a placeholder
  918. // for now. This will be replaced with the real content in {@link core_renderer::footer()}.
  919. $output = '';
  920. if ($this->page->pagelayout !== 'embedded' && !empty($CFG->additionalhtmlfooter)) {
  921. $output .= "\n".$CFG->additionalhtmlfooter;
  922. }
  923. $output .= $this->unique_end_html_token;
  924. return $output;
  925. }
  926. /**
  927. * The standard HTML that should be output just before the <footer> tag.
  928. * Designed to be called in theme layout.php files.
  929. *
  930. * @return string HTML fragment.
  931. */
  932. public function standard_after_main_region_html() {
  933. global $CFG;
  934. $output = '';
  935. if ($this->page->pagelayout !== 'embedded' && !empty($CFG->additionalhtmlbottomofbody)) {
  936. $output .= "\n".$CFG->additionalhtmlbottomofbody;
  937. }
  938. // Give subsystems an opportunity to inject extra html content. The callback
  939. // must always return a string containing valid html.
  940. foreach (\core_component::get_core_subsystems() as $name => $path) {
  941. if ($path) {
  942. $output .= component_callback($name, 'standard_after_main_region_html', [], '');
  943. }
  944. }
  945. // Give plugins an opportunity to inject extra html content. The callback
  946. // must always return a string containing valid html.
  947. $pluginswithfunction = get_plugins_with_function('standard_after_main_region_html', 'lib.php');
  948. foreach ($pluginswithfunction as $plugins) {
  949. foreach ($plugins as $function) {
  950. $output .= $function();
  951. }
  952. }
  953. return $output;
  954. }
  955. /**
  956. * Return the standard string that says whether you are logged in (and switched
  957. * roles/logged in as another user).
  958. * @param bool $withlinks if false, then don't include any links in the HTML produced.
  959. * If not set, the default is the nologinlinks option from the theme config.php file,
  960. * and if that is not set, then links are included.
  961. * @return string HTML fragment.
  962. */
  963. public function login_info($withlinks = null) {
  964. global $USER, $CFG, $DB, $SESSION;
  965. if (during_initial_install()) {
  966. return '';
  967. }
  968. if (is_null($withlinks)) {
  969. $withlinks = empty($this->page->layout_options['nologinlinks']);
  970. }
  971. $course = $this->page->course;
  972. if (\core\session\manager::is_loggedinas()) {
  973. $realuser = \core\session\manager::get_realuser();
  974. $fullname = fullname($realuser);
  975. if ($withlinks) {
  976. $loginastitle = get_string('loginas');
  977. $realuserinfo = " [<a href=\"$CFG->wwwroot/course/loginas.php?id=$course->id&amp;sesskey=".sesskey()."\"";
  978. $realuserinfo .= "title =\"".$loginastitle."\">$fullname</a>] ";
  979. } else {
  980. $realuserinfo = " [$fullname] ";
  981. }
  982. } else {
  983. $realuserinfo = '';
  984. }
  985. $loginpage = $this->is_login_page();
  986. $loginurl = get_login_url();
  987. if (empty($course->id)) {
  988. // $course->id is not defined during installation
  989. return '';
  990. } else if (isloggedin()) {
  991. $context = context_course::instance($course->id);
  992. $fullname = fullname($USER);
  993. // Since Moodle 2.0 this link always goes to the public profile page (not the course profile page)
  994. if ($withlinks) {
  995. $linktitle = get_string('viewprofile');
  996. $username = "<a href=\"$CFG->wwwroot/user/profile.php?id=$USER->id\" title=\"$linktitle\">$fullname</a>";
  997. } else {
  998. $username = $fullname;
  999. }
  1000. if (is_mnet_remote_user($USER) and $idprovider = $DB->get_record('mnet_host', array('id'=>$USER->mnethostid))) {
  1001. if ($withlinks) {
  1002. $username .= " from <a href=\"{$idprovider->wwwroot}\">{$idprovider->name}</a>";
  1003. } else {
  1004. $username .= " from {$idprovider->name}";
  1005. }
  1006. }
  1007. if (isguestuser()) {
  1008. $loggedinas = $realuserinfo.get_string('loggedinasguest');
  1009. if (!$loginpage && $withlinks) {
  1010. $loggedinas .= " (<a href=\"$loginurl\">".get_string('login').'</a>)';
  1011. }
  1012. } else if (is_role_switched($course->id)) { // Has switched roles
  1013. $rolename = '';
  1014. if ($role = $DB->get_record('role', array('id'=>$USER->access['rsw'][$context->path]))) {
  1015. $rolename = ': '.role_get_name($role, $context);
  1016. }
  1017. $loggedinas = get_string('loggedinas', 'moodle', $username).$rolename;
  1018. if ($withlinks) {
  1019. $url = new moodle_url('/course/switchrole.php', array('id'=>$course->id,'sesskey'=>sesskey(), 'switchrole'=>0, 'returnurl'=>$this->page->url->out_as_local_url(false)));
  1020. $loggedinas .= ' ('.html_writer::tag('a', get_string('switchrolereturn'), array('href' => $url)).')';
  1021. }
  1022. } else {
  1023. $loggedinas = $realuserinfo.get_string('loggedinas', 'moodle', $username);
  1024. if ($withlinks) {
  1025. $loggedinas .= " (<a href=\"$CFG->wwwroot/login/logout.php?sesskey=".sesskey()."\">".get_string('logout').'</a>)';
  1026. }
  1027. }
  1028. } else {
  1029. $loggedinas = get_string('loggedinnot', 'moodle');
  1030. if (!$loginpage && $withlinks) {
  1031. $loggedinas .= " (<a href=\"$loginurl\">".get_string('login').'</a>)';
  1032. }
  1033. }
  1034. $loggedinas = '<div class="logininfo">'.$loggedinas.'</div>';
  1035. if (isset($SESSION->justloggedin)) {
  1036. unset($SESSION->justloggedin);
  1037. if (!empty($CFG->displayloginfailures)) {
  1038. if (!isguestuser()) {
  1039. // Include this file only when required.
  1040. require_once($CFG->dirroot . '/user/lib.php');
  1041. if ($count = user_count_login_failures($USER)) {
  1042. $loggedinas .= '<div class="loginfailures">';
  1043. $a = new stdClass();
  1044. $a->attempts = $count;
  1045. $loggedinas .= get_string('failedloginattempts', '', $a);
  1046. if (file_exists("$CFG->dirroot/report/log/index.php") and has_capability('report/log:view', context_system::instance())) {
  1047. $loggedinas .= ' ('.html_writer::link(new moodle_url('/report/log/index.php', array('chooselog' => 1,
  1048. 'id' => 0 , 'modid' => 'site_errors')), get_string('logs')).')';
  1049. }
  1050. $loggedinas .= '</div>';
  1051. }
  1052. }
  1053. }
  1054. }
  1055. return $loggedinas;
  1056. }
  1057. /**
  1058. * Check whether the current page is a login page.
  1059. *
  1060. * @since Moodle 2.9
  1061. * @return bool
  1062. */
  1063. protected function is_login_page() {
  1064. // This is a real bit of a hack, but its a rarety that we need to do something like this.
  1065. // In fact the login pages should be only these two pages and as exposing this as an option for all pages
  1066. // could lead to abuse (or at least unneedingly complex code) the hack is the way to go.
  1067. return in_array(
  1068. $this->page->url->out_as_local_ur

Large files files are truncated, but you can click here to view the full file