PageRenderTime 32ms CodeModel.GetById 33ms RepoModel.GetById 0ms app.codeStats 1ms

/lib/outputlib.php

https://github.com/lsuits/moodle
PHP | 2086 lines | 1085 code | 226 blank | 775 comment | 249 complexity | b76c3fbfc7db17db4e61648445c5c3f7 MD5 | raw file
Possible License(s): GPL-3.0, LGPL-2.1, BSD-3-Clause, Apache-2.0
  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. * Functions for generating the HTML that Moodle should output.
  18. *
  19. * Please see http://docs.moodle.org/en/Developement:How_Moodle_outputs_HTML
  20. * for an overview.
  21. *
  22. * @copyright 2009 Tim Hunt
  23. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24. * @package core
  25. * @category output
  26. */
  27. defined('MOODLE_INTERNAL') || die();
  28. require_once($CFG->libdir.'/outputcomponents.php');
  29. require_once($CFG->libdir.'/outputactions.php');
  30. require_once($CFG->libdir.'/outputfactories.php');
  31. require_once($CFG->libdir.'/outputrenderers.php');
  32. require_once($CFG->libdir.'/outputrequirementslib.php');
  33. /**
  34. * Invalidate all server and client side caches.
  35. *
  36. * This method deletes the physical directory that is used to cache the theme
  37. * files used for serving.
  38. * Because it deletes the main theme cache directory all themes are reset by
  39. * this function.
  40. */
  41. function theme_reset_all_caches() {
  42. global $CFG, $PAGE;
  43. $next = time();
  44. if (isset($CFG->themerev) and $next <= $CFG->themerev and $CFG->themerev - $next < 60*60) {
  45. // This resolves problems when reset is requested repeatedly within 1s,
  46. // the < 1h condition prevents accidental switching to future dates
  47. // because we might not recover from it.
  48. $next = $CFG->themerev+1;
  49. }
  50. set_config('themerev', $next); // time is unique even when you reset/switch database
  51. if (!empty($CFG->themedesignermode)) {
  52. $cache = cache::make_from_params(cache_store::MODE_APPLICATION, 'core', 'themedesigner');
  53. $cache->purge();
  54. }
  55. if ($PAGE) {
  56. $PAGE->reload_theme();
  57. }
  58. }
  59. /**
  60. * Enable or disable theme designer mode.
  61. *
  62. * @param bool $state
  63. */
  64. function theme_set_designer_mod($state) {
  65. set_config('themedesignermode', (int)!empty($state));
  66. // Reset caches after switching mode so that any designer mode caches get purged too.
  67. theme_reset_all_caches();
  68. }
  69. /**
  70. * Returns current theme revision number.
  71. *
  72. * @return int
  73. */
  74. function theme_get_revision() {
  75. global $CFG;
  76. if (empty($CFG->themedesignermode)) {
  77. if (empty($CFG->themerev)) {
  78. return -1;
  79. } else {
  80. return $CFG->themerev;
  81. }
  82. } else {
  83. return -1;
  84. }
  85. }
  86. /**
  87. * This class represents the configuration variables of a Moodle theme.
  88. *
  89. * All the variables with access: public below (with a few exceptions that are marked)
  90. * are the properties you can set in your themes config.php file.
  91. *
  92. * There are also some methods and protected variables that are part of the inner
  93. * workings of Moodle's themes system. If you are just editing a themes config.php
  94. * file, you can just ignore those, and the following information for developers.
  95. *
  96. * Normally, to create an instance of this class, you should use the
  97. * {@link theme_config::load()} factory method to load a themes config.php file.
  98. * However, normally you don't need to bother, because moodle_page (that is, $PAGE)
  99. * will create one for you, accessible as $PAGE->theme.
  100. *
  101. * @copyright 2009 Tim Hunt
  102. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  103. * @since Moodle 2.0
  104. * @package core
  105. * @category output
  106. */
  107. class theme_config {
  108. /**
  109. * @var string Default theme, used when requested theme not found.
  110. */
  111. const DEFAULT_THEME = 'clean';
  112. /**
  113. * @var array You can base your theme on other themes by linking to the other theme as
  114. * parents. This lets you use the CSS and layouts from the other themes
  115. * (see {@link theme_config::$layouts}).
  116. * That makes it easy to create a new theme that is similar to another one
  117. * but with a few changes. In this themes CSS you only need to override
  118. * those rules you want to change.
  119. */
  120. public $parents;
  121. /**
  122. * @var array The names of all the stylesheets from this theme that you would
  123. * like included, in order. Give the names of the files without .css.
  124. */
  125. public $sheets = array();
  126. /**
  127. * @var array The names of all the stylesheets from parents that should be excluded.
  128. * true value may be used to specify all parents or all themes from one parent.
  129. * If no value specified value from parent theme used.
  130. */
  131. public $parents_exclude_sheets = null;
  132. /**
  133. * @var array List of plugin sheets to be excluded.
  134. * If no value specified value from parent theme used.
  135. */
  136. public $plugins_exclude_sheets = null;
  137. /**
  138. * @var array List of style sheets that are included in the text editor bodies.
  139. * Sheets from parent themes are used automatically and can not be excluded.
  140. */
  141. public $editor_sheets = array();
  142. /**
  143. * @var array The names of all the javascript files this theme that you would
  144. * like included from head, in order. Give the names of the files without .js.
  145. */
  146. public $javascripts = array();
  147. /**
  148. * @var array The names of all the javascript files this theme that you would
  149. * like included from footer, in order. Give the names of the files without .js.
  150. */
  151. public $javascripts_footer = array();
  152. /**
  153. * @var array The names of all the javascript files from parents that should
  154. * be excluded. true value may be used to specify all parents or all themes
  155. * from one parent.
  156. * If no value specified value from parent theme used.
  157. */
  158. public $parents_exclude_javascripts = null;
  159. /**
  160. * @var array Which file to use for each page layout.
  161. *
  162. * This is an array of arrays. The keys of the outer array are the different layouts.
  163. * Pages in Moodle are using several different layouts like 'normal', 'course', 'home',
  164. * 'popup', 'form', .... The most reliable way to get a complete list is to look at
  165. * {@link http://cvs.moodle.org/moodle/theme/base/config.php?view=markup the base theme config.php file}.
  166. * That file also has a good example of how to set this setting.
  167. *
  168. * For each layout, the value in the outer array is an array that describes
  169. * how you want that type of page to look. For example
  170. * <pre>
  171. * $THEME->layouts = array(
  172. * // Most pages - if we encounter an unknown or a missing page type, this one is used.
  173. * 'standard' => array(
  174. * 'theme' = 'mytheme',
  175. * 'file' => 'normal.php',
  176. * 'regions' => array('side-pre', 'side-post'),
  177. * 'defaultregion' => 'side-post'
  178. * ),
  179. * // The site home page.
  180. * 'home' => array(
  181. * 'theme' = 'mytheme',
  182. * 'file' => 'home.php',
  183. * 'regions' => array('side-pre', 'side-post'),
  184. * 'defaultregion' => 'side-post'
  185. * ),
  186. * // ...
  187. * );
  188. * </pre>
  189. *
  190. * 'theme' name of the theme where is the layout located
  191. * 'file' is the layout file to use for this type of page.
  192. * layout files are stored in layout subfolder
  193. * 'regions' This lists the regions on the page where blocks may appear. For
  194. * each region you list here, your layout file must include a call to
  195. * <pre>
  196. * echo $OUTPUT->blocks_for_region($regionname);
  197. * </pre>
  198. * or equivalent so that the blocks are actually visible.
  199. *
  200. * 'defaultregion' If the list of regions is non-empty, then you must pick
  201. * one of the one of them as 'default'. This has two meanings. First, this is
  202. * where new blocks are added. Second, if there are any blocks associated with
  203. * the page, but in non-existent regions, they appear here. (Imaging, for example,
  204. * that someone added blocks using a different theme that used different region
  205. * names, and then switched to this theme.)
  206. */
  207. public $layouts = array();
  208. /**
  209. * @var string Name of the renderer factory class to use. Must implement the
  210. * {@link renderer_factory} interface.
  211. *
  212. * This is an advanced feature. Moodle output is generated by 'renderers',
  213. * you can customise the HTML that is output by writing custom renderers,
  214. * and then you need to specify 'renderer factory' so that Moodle can find
  215. * your renderers.
  216. *
  217. * There are some renderer factories supplied with Moodle. Please follow these
  218. * links to see what they do.
  219. * <ul>
  220. * <li>{@link standard_renderer_factory} - the default.</li>
  221. * <li>{@link theme_overridden_renderer_factory} - use this if you want to write
  222. * your own custom renderers in a lib.php file in this theme (or the parent theme).</li>
  223. * </ul>
  224. */
  225. public $rendererfactory = 'standard_renderer_factory';
  226. /**
  227. * @var string Function to do custom CSS post-processing.
  228. *
  229. * This is an advanced feature. If you want to do custom post-processing on the
  230. * CSS before it is output (for example, to replace certain variable names
  231. * with particular values) you can give the name of a function here.
  232. */
  233. public $csspostprocess = null;
  234. /**
  235. * @var string Accessibility: Right arrow-like character is
  236. * used in the breadcrumb trail, course navigation menu
  237. * (previous/next activity), calendar, and search forum block.
  238. * If the theme does not set characters, appropriate defaults
  239. * are set automatically. Please DO NOT
  240. * use &lt; &gt; &raquo; - these are confusing for blind users.
  241. */
  242. public $rarrow = null;
  243. /**
  244. * @var string Accessibility: Right arrow-like character is
  245. * used in the breadcrumb trail, course navigation menu
  246. * (previous/next activity), calendar, and search forum block.
  247. * If the theme does not set characters, appropriate defaults
  248. * are set automatically. Please DO NOT
  249. * use &lt; &gt; &raquo; - these are confusing for blind users.
  250. */
  251. public $larrow = null;
  252. /**
  253. * @var bool Some themes may want to disable ajax course editing.
  254. */
  255. public $enablecourseajax = true;
  256. /**
  257. * @var string Determines served document types
  258. * - 'html5' the only officially supported doctype in Moodle
  259. * - 'xhtml5' may be used in development for validation (not intended for production servers!)
  260. * - 'xhtml' XHTML 1.0 Strict for legacy themes only
  261. */
  262. public $doctype = 'html5';
  263. //==Following properties are not configurable from theme config.php==
  264. /**
  265. * @var string The name of this theme. Set automatically when this theme is
  266. * loaded. This can not be set in theme config.php
  267. */
  268. public $name;
  269. /**
  270. * @var string The folder where this themes files are stored. This is set
  271. * automatically. This can not be set in theme config.php
  272. */
  273. public $dir;
  274. /**
  275. * @var stdClass Theme settings stored in config_plugins table.
  276. * This can not be set in theme config.php
  277. */
  278. public $setting = null;
  279. /**
  280. * @var bool If set to true and the theme enables the dock then blocks will be able
  281. * to be moved to the special dock
  282. */
  283. public $enable_dock = false;
  284. /**
  285. * @var bool If set to true then this theme will not be shown in the theme selector unless
  286. * theme designer mode is turned on.
  287. */
  288. public $hidefromselector = false;
  289. /**
  290. * @var array list of YUI CSS modules to be included on each page. This may be used
  291. * to remove cssreset and use cssnormalise module instead.
  292. */
  293. public $yuicssmodules = array('cssreset', 'cssfonts', 'cssgrids', 'cssbase');
  294. /**
  295. * An associative array of block manipulations that should be made if the user is using an rtl language.
  296. * The key is the original block region, and the value is the block region to change to.
  297. * This is used when displaying blocks for regions only.
  298. * @var array
  299. */
  300. public $blockrtlmanipulations = array();
  301. /**
  302. * @var renderer_factory Instance of the renderer_factory implementation
  303. * we are using. Implementation detail.
  304. */
  305. protected $rf = null;
  306. /**
  307. * @var array List of parent config objects.
  308. **/
  309. protected $parent_configs = array();
  310. /**
  311. * @var bool If set to true then the theme is safe to run through the optimiser (if it is enabled)
  312. * If set to false then we know either the theme has already been optimised and the CSS optimiser is not needed
  313. * or the theme is not compatible with the CSS optimiser. In both cases even if enabled the CSS optimiser will not
  314. * be used with this theme if set to false.
  315. */
  316. public $supportscssoptimisation = true;
  317. /**
  318. * Used to determine whether we can serve SVG images or not.
  319. * @var bool
  320. */
  321. private $usesvg = null;
  322. /**
  323. * The LESS file to compile. When set, the theme will attempt to compile the file itself.
  324. * @var bool
  325. */
  326. public $lessfile = false;
  327. /**
  328. * The name of the function to call to get the LESS code to inject.
  329. * @var string
  330. */
  331. public $extralesscallback = null;
  332. /**
  333. * The name of the function to call to get extra LESS variables.
  334. * @var string
  335. */
  336. public $lessvariablescallback = null;
  337. /**
  338. * Sets the render method that should be used for rendering custom block regions by scripts such as my/index.php
  339. * Defaults to {@link core_renderer::blocks_for_region()}
  340. * @var string
  341. */
  342. public $blockrendermethod = null;
  343. /**
  344. * Load the config.php file for a particular theme, and return an instance
  345. * of this class. (That is, this is a factory method.)
  346. *
  347. * @param string $themename the name of the theme.
  348. * @return theme_config an instance of this class.
  349. */
  350. public static function load($themename) {
  351. global $CFG;
  352. // load theme settings from db
  353. try {
  354. $settings = get_config('theme_'.$themename);
  355. } catch (dml_exception $e) {
  356. // most probably moodle tables not created yet
  357. $settings = new stdClass();
  358. }
  359. if ($config = theme_config::find_theme_config($themename, $settings)) {
  360. return new theme_config($config);
  361. } else if ($themename == theme_config::DEFAULT_THEME) {
  362. throw new coding_exception('Default theme '.theme_config::DEFAULT_THEME.' not available or broken!');
  363. } else if ($config = theme_config::find_theme_config($CFG->theme, $settings)) {
  364. return new theme_config($config);
  365. } else {
  366. // bad luck, the requested theme has some problems - admin see details in theme config
  367. return new theme_config(theme_config::find_theme_config(theme_config::DEFAULT_THEME, $settings));
  368. }
  369. }
  370. /**
  371. * Theme diagnostic code. It is very problematic to send debug output
  372. * to the actual CSS file, instead this functions is supposed to
  373. * diagnose given theme and highlights all potential problems.
  374. * This information should be available from the theme selection page
  375. * or some other debug page for theme designers.
  376. *
  377. * @param string $themename
  378. * @return array description of problems
  379. */
  380. public static function diagnose($themename) {
  381. //TODO: MDL-21108
  382. return array();
  383. }
  384. /**
  385. * Private constructor, can be called only from the factory method.
  386. * @param stdClass $config
  387. */
  388. private function __construct($config) {
  389. global $CFG; //needed for included lib.php files
  390. $this->settings = $config->settings;
  391. $this->name = $config->name;
  392. $this->dir = $config->dir;
  393. if ($this->name != 'base') {
  394. $baseconfig = theme_config::find_theme_config('base', $this->settings);
  395. } else {
  396. $baseconfig = $config;
  397. }
  398. $configurable = array('parents', 'sheets', 'parents_exclude_sheets', 'plugins_exclude_sheets', 'javascripts', 'javascripts_footer',
  399. 'parents_exclude_javascripts', 'layouts', 'enable_dock', 'enablecourseajax', 'supportscssoptimisation',
  400. 'rendererfactory', 'csspostprocess', 'editor_sheets', 'rarrow', 'larrow', 'hidefromselector', 'doctype',
  401. 'yuicssmodules', 'blockrtlmanipulations', 'lessfile', 'extralesscallback', 'lessvariablescallback',
  402. 'blockrendermethod');
  403. foreach ($config as $key=>$value) {
  404. if (in_array($key, $configurable)) {
  405. $this->$key = $value;
  406. }
  407. }
  408. // verify all parents and load configs and renderers
  409. foreach ($this->parents as $parent) {
  410. if ($parent == 'base') {
  411. $parent_config = $baseconfig;
  412. } else if (!$parent_config = theme_config::find_theme_config($parent, $this->settings)) {
  413. // this is not good - better exclude faulty parents
  414. continue;
  415. }
  416. $libfile = $parent_config->dir.'/lib.php';
  417. if (is_readable($libfile)) {
  418. // theme may store various function here
  419. include_once($libfile);
  420. }
  421. $renderersfile = $parent_config->dir.'/renderers.php';
  422. if (is_readable($renderersfile)) {
  423. // may contain core and plugin renderers and renderer factory
  424. include_once($renderersfile);
  425. }
  426. $this->parent_configs[$parent] = $parent_config;
  427. }
  428. $libfile = $this->dir.'/lib.php';
  429. if (is_readable($libfile)) {
  430. // theme may store various function here
  431. include_once($libfile);
  432. }
  433. $rendererfile = $this->dir.'/renderers.php';
  434. if (is_readable($rendererfile)) {
  435. // may contain core and plugin renderers and renderer factory
  436. include_once($rendererfile);
  437. } else {
  438. // check if renderers.php file is missnamed renderer.php
  439. if (is_readable($this->dir.'/renderer.php')) {
  440. debugging('Developer hint: '.$this->dir.'/renderer.php should be renamed to ' . $this->dir."/renderers.php.
  441. See: http://docs.moodle.org/dev/Output_renderers#Theme_renderers.", DEBUG_DEVELOPER);
  442. }
  443. }
  444. // cascade all layouts properly
  445. foreach ($baseconfig->layouts as $layout=>$value) {
  446. if (!isset($this->layouts[$layout])) {
  447. foreach ($this->parent_configs as $parent_config) {
  448. if (isset($parent_config->layouts[$layout])) {
  449. $this->layouts[$layout] = $parent_config->layouts[$layout];
  450. continue 2;
  451. }
  452. }
  453. $this->layouts[$layout] = $value;
  454. }
  455. }
  456. //fix arrows if needed
  457. $this->check_theme_arrows();
  458. }
  459. /**
  460. * Let the theme initialise the page object (usually $PAGE).
  461. *
  462. * This may be used for example to request jQuery in add-ons.
  463. *
  464. * @param moodle_page $page
  465. */
  466. public function init_page(moodle_page $page) {
  467. $themeinitfunction = 'theme_'.$this->name.'_page_init';
  468. if (function_exists($themeinitfunction)) {
  469. $themeinitfunction($page);
  470. }
  471. }
  472. /**
  473. * Checks if arrows $THEME->rarrow, $THEME->larrow have been set (theme/-/config.php).
  474. * If not it applies sensible defaults.
  475. *
  476. * Accessibility: right and left arrow Unicode characters for breadcrumb, calendar,
  477. * search forum block, etc. Important: these are 'silent' in a screen-reader
  478. * (unlike &gt; &raquo;), and must be accompanied by text.
  479. */
  480. private function check_theme_arrows() {
  481. if (!isset($this->rarrow) and !isset($this->larrow)) {
  482. // Default, looks good in Win XP/IE 6, Win/Firefox 1.5, Win/Netscape 8...
  483. // Also OK in Win 9x/2K/IE 5.x
  484. $this->rarrow = '&#x25BA;';
  485. $this->larrow = '&#x25C4;';
  486. if (empty($_SERVER['HTTP_USER_AGENT'])) {
  487. $uagent = '';
  488. } else {
  489. $uagent = $_SERVER['HTTP_USER_AGENT'];
  490. }
  491. if (false !== strpos($uagent, 'Opera')
  492. || false !== strpos($uagent, 'Mac')) {
  493. // Looks good in Win XP/Mac/Opera 8/9, Mac/Firefox 2, Camino, Safari.
  494. // Not broken in Mac/IE 5, Mac/Netscape 7 (?).
  495. $this->rarrow = '&#x25B6;';
  496. $this->larrow = '&#x25C0;';
  497. }
  498. elseif ((false !== strpos($uagent, 'Konqueror'))
  499. || (false !== strpos($uagent, 'Android'))) {
  500. // The fonts on Android don't include the characters required for this to work as expected.
  501. // So we use the same ones Konqueror uses.
  502. $this->rarrow = '&rarr;';
  503. $this->larrow = '&larr;';
  504. }
  505. elseif (isset($_SERVER['HTTP_ACCEPT_CHARSET'])
  506. && false === stripos($_SERVER['HTTP_ACCEPT_CHARSET'], 'utf-8')) {
  507. // (Win/IE 5 doesn't set ACCEPT_CHARSET, but handles Unicode.)
  508. // To be safe, non-Unicode browsers!
  509. $this->rarrow = '&gt;';
  510. $this->larrow = '&lt;';
  511. }
  512. // RTL support - in RTL languages, swap r and l arrows
  513. if (right_to_left()) {
  514. $t = $this->rarrow;
  515. $this->rarrow = $this->larrow;
  516. $this->larrow = $t;
  517. }
  518. }
  519. }
  520. /**
  521. * Returns output renderer prefixes, these are used when looking
  522. * for the overridden renderers in themes.
  523. *
  524. * @return array
  525. */
  526. public function renderer_prefixes() {
  527. global $CFG; // just in case the included files need it
  528. $prefixes = array('theme_'.$this->name);
  529. foreach ($this->parent_configs as $parent) {
  530. $prefixes[] = 'theme_'.$parent->name;
  531. }
  532. return $prefixes;
  533. }
  534. /**
  535. * Returns the stylesheet URL of this editor content
  536. *
  537. * @param bool $encoded false means use & and true use &amp; in URLs
  538. * @return moodle_url
  539. */
  540. public function editor_css_url($encoded=true) {
  541. global $CFG;
  542. $rev = theme_get_revision();
  543. if ($rev > -1) {
  544. $url = new moodle_url("$CFG->httpswwwroot/theme/styles.php");
  545. if (!empty($CFG->slasharguments)) {
  546. $url->set_slashargument('/'.$this->name.'/'.$rev.'/editor', 'noparam', true);
  547. } else {
  548. $url->params(array('theme'=>$this->name,'rev'=>$rev, 'type'=>'editor'));
  549. }
  550. } else {
  551. $params = array('theme'=>$this->name, 'type'=>'editor');
  552. $url = new moodle_url($CFG->httpswwwroot.'/theme/styles_debug.php', $params);
  553. }
  554. return $url;
  555. }
  556. /**
  557. * Returns the content of the CSS to be used in editor content
  558. *
  559. * @return array
  560. */
  561. public function editor_css_files() {
  562. $files = array();
  563. // First editor plugins.
  564. $plugins = core_component::get_plugin_list('editor');
  565. foreach ($plugins as $plugin=>$fulldir) {
  566. $sheetfile = "$fulldir/editor_styles.css";
  567. if (is_readable($sheetfile)) {
  568. $files['plugin_'.$plugin] = $sheetfile;
  569. }
  570. }
  571. // Then parent themes - base first, the immediate parent last.
  572. foreach (array_reverse($this->parent_configs) as $parent_config) {
  573. if (empty($parent_config->editor_sheets)) {
  574. continue;
  575. }
  576. foreach ($parent_config->editor_sheets as $sheet) {
  577. $sheetfile = "$parent_config->dir/style/$sheet.css";
  578. if (is_readable($sheetfile)) {
  579. $files['parent_'.$parent_config->name.'_'.$sheet] = $sheetfile;
  580. }
  581. }
  582. }
  583. // Finally this theme.
  584. if (!empty($this->editor_sheets)) {
  585. foreach ($this->editor_sheets as $sheet) {
  586. $sheetfile = "$this->dir/style/$sheet.css";
  587. if (is_readable($sheetfile)) {
  588. $files['theme_'.$sheet] = $sheetfile;
  589. }
  590. }
  591. }
  592. return $files;
  593. }
  594. /**
  595. * Get the stylesheet URL of this theme.
  596. *
  597. * @param moodle_page $page Not used... deprecated?
  598. * @return moodle_url[]
  599. */
  600. public function css_urls(moodle_page $page) {
  601. global $CFG;
  602. $rev = theme_get_revision();
  603. $urls = array();
  604. $svg = $this->use_svg_icons();
  605. $separate = (core_useragent::is_ie() && !core_useragent::check_ie_version('10'));
  606. if ($rev > -1) {
  607. $url = new moodle_url("$CFG->httpswwwroot/theme/styles.php");
  608. if (!empty($CFG->slasharguments)) {
  609. $slashargs = '';
  610. if (!$svg) {
  611. // We add a simple /_s to the start of the path.
  612. // The underscore is used to ensure that it isn't a valid theme name.
  613. $slashargs .= '/_s'.$slashargs;
  614. }
  615. $slashargs .= '/'.$this->name.'/'.$rev.'/all';
  616. if ($separate) {
  617. $slashargs .= '/chunk0';
  618. }
  619. $url->set_slashargument($slashargs, 'noparam', true);
  620. } else {
  621. $params = array('theme' => $this->name,'rev' => $rev, 'type' => 'all');
  622. if (!$svg) {
  623. // We add an SVG param so that we know not to serve SVG images.
  624. // We do this because all modern browsers support SVG and this param will one day be removed.
  625. $params['svg'] = '0';
  626. }
  627. if ($separate) {
  628. $params['chunk'] = '0';
  629. }
  630. $url->params($params);
  631. }
  632. $urls[] = $url;
  633. } else {
  634. $baseurl = new moodle_url($CFG->httpswwwroot.'/theme/styles_debug.php');
  635. $css = $this->get_css_files(true);
  636. if (!$svg) {
  637. // We add an SVG param so that we know not to serve SVG images.
  638. // We do this because all modern browsers support SVG and this param will one day be removed.
  639. $baseurl->param('svg', '0');
  640. }
  641. if ($separate) {
  642. // We might need to chunk long files.
  643. $baseurl->param('chunk', '0');
  644. }
  645. if (core_useragent::is_ie()) {
  646. // Lalala, IE does not allow more than 31 linked CSS files from main document.
  647. $urls[] = new moodle_url($baseurl, array('theme'=>$this->name, 'type'=>'ie', 'subtype'=>'plugins'));
  648. foreach ($css['parents'] as $parent=>$sheets) {
  649. // We need to serve parents individually otherwise we may easily exceed the style limit IE imposes (4096).
  650. $urls[] = new moodle_url($baseurl, array('theme'=>$this->name,'type'=>'ie', 'subtype'=>'parents', 'sheet'=>$parent));
  651. }
  652. if (!empty($this->lessfile)) {
  653. // No need to define the type as IE here.
  654. $urls[] = new moodle_url($baseurl, array('theme' => $this->name, 'type' => 'less'));
  655. }
  656. $urls[] = new moodle_url($baseurl, array('theme'=>$this->name, 'type'=>'ie', 'subtype'=>'theme'));
  657. } else {
  658. foreach ($css['plugins'] as $plugin=>$unused) {
  659. $urls[] = new moodle_url($baseurl, array('theme'=>$this->name,'type'=>'plugin', 'subtype'=>$plugin));
  660. }
  661. foreach ($css['parents'] as $parent=>$sheets) {
  662. foreach ($sheets as $sheet=>$unused2) {
  663. $urls[] = new moodle_url($baseurl, array('theme'=>$this->name,'type'=>'parent', 'subtype'=>$parent, 'sheet'=>$sheet));
  664. }
  665. }
  666. foreach ($css['theme'] as $sheet => $filename) {
  667. if ($sheet === $this->lessfile) {
  668. // This is the theme LESS file.
  669. $urls[] = new moodle_url($baseurl, array('theme' => $this->name, 'type' => 'less'));
  670. } else {
  671. // Sheet first in order to make long urls easier to read.
  672. $urls[] = new moodle_url($baseurl, array('sheet'=>$sheet, 'theme'=>$this->name, 'type'=>'theme'));
  673. }
  674. }
  675. }
  676. }
  677. return $urls;
  678. }
  679. /**
  680. * Get the whole css stylesheet for production mode.
  681. *
  682. * NOTE: this method is not expected to be used from any addons.
  683. *
  684. * @return string CSS markup, already optimised and compressed
  685. */
  686. public function get_css_content() {
  687. global $CFG;
  688. require_once($CFG->dirroot.'/lib/csslib.php');
  689. $csscontent = '';
  690. foreach ($this->get_css_files(false) as $type => $value) {
  691. foreach ($value as $identifier => $val) {
  692. if (is_array($val)) {
  693. foreach ($val as $v) {
  694. $csscontent .= file_get_contents($v) . "\n";
  695. }
  696. } else {
  697. if ($type === 'theme' && $identifier === $this->lessfile) {
  698. // We need the content from LESS because this is the LESS file from the theme.
  699. $csscontent .= $this->get_css_content_from_less(false);
  700. } else {
  701. $csscontent .= file_get_contents($val) . "\n";
  702. }
  703. }
  704. }
  705. }
  706. $csscontent = $this->post_process($csscontent);
  707. if (!empty($CFG->enablecssoptimiser) && $this->supportscssoptimisation) {
  708. // This is an experimental feature introduced in Moodle 2.3
  709. // The CSS optimiser organises the CSS in order to reduce the overall number
  710. // of rules and styles being sent to the client. It does this by collating
  711. // the CSS before it is cached removing excess styles and rules and stripping
  712. // out any extraneous content such as comments and empty rules.
  713. $optimiser = new css_optimiser();
  714. $csscontent = $optimiser->process($csscontent);
  715. } else {
  716. $csscontent = core_minify::css($csscontent);
  717. }
  718. return $csscontent;
  719. }
  720. /**
  721. * Get the theme designer css markup,
  722. * the parameters are coming from css_urls().
  723. *
  724. * NOTE: this method is not expected to be used from any addons.
  725. *
  726. * @param string $type
  727. * @param string $subtype
  728. * @param string $sheet
  729. * @return string CSS markup
  730. */
  731. public function get_css_content_debug($type, $subtype, $sheet) {
  732. global $CFG;
  733. require_once($CFG->dirroot.'/lib/csslib.php');
  734. // The LESS file of the theme is requested.
  735. if ($type === 'less') {
  736. $csscontent = $this->get_css_content_from_less(true);
  737. if ($csscontent !== false) {
  738. return $csscontent;
  739. }
  740. return '';
  741. }
  742. $optimiser = null;
  743. if (!empty($CFG->enablecssoptimiser) && $this->supportscssoptimisation) {
  744. // This is an experimental feature introduced in Moodle 2.3
  745. // The CSS optimiser organises the CSS in order to reduce the overall number
  746. // of rules and styles being sent to the client. It does this by collating
  747. // the CSS before it is cached removing excess styles and rules and stripping
  748. // out any extraneous content such as comments and empty rules.
  749. $optimiser = new css_optimiser();
  750. }
  751. $cssfiles = array();
  752. $css = $this->get_css_files(true);
  753. if ($type === 'ie') {
  754. // IE is a sloppy browser with weird limits, sorry.
  755. if ($subtype === 'plugins') {
  756. $cssfiles = $css['plugins'];
  757. } else if ($subtype === 'parents') {
  758. if (empty($sheet)) {
  759. // Do not bother with the empty parent here.
  760. } else {
  761. // Build up the CSS for that parent so we can serve it as one file.
  762. foreach ($css[$subtype][$sheet] as $parent => $css) {
  763. $cssfiles[] = $css;
  764. }
  765. }
  766. } else if ($subtype === 'theme') {
  767. $cssfiles = $css['theme'];
  768. foreach ($cssfiles as $key => $value) {
  769. if ($this->lessfile && $key === $this->lessfile) {
  770. // Remove the LESS file from the theme CSS files.
  771. // The LESS files use the type 'less', not 'ie'.
  772. unset($cssfiles[$key]);
  773. }
  774. }
  775. }
  776. } else if ($type === 'plugin') {
  777. if (isset($css['plugins'][$subtype])) {
  778. $cssfiles[] = $css['plugins'][$subtype];
  779. }
  780. } else if ($type === 'parent') {
  781. if (isset($css['parents'][$subtype][$sheet])) {
  782. $cssfiles[] = $css['parents'][$subtype][$sheet];
  783. }
  784. } else if ($type === 'theme') {
  785. if (isset($css['theme'][$sheet])) {
  786. $cssfiles[] = $css['theme'][$sheet];
  787. }
  788. }
  789. $csscontent = '';
  790. foreach ($cssfiles as $file) {
  791. $contents = file_get_contents($file);
  792. $contents = $this->post_process($contents);
  793. $comment = "/** Path: $type $subtype $sheet.' **/\n";
  794. $stats = '';
  795. if ($optimiser) {
  796. $contents = $optimiser->process($contents);
  797. if (!empty($CFG->cssoptimiserstats)) {
  798. $stats = $optimiser->output_stats_css();
  799. }
  800. }
  801. $csscontent .= $comment.$stats.$contents."\n\n";
  802. }
  803. return $csscontent;
  804. }
  805. /**
  806. * Get the whole css stylesheet for editor iframe.
  807. *
  808. * NOTE: this method is not expected to be used from any addons.
  809. *
  810. * @return string CSS markup
  811. */
  812. public function get_css_content_editor() {
  813. // Do not bother to optimise anything here, just very basic stuff.
  814. $cssfiles = $this->editor_css_files();
  815. $css = '';
  816. foreach ($cssfiles as $file) {
  817. $css .= file_get_contents($file)."\n";
  818. }
  819. return $this->post_process($css);
  820. }
  821. /**
  822. * Returns an array of organised CSS files required for this output.
  823. *
  824. * @param bool $themedesigner
  825. * @return array nested array of file paths
  826. */
  827. protected function get_css_files($themedesigner) {
  828. global $CFG;
  829. $cache = null;
  830. $cachekey = 'cssfiles';
  831. if ($themedesigner) {
  832. require_once($CFG->dirroot.'/lib/csslib.php');
  833. // We need some kind of caching here because otherwise the page navigation becomes
  834. // way too slow in theme designer mode. Feel free to create full cache definition later...
  835. $cache = cache::make_from_params(cache_store::MODE_APPLICATION, 'core', 'themedesigner', array('theme' => $this->name));
  836. if ($files = $cache->get($cachekey)) {
  837. if ($files['created'] > time() - THEME_DESIGNER_CACHE_LIFETIME) {
  838. unset($files['created']);
  839. return $files;
  840. }
  841. }
  842. }
  843. $cssfiles = array('plugins'=>array(), 'parents'=>array(), 'theme'=>array());
  844. // Get all plugin sheets.
  845. $excludes = $this->resolve_excludes('plugins_exclude_sheets');
  846. if ($excludes !== true) {
  847. foreach (core_component::get_plugin_types() as $type=>$unused) {
  848. if ($type === 'theme' || (!empty($excludes[$type]) and $excludes[$type] === true)) {
  849. continue;
  850. }
  851. $plugins = core_component::get_plugin_list($type);
  852. foreach ($plugins as $plugin=>$fulldir) {
  853. if (!empty($excludes[$type]) and is_array($excludes[$type])
  854. and in_array($plugin, $excludes[$type])) {
  855. continue;
  856. }
  857. // Get the CSS from the plugin.
  858. $sheetfile = "$fulldir/styles.css";
  859. if (is_readable($sheetfile)) {
  860. $cssfiles['plugins'][$type.'_'.$plugin] = $sheetfile;
  861. }
  862. // Create a list of candidate sheets from parents (direct parent last) and current theme.
  863. $candidates = array();
  864. foreach (array_reverse($this->parent_configs) as $parent_config) {
  865. $candidates[] = $parent_config->name;
  866. }
  867. $candidates[] = $this->name;
  868. // Add the sheets found.
  869. foreach ($candidates as $candidate) {
  870. $sheetthemefile = "$fulldir/styles_{$candidate}.css";
  871. if (is_readable($sheetthemefile)) {
  872. $cssfiles['plugins'][$type.'_'.$plugin.'_'.$candidate] = $sheetthemefile;
  873. }
  874. }
  875. }
  876. }
  877. }
  878. // Find out wanted parent sheets.
  879. $excludes = $this->resolve_excludes('parents_exclude_sheets');
  880. if ($excludes !== true) {
  881. foreach (array_reverse($this->parent_configs) as $parent_config) { // Base first, the immediate parent last.
  882. $parent = $parent_config->name;
  883. if (empty($parent_config->sheets) || (!empty($excludes[$parent]) and $excludes[$parent] === true)) {
  884. continue;
  885. }
  886. foreach ($parent_config->sheets as $sheet) {
  887. if (!empty($excludes[$parent]) && is_array($excludes[$parent])
  888. && in_array($sheet, $excludes[$parent])) {
  889. continue;
  890. }
  891. // We never refer to the parent LESS files.
  892. $sheetfile = "$parent_config->dir/style/$sheet.css";
  893. if (is_readable($sheetfile)) {
  894. $cssfiles['parents'][$parent][$sheet] = $sheetfile;
  895. }
  896. }
  897. }
  898. }
  899. // Current theme sheets and less file.
  900. // We first add the LESS files because we want the CSS ones to be included after the
  901. // LESS code. However, if both the LESS file and the CSS file share the same name,
  902. // the CSS file is ignored.
  903. if (!empty($this->lessfile)) {
  904. $sheetfile = "{$this->dir}/less/{$this->lessfile}.less";
  905. if (is_readable($sheetfile)) {
  906. $cssfiles['theme'][$this->lessfile] = $sheetfile;
  907. }
  908. }
  909. if (is_array($this->sheets)) {
  910. foreach ($this->sheets as $sheet) {
  911. $sheetfile = "$this->dir/style/$sheet.css";
  912. if (is_readable($sheetfile) && !isset($cssfiles['theme'][$sheet])) {
  913. $cssfiles['theme'][$sheet] = $sheetfile;
  914. }
  915. }
  916. }
  917. if ($cache) {
  918. $files = $cssfiles;
  919. $files['created'] = time();
  920. $cache->set($cachekey, $files);
  921. }
  922. return $cssfiles;
  923. }
  924. /**
  925. * Return the CSS content generated from LESS the file.
  926. *
  927. * @param bool $themedesigner True if theme designer is enabled.
  928. * @return bool|string Return false when the compilation failed. Else the compiled string.
  929. */
  930. protected function get_css_content_from_less($themedesigner) {
  931. $lessfile = $this->lessfile;
  932. if (!$lessfile || !is_readable($this->dir . '/less/' . $lessfile . '.less')) {
  933. throw new coding_exception('The theme did not define a LESS file, or it is not readable.');
  934. }
  935. // We might need more memory to do this, so let's play safe.
  936. raise_memory_limit(MEMORY_EXTRA);
  937. // Files list.
  938. $files = $this->get_css_files($themedesigner);
  939. // Get the LESS file path.
  940. $themelessfile = $files['theme'][$lessfile];
  941. // Setup compiler options.
  942. $options = array(
  943. // We need to set the import directory to where $lessfile is.
  944. 'import_dirs' => array(dirname($themelessfile) => '/'),
  945. // Always disable default caching.
  946. 'cache_method' => false,
  947. // Disable the relative URLs, we have post_process() to handle that.
  948. 'relativeUrls' => false,
  949. );
  950. if ($themedesigner) {
  951. // Add the sourceMap inline to ensure that it is atomically generated.
  952. $options['sourceMap'] = true;
  953. $options['sourceRoot'] = 'theme';
  954. }
  955. // Instantiate the compiler.
  956. $compiler = new core_lessc($options);
  957. try {
  958. $compiler->parse_file_content($themelessfile);
  959. // Get the callbacks.
  960. $compiler->parse($this->get_extra_less_code());
  961. $compiler->ModifyVars($this->get_less_variables());
  962. // Compile the CSS.
  963. $compiled = $compiler->getCss();
  964. // Post process the entire thing.
  965. $compiled = $this->post_process($compiled);
  966. } catch (Less_Exception_Parser $e) {
  967. $compiled = false;
  968. debugging('Error while compiling LESS ' . $lessfile . ' file: ' . $e->getMessage(), DEBUG_DEVELOPER);
  969. }
  970. // Try to save memory.
  971. $compiler = null;
  972. unset($compiler);
  973. return $compiled;
  974. }
  975. /**
  976. * Return extra LESS variables to use when compiling.
  977. *
  978. * @return array Where keys are the variable names (omitting the @), and the values are the value.
  979. */
  980. protected function get_less_variables() {
  981. $variables = array();
  982. // Getting all the candidate functions.
  983. $candidates = array();
  984. foreach ($this->parent_configs as $parent_config) {
  985. if (!isset($parent_config->lessvariablescallback)) {
  986. continue;
  987. }
  988. $candidates[] = $parent_config->lessvariablescallback;
  989. }
  990. $candidates[] = $this->lessvariablescallback;
  991. // Calling the functions.
  992. foreach ($candidates as $function) {
  993. if (function_exists($function)) {
  994. $vars = $function($this);
  995. if (!is_array($vars)) {
  996. debugging('Callback ' . $function . ' did not return an array() as expected', DEBUG_DEVELOPER);
  997. continue;
  998. }
  999. $variables = array_merge($variables, $vars);
  1000. }
  1001. }
  1002. return $variables;
  1003. }
  1004. /**
  1005. * Return extra LESS code to add when compiling.
  1006. *
  1007. * This is intended to be used by themes to inject some LESS code
  1008. * before it gets compiled. If you want to inject variables you
  1009. * should use {@link self::get_less_variables()}.
  1010. *
  1011. * @return string The LESS code to inject.
  1012. */
  1013. protected function get_extra_less_code() {
  1014. $content = '';
  1015. // Getting all the candidate functions.
  1016. $candidates = array();
  1017. foreach ($this->parent_configs as $parent_config) {
  1018. if (!isset($parent_config->extralesscallback)) {
  1019. continue;
  1020. }
  1021. $candidates[] = $parent_config->extralesscallback;
  1022. }
  1023. $candidates[] = $this->extralesscallback;
  1024. // Calling the functions.
  1025. foreach ($candidates as $function) {
  1026. if (function_exists($function)) {
  1027. $content .= "\n/** Extra LESS from $function **/\n" . $function($this) . "\n";
  1028. }
  1029. }
  1030. return $content;
  1031. }
  1032. /**
  1033. * Generate a URL to the file that serves theme JavaScript files.
  1034. *
  1035. * If we determine that the theme has no relevant files, then we return
  1036. * early with a null value.
  1037. *
  1038. * @param bool $inhead true means head url, false means footer
  1039. * @return moodle_url|null
  1040. */
  1041. public function javascript_url($inhead) {
  1042. global $CFG;
  1043. $rev = theme_get_revision();
  1044. $params = array('theme'=>$this->name,'rev'=>$rev);
  1045. $params['type'] = $inhead ? 'head' : 'footer';
  1046. // Return early if there are no files to serve
  1047. if (count($this->javascript_files($params['type'])) === 0) {
  1048. return null;
  1049. }
  1050. if (!empty($CFG->slasharguments) and $rev > 0) {
  1051. $url = new moodle_url("$CFG->httpswwwroot/theme/javascript.php");
  1052. $url->set_slashargument('/'.$this->name.'/'.$rev.'/'.$params['type'], 'noparam', true);
  1053. return $url;
  1054. } else {
  1055. return new moodle_url($CFG->httpswwwroot.'/theme/javascript.php', $params);
  1056. }
  1057. }
  1058. /**
  1059. * Get the URL's for the JavaScript files used by this theme.
  1060. * They won't be served directly, instead they'll be mediated through
  1061. * theme/javascript.php.
  1062. *
  1063. * @param string $type Either javascripts_footer, or javascripts
  1064. * @return array
  1065. */
  1066. public function javascript_files($type) {
  1067. if ($type === 'footer') {
  1068. $type = 'javascripts_footer';
  1069. } else {
  1070. $type = 'javascripts';
  1071. }
  1072. $js = array();
  1073. // find out wanted parent javascripts
  1074. $excludes = $this->resolve_excludes('parents_exclude_javascripts');
  1075. if ($excludes !== true) {
  1076. foreach (array_reverse($this->parent_configs) as $parent_config) { // base first, the immediate parent last
  1077. $parent = $parent_config->name;
  1078. if (empty($parent_config->$type)) {
  1079. continue;
  1080. }
  1081. if (!empty($excludes[$parent]) and $excludes[$parent] === true) {
  1082. continue;
  1083. }
  1084. foreach ($parent_config->$type as $javascript) {
  1085. if (!empty($excludes[$parent]) and is_array($excludes[$parent])
  1086. and in_array($javascript, $excludes[$parent])) {
  1087. continue;
  1088. }
  1089. $javascriptfile = "$parent_config->dir/javascript/$javascript.js";
  1090. if (is_readable($javascriptfile)) {
  1091. $js[] = $javascriptfile;
  1092. }
  1093. }
  1094. }
  1095. }
  1096. // current theme javascripts
  1097. if (is_array($this->$type)) {
  1098. foreach ($this->$type as $javascript) {
  1099. $javascriptfile = "$this->dir/javascript/$javascript.js";
  1100. if (is_readable($javascriptfile)) {
  1101. $js[] = $javascriptfile;
  1102. }
  1103. }
  1104. }
  1105. return $js;
  1106. }
  1107. /**
  1108. * Resolves an exclude setting to the themes setting is applicable or the
  1109. * setting of its closest parent.
  1110. *
  1111. * @param string $variable The name of the setting the exclude setting to resolve
  1112. * @param string $default
  1113. * @return mixed
  1114. */
  1115. protected function resolve_excludes($variable, $default = null) {
  1116. $setting = $default;
  1117. if (is_array($this->{$variable}) or $this->{$variable} === true) {
  1118. $setting = $this->{$variable};
  1119. } else {
  1120. foreach ($this->parent_configs as $parent_config) { // the immediate parent first, base last
  1121. if (!isset($parent_config->{$variable})) {
  1122. continue;
  1123. }
  1124. if (is_array($parent_config->{$variable}) or $parent_config->{$variable} === true) {
  1125. $setting = $parent_config->{$variable};
  1126. break;
  1127. }
  1128. }
  1129. }
  1130. return $setting;
  1131. }
  1132. /**
  1133. * Returns the content of the one huge javascript file merged from all theme javascript files.
  1134. *
  1135. * @param bool $type
  1136. * @return string
  1137. */
  1138. public function javascript_content($type) {
  1139. $jsfiles = $this->javascript_files($type);
  1140. $js = '';
  1141. foreach ($jsfiles as $jsfile) {
  1142. $js .= file_get_contents($jsfile)."\n";
  1143. }
  1144. return $js;
  1145. }
  1146. /**
  1147. * Post processes CSS.
  1148. *
  1149. * This method post processes all of the CSS before it is served for this theme.
  1150. * This is done so that things such as image URL's can be swapped in and to
  1151. * run any specific CSS post process method the theme has requested.
  1152. * This allows themes to use CSS settings.
  1153. *
  1154. * @param string $css The CSS to process.
  1155. * @return string The processed CSS.
  1156. */
  1157. public function post_process($css) {
  1158. // now resolve all image locations
  1159. if (preg_match_all('/\[\[pix:([a-z0-9_]+\|)?([^\]]+)\]\]/', $css, $matches, PREG_SET_ORDER)) {
  1160. $replaced = array();
  1161. foreach ($matches as $match) {
  1162. if (isset($replaced[$match[0]])) {
  1163. continue;
  1164. }
  1165. $replaced[$match[0]] = true;
  1166. $imagename = $match[2];
  1167. $component = rtrim($match[1], '|');
  1168. $imageurl = $this->pix_url($imagename, $component)->out(false);
  1169. // we do not need full url because the image.php is always in the same dir
  1170. $imageurl = preg_replace('|^http.?://[^/]+|', '', $imageurl);
  1171. $css = str_replace($match[0], $imageurl, $css);
  1172. }
  1173. }
  1174. // Now resolve all font locations.
  1175. if (preg_match_all('/\[\[font:([a-z0-9_]+\|)?([^\]]+)\]\]/', $css, $matches, PREG_SET_ORDER)) {
  1176. $replaced = array();
  1177. foreach ($matches as $match) {
  1178. if (isset($replaced[$match[0]])) {
  1179. continue;
  1180. }
  1181. $replaced[$match[0]] = true;
  1182. $fontname = $match[2];
  1183. $component = rtrim($match[1], '|');
  1184. $fonturl = $this->font_url($fontname, $component)->out(false);
  1185. // We do not need full url because the font.php is always in the same dir.
  1186. $fonturl = preg_replace('|^http.?://[^/]+|', '', $fonturl);
  1187. $css = str_replace($match[0], $fonturl, $css);
  1188. }
  1189. }
  1190. // now resolve all theme settings or do any other postprocessing
  1191. $csspostprocess = $this->csspostprocess;
  1192. if (function_exists($csspostprocess)) {
  1193. $css = $csspostprocess($css, $this);
  1194. }
  1195. return $css;
  1196. }
  1197. /**
  1198. * Return the URL for an image
  1199. *
  1200. * @param string $imagename the name of the icon.
  1201. * @param string $component specification of one plugin like in get_string()
  1202. * @return moodle_url
  1203. */
  1204. public function pix_url($imagename, $component) {
  1205. global $CFG;
  1206. $params = array('theme'=>$this->name);
  1207. $svg = $this->use_svg_icons();
  1208. if (empty($component) or $component === 'moodle' or $component === 'core') {
  1209. $params['component'] = 'core';
  1210. } else {
  1211. $params['component'] = $component;
  1212. }
  1213. $rev = theme_get_revision();
  1214. if ($rev != -1) {
  1215. $params['rev'] = $rev;
  1216. }
  1217. $params['image'] = $imagename;
  1218. $url = new moodle_url("$CFG->httpswwwroot/theme/image.php");
  1219. if (!empty($CFG->slasharguments) and $rev > 0) {
  1220. $path = '/'.$params['theme'].'/'.$params['component'].'/'.$params['rev'].'/'.$params['image'];
  1221. if (!$svg) {
  1222. // We add a simple /_s to the start of the path.
  1223. // The underscore is used to ensure that it isn't a valid theme name.
  1224. $path = '/_s'.$path;
  1225. }
  1226. $url->set_slashargument($path, 'noparam', true);
  1227. } else {
  1228. if (!$svg) {
  1229. // We add an SVG param so that we know not to serve SVG images.
  1230. // We do this because all modern browsers support SVG and this param will one day be removed.
  1231. $params['svg'] = '0';
  1232. }
  1233. $url->params($params);
  1234. }
  1235. return $url;
  1236. }
  1237. /**
  1238. * Return the URL for a font
  1239. *
  1240. * @param string $font the name of the font (including extension).
  1241. * @param string $component specification of one plugin like in get_string()
  1242. * @return moodle_url
  1243. */
  1244. public function font_url($font, $component) {
  1245. global $CFG;
  1246. $params = array('theme'=>$this->name);
  1247. if (empty($component) or $component === 'moodle' or $component === 'core') {
  1248. $params['component'] = 'core';
  1249. } else {
  1250. $params['component'] = $component;
  1251. }
  1252. $rev = theme_get_revision();
  1253. if ($rev != -1) {
  1254. $params['rev'] = $rev;
  1255. }
  1256. $params['font'] = $font;
  1257. $url = new moodle_url("$CFG->httpswwwroot/theme/font.php");
  1258. if (!empty($CFG->slasharguments) and $rev > 0) {
  1259. $path = '/'.$params['theme'].'/'.$params['component'].'/'.$params['rev'].'/'.$params['font'];
  1260. $url->set_slashargument($path, 'noparam', true);
  1261. } else {
  1262. $url->params($params);
  1263. }
  1264. return $url;
  1265. }
  1266. /**
  1267. * Returns URL to the stored file via pluginfile.php.
  1268. *
  1269. * Note the theme must also implement pluginfile.php handler,
  1270. * theme revision is used instead of the itemid.
  1271. *
  1272. * @param string $setting
  1273. * @param string $filearea
  1274. * @return string protocol relative URL or null if not present
  1275. */
  1276. public function setting_file_url($setting, $filearea) {
  1277. global $CFG;
  1278. if (empty($this->settings->$setting)) {
  1279. return null;
  1280. }
  1281. $component = 'theme_'.$this->name;
  1282. $itemid = theme_get_revision();
  1283. $filepath = $this->settings->$setting;
  1284. $syscontext = context_system::instance();
  1285. $url = moodle_url::make_file_url("$CFG->wwwroot/pluginfile.php", "/$syscontext->id/$component/$filearea/$itemid".$filepath);
  1286. // Now this is tricky because the we can not hardcode http or https here, lets use the relative link.
  1287. // Note: unfortunately moodle_url does not support //urls yet.
  1288. $url = preg_replace('|^https?://|i', '//', $url->out(false));
  1289. return $url;
  1290. }
  1291. /**
  1292. * Serve the theme setting file.
  1293. *
  1294. * @param string $filearea
  1295. * @param array $args
  1296. * @param bool $forcedownload
  1297. * @param array $options
  1298. * @return bool may terminate if file not found or donotdie not specified
  1299. */
  1300. public function setting_file_serve($filearea, $args, $forcedownload, $options) {
  1301. global $CFG;
  1302. require_once("$CFG->libdir/filelib.php");
  1303. $syscontext = context_system::instance();
  1304. $component = 'theme_'.$this->name;
  1305. $revision = array_shift($args);
  1306. if ($revision < 0) {
  1307. $lifetime = 0;
  1308. } else {
  1309. $lifetime = 60*60*24*60;
  1310. }
  1311. $fs = get_file_storage();
  1312. $relativepath = implode('/', $args);
  1313. $fullpath = "/{$syscontext->id}/{$component}/{$filearea}/0/{$relativepath}";
  1314. $fullpath = rtrim($fullpath, '/');
  1315. if ($file = $fs->get_file_by_hash(sha1($fullpath))) {
  1316. send_stored_file($file, $lifetime, 0, $forcedownload, $options);
  1317. return true;
  1318. } else {
  1319. send_file_not_found();
  1320. }
  1321. }
  1322. /**
  1323. * Resolves the real image location.
  1324. *
  1325. * $svg was introduced as an arg in 2.4. It is important because not all supported browsers support the use of SVG
  1326. * and we need a way in which to turn it off.
  1327. * By default SVG won't be used unless asked for. This is done for two reasons:
  1328. * 1. It ensures that we don't serve svg images unless we really want to. The admin has selected to force them, of the users
  1329. * browser supports SVG.
  1330. * 2. We only serve SVG images from locations we trust. This must NOT include any areas where the image may have been uploaded
  1331. * by the user due to security concerns.
  1332. *
  1333. * @param string $image name of image, may contain relative path
  1334. * @param string $component
  1335. * @param bool $svg If set to true SVG images will also be looked for.
  1336. * @return string full file path
  1337. */
  1338. public function resolve_image_location($image, $component, $svg = false) {
  1339. global $CFG;
  1340. if (!is_bool($svg)) {
  1341. // If $svg isn't a bool then we need to decide for ourselves.
  1342. $svg = $this->use_svg_icons();
  1343. }
  1344. if ($component === 'moodle' or $component === 'core' or empty($component)) {
  1345. if ($imagefile = $this->image_exists("$this->dir/pix_core/$image", $svg)) {
  1346. return $imagefile;
  1347. }
  1348. foreach (array_reverse($this->parent_configs) as $parent_config) { // base first, the immediate parent last
  1349. if ($imagefile = $this->image_exists("$parent_config->dir/pix_core/$image", $svg)) {
  1350. return $imagefile;
  1351. }
  1352. }
  1353. if ($imagefile = $this->image_exists("$CFG->dataroot/pix/$image", $svg)) {
  1354. return $imagefile;
  1355. }
  1356. if ($imagefile = $this->image_exists("$CFG->dirroot/pix/$image", $svg)) {
  1357. return $imagefile;
  1358. }
  1359. return null;
  1360. } else if ($component === 'theme') { //exception
  1361. if ($image === 'favicon') {
  1362. return "$this->dir/pix/favicon.ico";
  1363. }
  1364. if ($imagefile = $this->image_exists("$this->dir/pix/$image", $svg)) {
  1365. return $imagefile;
  1366. }
  1367. foreach (array_reverse($this->parent_configs) as $parent_config) { // base first, the immediate parent last
  1368. if ($imagefile = $this->image_exists("$parent_config->dir/pix/$image", $svg)) {
  1369. return $imagefile;
  1370. }
  1371. }
  1372. return null;
  1373. } else {
  1374. if (strpos($component, '_') === false) {
  1375. $component = 'mod_'.$component;
  1376. }
  1377. list($type, $plugin) = explode('_', $component, 2);
  1378. if ($imagefile = $this->image_exists("$this->dir/pix_plugins/$type/$plugin/$image", $svg)) {
  1379. return $imagefile;
  1380. }
  1381. foreach (array_reverse($this->parent_configs) as $parent_config) { // base first, the immediate parent last
  1382. if ($imagefile = $this->image_exists("$parent_config->dir/pix_plugins/$type/$plugin/$image", $svg)) {
  1383. return $imagefile;
  1384. }
  1385. }
  1386. if ($imagefile = $this->image_exists("$CFG->dataroot/pix_plugins/$type/$plugin/$image", $svg)) {
  1387. return $imagefile;
  1388. }
  1389. $dir = core_component::get_plugin_directory($type, $plugin);
  1390. if ($imagefile = $this->image_exists("$dir/pix/$image", $svg)) {
  1391. return $imagefile;
  1392. }
  1393. return null;
  1394. }
  1395. }
  1396. /**
  1397. * Resolves the real font location.
  1398. *
  1399. * @param string $font name of font file
  1400. * @param string $component
  1401. * @return string full file path
  1402. */
  1403. public function resolve_font_location($font, $component) {
  1404. global $CFG;
  1405. if ($component === 'moodle' or $component === 'core' or empty($component)) {
  1406. if (file_exists("$this->dir/fonts_core/$font")) {
  1407. return "$this->dir/fonts_core/$font";
  1408. }
  1409. foreach (array_reverse($this->parent_configs) as $parent_config) { // Base first, the immediate parent last.
  1410. if (file_exists("$parent_config->dir/fonts_core/$font")) {
  1411. return "$parent_config->dir/fonts_core/$font";
  1412. }
  1413. }
  1414. if (file_exists("$CFG->dataroot/fonts/$font")) {
  1415. return "$CFG->dataroot/fonts/$font";
  1416. }
  1417. if (file_exists("$CFG->dirroot/lib/fonts/$font")) {
  1418. return "$CFG->dirroot/lib/fonts/$font";
  1419. }
  1420. return null;
  1421. } else if ($component === 'theme') { // Exception.
  1422. if (file_exists("$this->dir/fonts/$font")) {
  1423. return "$this->dir/fonts/$font";
  1424. }
  1425. foreach (array_reverse($this->parent_configs) as $parent_config) { // Base first, the immediate parent last.
  1426. if (file_exists("$parent_config->dir/fonts/$font")) {
  1427. return "$parent_config->dir/fonts/$font";
  1428. }
  1429. }
  1430. return null;
  1431. } else {
  1432. if (strpos($component, '_') === false) {
  1433. $component = 'mod_'.$component;
  1434. }
  1435. list($type, $plugin) = explode('_', $component, 2);
  1436. if (file_exists("$this->dir/fonts_plugins/$type/$plugin/$font")) {
  1437. return "$this->dir/fonts_plugins/$type/$plugin/$font";
  1438. }
  1439. foreach (array_reverse($this->parent_configs) as $parent_config) { // Base first, the immediate parent last.
  1440. if (file_exists("$parent_config->dir/fonts_plugins/$type/$plugin/$font")) {
  1441. return "$parent_config->dir/fonts_plugins/$type/$plugin/$font";
  1442. }
  1443. }
  1444. if (file_exists("$CFG->dataroot/fonts_plugins/$type/$plugin/$font")) {
  1445. return "$CFG->dataroot/fonts_plugins/$type/$plugin/$font";
  1446. }
  1447. $dir = core_component::get_plugin_directory($type, $plugin);
  1448. if (file_exists("$dir/fonts/$font")) {
  1449. return "$dir/fonts/$font";
  1450. }
  1451. return null;
  1452. }
  1453. }
  1454. /**
  1455. * Return true if we should look for SVG images as well.
  1456. *
  1457. * @return bool
  1458. */
  1459. public function use_svg_icons() {
  1460. global $CFG;
  1461. if ($this->usesvg === null) {
  1462. if (!isset($CFG->svgicons) || !is_bool($CFG->svgicons)) {
  1463. $this->usesvg = core_useragent::supports_svg();
  1464. } else {
  1465. // Force them on/off depending upon the setting.
  1466. $this->usesvg = $CFG->svgicons;
  1467. }
  1468. }
  1469. return $this->usesvg;
  1470. }
  1471. /**
  1472. * Forces the usesvg setting to either true or false, avoiding any decision making.
  1473. *
  1474. * This function should only ever be used when absolutely required, and before any generation of image URL's has occurred.
  1475. * DO NOT ABUSE THIS FUNCTION... not that you'd want to right ;)
  1476. *
  1477. * @param bool $setting True to force the use of svg when available, null otherwise.
  1478. */
  1479. public function force_svg_use($setting) {
  1480. $this->usesvg = (bool)$setting;
  1481. }
  1482. /**
  1483. * Checks if file with any image extension exists.
  1484. *
  1485. * The order to these images was adjusted prior to the release of 2.4
  1486. * At that point the were the following image counts in Moodle core:
  1487. *
  1488. * - png = 667 in pix dirs (1499 total)
  1489. * - gif = 385 in pix dirs (606 total)
  1490. * - jpg = 62 in pix dirs (74 total)
  1491. * - jpeg = 0 in pix dirs (1 total)
  1492. *
  1493. * There is work in progress to move towards SVG presently hence that has been prioritiesed.
  1494. *
  1495. * @param string $filepath
  1496. * @param bool $svg If set to true SVG images will also be looked for.
  1497. * @return string image name with extension
  1498. */
  1499. private static function image_exists($filepath, $svg = false) {
  1500. if ($svg && file_exists("$filepath.svg")) {
  1501. return "$filepath.svg";
  1502. } else if (file_exists("$filepath.png")) {
  1503. return "$filepath.png";
  1504. } else if (file_exists("$filepath.gif")) {
  1505. return "$filepath.gif";
  1506. } else if (file_exists("$filepath.jpg")) {
  1507. return "$filepath.jpg";
  1508. } else if (file_exists("$filepath.jpeg")) {
  1509. return "$filepath.jpeg";
  1510. } else {
  1511. return false;
  1512. }
  1513. }
  1514. /**
  1515. * Loads the theme config from config.php file.
  1516. *
  1517. * @param string $themename
  1518. * @param stdClass $settings from config_plugins table
  1519. * @param boolean $parentscheck true to also check the parents. .
  1520. * @return stdClass The theme configuration
  1521. */
  1522. private static function find_theme_config($themename, $settings, $parentscheck = true) {
  1523. // We have to use the variable name $THEME (upper case) because that
  1524. // is what is used in theme config.php files.
  1525. if (!$dir = theme_config::find_theme_location($themename)) {
  1526. return null;
  1527. }
  1528. $THEME = new stdClass();
  1529. $THEME->name = $themename;
  1530. $THEME->dir = $dir;
  1531. $THEME->settings = $settings;
  1532. global $CFG; // just in case somebody tries to use $CFG in theme config
  1533. include("$THEME->dir/config.php");
  1534. // verify the theme configuration is OK
  1535. if (!is_array($THEME->parents)) {
  1536. // parents option is mandatory now
  1537. return null;
  1538. } else {
  1539. // We use $parentscheck to only check the direct parents (avoid infinite loop).
  1540. if ($parentscheck) {
  1541. // Find all parent theme configs.
  1542. foreach ($THEME->parents as $parent) {
  1543. $parentconfig = theme_config::find_theme_config($parent, $settings, false);
  1544. if (empty($parentconfig)) {
  1545. return null;
  1546. }
  1547. }
  1548. }
  1549. }
  1550. return $THEME;
  1551. }
  1552. /**
  1553. * Finds the theme location and verifies the theme has all needed files
  1554. * and is not obsoleted.
  1555. *
  1556. * @param string $themename
  1557. * @return string full dir path or null if not found
  1558. */
  1559. private static function find_theme_location($themename) {
  1560. global $CFG;
  1561. if (file_exists("$CFG->dirroot/theme/$themename/config.php")) {
  1562. $dir = "$CFG->dirroot/theme/$themename";
  1563. } else if (!empty($CFG->themedir) and file_exists("$CFG->themedir/$themename/config.php")) {
  1564. $dir = "$CFG->themedir/$themename";
  1565. } else {
  1566. return null;
  1567. }
  1568. if (file_exists("$dir/styles.php")) {
  1569. //legacy theme - needs to be upgraded - upgrade info is displayed on the admin settings page
  1570. return null;
  1571. }
  1572. return $dir;
  1573. }
  1574. /**
  1575. * Get the renderer for a part of Moodle for this theme.
  1576. *
  1577. * @param moodle_page $page the page we are rendering
  1578. * @param string $component the name of part of moodle. E.g. 'core', 'quiz', 'qtype_multichoice'.
  1579. * @param string $subtype optional subtype such as 'news' resulting to 'mod_forum_news'
  1580. * @param string $target one of rendering target constants
  1581. * @return renderer_base the requested renderer.
  1582. */
  1583. public function get_renderer(moodle_page $page, $component, $subtype = null, $target = null) {
  1584. if (is_null($this->rf)) {
  1585. $classname = $this->rendererfactory;
  1586. $this->rf = new $classname($this);
  1587. }
  1588. return $this->rf->get_renderer($page, $component, $subtype, $target);
  1589. }
  1590. /**
  1591. * Get the information from {@link $layouts} for this type of page.
  1592. *
  1593. * @param string $pagelayout the the page layout name.
  1594. * @return array the appropriate part of {@link $layouts}.
  1595. */
  1596. protected function layout_info_for_page($pagelayout) {
  1597. if (array_key_exists($pagelayout, $this->layouts)) {
  1598. return $this->layouts[$pagelayout];
  1599. } else {
  1600. debugging('Invalid page layout specified: ' . $pagelayout);
  1601. return $this->layouts['standard'];
  1602. }
  1603. }
  1604. /**
  1605. * Given the settings of this theme, and the page pagelayout, return the
  1606. * full path of the page layout file to use.
  1607. *
  1608. * Used by {@link core_renderer::header()}.
  1609. *
  1610. * @param string $pagelayout the the page layout name.
  1611. * @return string Full path to the lyout file to use
  1612. */
  1613. public function layout_file($pagelayout) {
  1614. global $CFG;
  1615. $layoutinfo = $this->layout_info_for_page($pagelayout);
  1616. $layoutfile = $layoutinfo['file'];
  1617. if (array_key_exists('theme', $layoutinfo)) {
  1618. $themes = array($layoutinfo['theme']);
  1619. } else {
  1620. $themes = array_merge(array($this->name),$this->parents);
  1621. }
  1622. foreach ($themes as $theme) {
  1623. if ($dir = $this->find_theme_location($theme)) {
  1624. $path = "$dir/layout/$layoutfile";
  1625. // Check the template exists, return general base theme template if not.
  1626. if (is_readable($path)) {
  1627. return $path;
  1628. }
  1629. }
  1630. }
  1631. debugging('Can not find layout file for: ' . $pagelayout);
  1632. // fallback to standard normal layout
  1633. return "$CFG->dirroot/theme/base/layout/general.php";
  1634. }
  1635. /**
  1636. * Returns auxiliary page layout options specified in layout configuration array.
  1637. *
  1638. * @param string $pagelayout
  1639. * @return array
  1640. */
  1641. public function pagelayout_options($pagelayout) {
  1642. $info = $this->layout_info_for_page($pagelayout);
  1643. if (!empty($info['options'])) {
  1644. return $info['options'];
  1645. }
  1646. return array();
  1647. }
  1648. /**
  1649. * Inform a block_manager about the block regions this theme wants on this
  1650. * page layout.
  1651. *
  1652. * @param string $pagelayout the general type of the page.
  1653. * @param block_manager $blockmanager the block_manger to set up.
  1654. */
  1655. public function setup_blocks($pagelayout, $blockmanager) {
  1656. $layoutinfo = $this->layout_info_for_page($pagelayout);
  1657. if (!empty($layoutinfo['regions'])) {
  1658. $blockmanager->add_regions($layoutinfo['regions'], false);
  1659. $blockmanager->set_default_region($layoutinfo['defaultregion']);
  1660. }
  1661. }
  1662. /**
  1663. * Gets the visible name for the requested block region.
  1664. *
  1665. * @param string $region The region name to get
  1666. * @param string $theme The theme the region belongs to (may come from the parent theme)
  1667. * @return string
  1668. */
  1669. protected function get_region_name($region, $theme) {
  1670. $regionstring = get_string('region-' . $region, 'theme_' . $theme);
  1671. // A name exists in this theme, so use it
  1672. if (substr($regionstring, 0, 1) != '[') {
  1673. return $regionstring;
  1674. }
  1675. // Otherwise, try to find one elsewhere
  1676. // Check parents, if any
  1677. foreach ($this->parents as $parentthemename) {
  1678. $regionstring = get_string('region-' . $region, 'theme_' . $parentthemename);
  1679. if (substr($regionstring, 0, 1) != '[') {
  1680. return $regionstring;
  1681. }
  1682. }
  1683. // Last resort, try the base theme for names
  1684. return get_string('region-' . $region, 'theme_base');
  1685. }
  1686. /**
  1687. * Get the list of all block regions known to this theme in all templates.
  1688. *
  1689. * @return array internal region name => human readable name.
  1690. */
  1691. public function get_all_block_regions() {
  1692. $regions = array();
  1693. foreach ($this->layouts as $layoutinfo) {
  1694. foreach ($layoutinfo['regions'] as $region) {
  1695. $regions[$region] = $this->get_region_name($region, $this->name);
  1696. }
  1697. }
  1698. return $regions;
  1699. }
  1700. /**
  1701. * Returns the human readable name of the theme
  1702. *
  1703. * @return string
  1704. */
  1705. public function get_theme_name() {
  1706. return get_string('pluginname', 'theme_'.$this->name);
  1707. }
  1708. /**
  1709. * Returns the block render method.
  1710. *
  1711. * It is set by the theme via:
  1712. * $THEME->blockrendermethod = '...';
  1713. *
  1714. * It can be one of two values, blocks or blocks_for_region.
  1715. * It should be set to the method being used by the theme layouts.
  1716. *
  1717. * @return string
  1718. */
  1719. public function get_block_render_method() {
  1720. if ($this->blockrendermethod) {
  1721. // Return the specified block render method.
  1722. return $this->blockrendermethod;
  1723. }
  1724. // Its not explicitly set, check the parent theme configs.
  1725. foreach ($this->parent_configs as $config) {
  1726. if (isset($config->blockrendermethod)) {
  1727. return $config->blockrendermethod;
  1728. }
  1729. }
  1730. // Default it to blocks.
  1731. return 'blocks';
  1732. }
  1733. }
  1734. /**
  1735. * This class keeps track of which HTML tags are currently open.
  1736. *
  1737. * This makes it much easier to always generate well formed XHTML output, even
  1738. * if execution terminates abruptly. Any time you output some opening HTML
  1739. * without the matching closing HTML, you should push the necessary close tags
  1740. * onto the stack.
  1741. *
  1742. * @copyright 2009 Tim Hunt
  1743. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  1744. * @since Moodle 2.0
  1745. * @package core
  1746. * @category output
  1747. */
  1748. class xhtml_container_stack {
  1749. /**
  1750. * @var array Stores the list of open containers.
  1751. */
  1752. protected $opencontainers = array();
  1753. /**
  1754. * @var array In developer debug mode, stores a stack trace of all opens and
  1755. * closes, so we can output helpful error messages when there is a mismatch.
  1756. */
  1757. protected $log = array();
  1758. /**
  1759. * @var boolean Store whether we are developer debug mode. We need this in
  1760. * several places including in the destructor where we may not have access to $CFG.
  1761. */
  1762. protected $isdebugging;
  1763. /**
  1764. * Constructor
  1765. */
  1766. public function __construct() {
  1767. global $CFG;
  1768. $this->isdebugging = $CFG->debugdeveloper;
  1769. }
  1770. /**
  1771. * Push the close HTML for a recently opened container onto the stack.
  1772. *
  1773. * @param string $type The type of container. This is checked when {@link pop()}
  1774. * is called and must match, otherwise a developer debug warning is output.
  1775. * @param string $closehtml The HTML required to close the container.
  1776. */
  1777. public function push($type, $closehtml) {
  1778. $container = new stdClass;
  1779. $container->type = $type;
  1780. $container->closehtml = $closehtml;
  1781. if ($this->isdebugging) {
  1782. $this->log('Open', $type);
  1783. }
  1784. array_push($this->opencontainers, $container);
  1785. }
  1786. /**
  1787. * Pop the HTML for the next closing container from the stack. The $type
  1788. * must match the type passed when the container was opened, otherwise a
  1789. * warning will be output.
  1790. *
  1791. * @param string $type The type of container.
  1792. * @return string the HTML required to close the container.
  1793. */
  1794. public function pop($type) {
  1795. if (empty($this->opencontainers)) {
  1796. debugging('<p>There are no more open containers. This suggests there is a nesting problem.</p>' .
  1797. $this->output_log(), DEBUG_DEVELOPER);
  1798. return;
  1799. }
  1800. $container = array_pop($this->opencontainers);
  1801. if ($container->type != $type) {
  1802. debugging('<p>The type of container to be closed (' . $container->type .
  1803. ') does not match the type of the next open container (' . $type .
  1804. '). This suggests there is a nesting problem.</p>' .
  1805. $this->output_log(), DEBUG_DEVELOPER);
  1806. }
  1807. if ($this->isdebugging) {
  1808. $this->log('Close', $type);
  1809. }
  1810. return $container->closehtml;
  1811. }
  1812. /**
  1813. * Close all but the last open container. This is useful in places like error
  1814. * handling, where you want to close all the open containers (apart from <body>)
  1815. * before outputting the error message.
  1816. *
  1817. * @param bool $shouldbenone assert that the stack should be empty now - causes a
  1818. * developer debug warning if it isn't.
  1819. * @return string the HTML required to close any open containers inside <body>.
  1820. */
  1821. public function pop_all_but_last($shouldbenone = false) {
  1822. if ($shouldbenone && count($this->opencontainers) != 1) {
  1823. debugging('<p>Some HTML tags were opened in the body of the page but not closed.</p>' .
  1824. $this->output_log(), DEBUG_DEVELOPER);
  1825. }
  1826. $output = '';
  1827. while (count($this->opencontainers) > 1) {
  1828. $container = array_pop($this->opencontainers);
  1829. $output .= $container->closehtml;
  1830. }
  1831. return $output;
  1832. }
  1833. /**
  1834. * You can call this function if you want to throw away an instance of this
  1835. * class without properly emptying the stack (for example, in a unit test).
  1836. * Calling this method stops the destruct method from outputting a developer
  1837. * debug warning. After calling this method, the instance can no longer be used.
  1838. */
  1839. public function discard() {
  1840. $this->opencontainers = null;
  1841. }
  1842. /**
  1843. * Adds an entry to the log.
  1844. *
  1845. * @param string $action The name of the action
  1846. * @param string $type The type of action
  1847. */
  1848. protected function log($action, $type) {
  1849. $this->log[] = '<li>' . $action . ' ' . $type . ' at:' .
  1850. format_backtrace(debug_backtrace()) . '</li>';
  1851. }
  1852. /**
  1853. * Outputs the log's contents as a HTML list.
  1854. *
  1855. * @return string HTML list of the log
  1856. */
  1857. protected function output_log() {
  1858. return '<ul>' . implode("\n", $this->log) . '</ul>';
  1859. }
  1860. }