PageRenderTime 59ms CodeModel.GetById 23ms 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

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. * 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…

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