Ir a la última revisión | Autoría | Comparar con el anterior | Ultima modificación | Ver Log |
<?php// This file is part of Moodle - http://moodle.org///// Moodle is free software: you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// Moodle is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with Moodle. If not, see <http://www.gnu.org/licenses/>./*** Functions for generating the HTML that Moodle should output.** Please see http://docs.moodle.org/en/Developement:How_Moodle_outputs_HTML* for an overview.** @copyright 2009 Tim Hunt* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later* @package core* @category output*/defined('MOODLE_INTERNAL') || die();require_once($CFG->libdir.'/outputcomponents.php');require_once($CFG->libdir.'/outputactions.php');require_once($CFG->libdir.'/outputfactories.php');require_once($CFG->libdir.'/outputrenderers.php');require_once($CFG->libdir.'/outputrequirementslib.php');/*** Returns current theme revision number.** @return int*/function theme_get_revision() {global $CFG;if (empty($CFG->themedesignermode)) {if (empty($CFG->themerev)) {// This only happens during install. It doesn't matter what themerev we use as long as it's positive.return 1;} else {return $CFG->themerev;}} else {return -1;}}/*** Returns current theme sub revision number. This is the revision for* this theme exclusively, not the global theme revision.** @param string $themename The non-frankenstyle name of the theme* @return int*/function theme_get_sub_revision_for_theme($themename) {global $CFG;if (empty($CFG->themedesignermode)) {$pluginname = "theme_{$themename}";$revision = during_initial_install() ? null : get_config($pluginname, 'themerev');if (empty($revision)) {// This only happens during install. It doesn't matter what themerev we use as long as it's positive.return 1;} else {return $revision;}} else {return -1;}}/*** Calculates and returns the next theme revision number.** @return int*/function theme_get_next_revision() {global $CFG;$next = time();if (isset($CFG->themerev) and $next <= $CFG->themerev and $CFG->themerev - $next < 60*60) {// This resolves problems when reset is requested repeatedly within 1s,// the < 1h condition prevents accidental switching to future dates// because we might not recover from it.$next = $CFG->themerev+1;}return $next;}/*** Calculates and returns the next theme revision number.** @param string $themename The non-frankenstyle name of the theme* @return int*/function theme_get_next_sub_revision_for_theme($themename) {global $CFG;$next = time();$current = theme_get_sub_revision_for_theme($themename);if ($next <= $current and $current - $next < 60 * 60) {// This resolves problems when reset is requested repeatedly within 1s,// the < 1h condition prevents accidental switching to future dates// because we might not recover from it.$next = $current + 1;}return $next;}/*** Sets the current theme revision number.** @param int $revision The new theme revision number*/function theme_set_revision($revision) {set_config('themerev', $revision);}/*** Sets the current theme revision number for a specific theme.* This does not affect the global themerev value.** @param string $themename The non-frankenstyle name of the theme* @param int $revision The new theme revision number*/function theme_set_sub_revision_for_theme($themename, $revision) {set_config('themerev', $revision, "theme_{$themename}");}/*** Get the path to a theme config.php file.** @param string $themename The non-frankenstyle name of the theme to check*/function theme_get_config_file_path($themename) {global $CFG;if (file_exists("{$CFG->dirroot}/theme/{$themename}/config.php")) {return "{$CFG->dirroot}/theme/{$themename}/config.php";} else if (!empty($CFG->themedir) and file_exists("{$CFG->themedir}/{$themename}/config.php")) {return "{$CFG->themedir}/{$themename}/config.php";} else {return null;}}/*** Get the path to the local cached CSS file.** @param string $themename The non-frankenstyle theme name.* @param int $globalrevision The global theme revision.* @param int $themerevision The theme specific revision.* @param string $direction Either 'ltr' or 'rtl' (case sensitive).*/function theme_get_css_filename($themename, $globalrevision, $themerevision, $direction) {global $CFG;$path = "{$CFG->localcachedir}/theme/{$globalrevision}/{$themename}/css";$filename = $direction == 'rtl' ? "all-rtl_{$themerevision}" : "all_{$themerevision}";return "{$path}/{$filename}.css";}/*** Generates and saves the CSS files for the given theme configs.** @param theme_config[] $themeconfigs An array of theme_config instances.* @param array $directions Must be a subset of ['rtl', 'ltr'].* @param bool $cache Should the generated files be stored in local cache.* @return array The built theme content in a multi-dimensional array of name => direction => content*/function theme_build_css_for_themes($themeconfigs = [], $directions = ['rtl', 'ltr'],$cache = true, $mtraceprogress = false): array {global $CFG;if (empty($themeconfigs)) {return [];}require_once("{$CFG->libdir}/csslib.php");$themescss = [];$themerev = theme_get_revision();// Make sure the local cache directory exists.make_localcache_directory('theme');foreach ($themeconfigs as $themeconfig) {$themecss = [];$oldrevision = theme_get_sub_revision_for_theme($themeconfig->name);$newrevision = theme_get_next_sub_revision_for_theme($themeconfig->name);// First generate all the new css.foreach ($directions as $direction) {if ($mtraceprogress) {$timestart = microtime(true);mtrace('Building theme CSS for ' . $themeconfig->name . ' [' .$direction . '] ...', '');}// Lock it on. Technically we should build all themes for SVG and no SVG - but ie9 is out of support.$themeconfig->force_svg_use(true);$themeconfig->set_rtl_mode(($direction === 'rtl'));$themecss[$direction] = $themeconfig->get_css_content();if ($cache) {$themeconfig->set_css_content_cache($themecss[$direction]);$filename = theme_get_css_filename($themeconfig->name, $themerev, $newrevision, $direction);css_store_css($themeconfig, $filename, $themecss[$direction]);}if ($mtraceprogress) {mtrace(' done in ' . round(microtime(true) - $timestart, 2) . ' seconds.');}}$themescss[$themeconfig->name] = $themecss;if ($cache) {// Only update the theme revision after we've successfully created the// new CSS cache.theme_set_sub_revision_for_theme($themeconfig->name, $newrevision);// Now purge old files. We must purge all old files in the local cache// because we've incremented the theme sub revision. This will leave any// files with the old revision inaccessbile so we might as well removed// them from disk.foreach (['ltr', 'rtl'] as $direction) {$oldcss = theme_get_css_filename($themeconfig->name, $themerev, $oldrevision, $direction);if (file_exists($oldcss)) {unlink($oldcss);}}}}return $themescss;}/*** Invalidate all server and client side caches.** This method deletes the physical directory that is used to cache the theme* files used for serving.* Because it deletes the main theme cache directory all themes are reset by* this function.*/function theme_reset_all_caches() {global $CFG, $PAGE;require_once("{$CFG->libdir}/filelib.php");$next = theme_get_next_revision();theme_set_revision($next);if (!empty($CFG->themedesignermode)) {$cache = cache::make_from_params(cache_store::MODE_APPLICATION, 'core', 'themedesigner');$cache->purge();}// Purge compiled post processed css.cache::make('core', 'postprocessedcss')->purge();// Delete all old theme localcaches.$themecachedirs = glob("{$CFG->localcachedir}/theme/*", GLOB_ONLYDIR);foreach ($themecachedirs as $localcachedir) {fulldelete($localcachedir);}if ($PAGE) {$PAGE->reload_theme();}}/*** Reset static caches.** This method indicates that all running cron processes should exit at the* next opportunity.*/function theme_reset_static_caches() {\core\task\manager::clear_static_caches();}/*** Enable or disable theme designer mode.** @param bool $state*/function theme_set_designer_mod($state) {set_config('themedesignermode', (int)!empty($state));// Reset caches after switching mode so that any designer mode caches get purged too.theme_reset_all_caches();}/*** Purge theme used in context caches.*/function theme_purge_used_in_context_caches() {\cache::make('core', 'theme_usedincontext')->purge();}/*** Delete theme used in context cache for a particular theme.** When switching themes, both old and new theme caches are deleted.* This gives the query the opportunity to recache accurate results for both themes.** @param string $newtheme The incoming new theme.* @param string $oldtheme The theme that was already set.*/function theme_delete_used_in_context_cache(string $newtheme, string $oldtheme): void {if ((strlen($newtheme) > 0) && (strlen($oldtheme) > 0)) {// Theme -> theme.\cache::make('core', 'theme_usedincontext')->delete($oldtheme);\cache::make('core', 'theme_usedincontext')->delete($newtheme);} else {// No theme -> theme, or theme -> no theme.\cache::make('core', 'theme_usedincontext')->delete($newtheme . $oldtheme);}}/*** This class represents the configuration variables of a Moodle theme.** All the variables with access: public below (with a few exceptions that are marked)* are the properties you can set in your themes config.php file.** There are also some methods and protected variables that are part of the inner* workings of Moodle's themes system. If you are just editing a themes config.php* file, you can just ignore those, and the following information for developers.** Normally, to create an instance of this class, you should use the* {@link theme_config::load()} factory method to load a themes config.php file.* However, normally you don't need to bother, because moodle_page (that is, $PAGE)* will create one for you, accessible as $PAGE->theme.** @copyright 2009 Tim Hunt* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later* @since Moodle 2.0* @package core* @category output*/class theme_config {/*** @var string Default theme, used when requested theme not found.*/const DEFAULT_THEME = 'boost';/** The key under which the SCSS file is stored amongst the CSS files. */const SCSS_KEY = '__SCSS__';/*** @var array You can base your theme on other themes by linking to the other theme as* parents. This lets you use the CSS and layouts from the other themes* (see {@link theme_config::$layouts}).* That makes it easy to create a new theme that is similar to another one* but with a few changes. In this themes CSS you only need to override* those rules you want to change.*/public $parents;/*** @var array The names of all the stylesheets from this theme that you would* like included, in order. Give the names of the files without .css.*/public $sheets = array();/*** @var array The names of all the stylesheets from parents that should be excluded.* true value may be used to specify all parents or all themes from one parent.* If no value specified value from parent theme used.*/public $parents_exclude_sheets = null;/*** @var array List of plugin sheets to be excluded.* If no value specified value from parent theme used.*/public $plugins_exclude_sheets = null;/*** @var array List of style sheets that are included in the text editor bodies.* Sheets from parent themes are used automatically and can not be excluded.*/public $editor_sheets = array();/*** @var bool Whether a fallback version of the stylesheet will be used* whilst the final version is generated.*/public $usefallback = false;/*** @var array The names of all the javascript files this theme that you would* like included from head, in order. Give the names of the files without .js.*/public $javascripts = array();/*** @var array The names of all the javascript files this theme that you would* like included from footer, in order. Give the names of the files without .js.*/public $javascripts_footer = array();/*** @var array The names of all the javascript files from parents that should* be excluded. true value may be used to specify all parents or all themes* from one parent.* If no value specified value from parent theme used.*/public $parents_exclude_javascripts = null;/*** @var array Which file to use for each page layout.** This is an array of arrays. The keys of the outer array are the different layouts.* Pages in Moodle are using several different layouts like 'normal', 'course', 'home',* 'popup', 'form', .... The most reliable way to get a complete list is to look at* {@link http://cvs.moodle.org/moodle/theme/base/config.php?view=markup the base theme config.php file}.* That file also has a good example of how to set this setting.** For each layout, the value in the outer array is an array that describes* how you want that type of page to look. For example* <pre>* $THEME->layouts = array(* // Most pages - if we encounter an unknown or a missing page type, this one is used.* 'standard' => array(* 'theme' = 'mytheme',* 'file' => 'normal.php',* 'regions' => array('side-pre', 'side-post'),* 'defaultregion' => 'side-post'* ),* // The site home page.* 'home' => array(* 'theme' = 'mytheme',* 'file' => 'home.php',* 'regions' => array('side-pre', 'side-post'),* 'defaultregion' => 'side-post'* ),* // ...* );* </pre>** 'theme' name of the theme where is the layout located* 'file' is the layout file to use for this type of page.* layout files are stored in layout subfolder* 'regions' This lists the regions on the page where blocks may appear. For* each region you list here, your layout file must include a call to* <pre>* echo $OUTPUT->blocks_for_region($regionname);* </pre>* or equivalent so that the blocks are actually visible.** 'defaultregion' If the list of regions is non-empty, then you must pick* one of the one of them as 'default'. This has two meanings. First, this is* where new blocks are added. Second, if there are any blocks associated with* the page, but in non-existent regions, they appear here. (Imaging, for example,* that someone added blocks using a different theme that used different region* names, and then switched to this theme.)*/public $layouts = array();/*** @var string Name of the renderer factory class to use. Must implement the* {@link renderer_factory} interface.** This is an advanced feature. Moodle output is generated by 'renderers',* you can customise the HTML that is output by writing custom renderers,* and then you need to specify 'renderer factory' so that Moodle can find* your renderers.** There are some renderer factories supplied with Moodle. Please follow these* links to see what they do.* <ul>* <li>{@link standard_renderer_factory} - the default.</li>* <li>{@link theme_overridden_renderer_factory} - use this if you want to write* your own custom renderers in a lib.php file in this theme (or the parent theme).</li>* </ul>*/public $rendererfactory = 'standard_renderer_factory';/*** @var string Function to do custom CSS post-processing.** This is an advanced feature. If you want to do custom post-processing on the* CSS before it is output (for example, to replace certain variable names* with particular values) you can give the name of a function here.*/public $csspostprocess = null;/*** @var string Function to do custom CSS post-processing on a parsed CSS tree.** This is an advanced feature. If you want to do custom post-processing on the* CSS before it is output, you can provide the name of the function here. The* function will receive a CSS tree document as first parameter, and the theme_config* object as second parameter. A return value is not required, the tree can* be edited in place.*/public $csstreepostprocessor = null;/*** @var string Accessibility: Right arrow-like character is* used in the breadcrumb trail, course navigation menu* (previous/next activity), calendar, and search forum block.* If the theme does not set characters, appropriate defaults* are set automatically. Please DO NOT* use < > » - these are confusing for blind users.*/public $rarrow = null;/*** @var string Accessibility: Left arrow-like character is* used in the breadcrumb trail, course navigation menu* (previous/next activity), calendar, and search forum block.* If the theme does not set characters, appropriate defaults* are set automatically. Please DO NOT* use < > » - these are confusing for blind users.*/public $larrow = null;/*** @var string Accessibility: Up arrow-like character is used in* the book heirarchical navigation.* If the theme does not set characters, appropriate defaults* are set automatically. Please DO NOT* use ^ - this is confusing for blind users.*/public $uarrow = null;/*** @var string Accessibility: Down arrow-like character.* If the theme does not set characters, appropriate defaults* are set automatically.*/public $darrow = null;/*** @var bool Some themes may want to disable ajax course editing.*/public $enablecourseajax = true;/*** @var string Determines served document types* - 'html5' the only officially supported doctype in Moodle* - 'xhtml5' may be used in development for validation (not intended for production servers!)* - 'xhtml' XHTML 1.0 Strict for legacy themes only*/public $doctype = 'html5';/*** @var string|false requiredblocks If set to a string, will list the block types that cannot be deleted. Defaults to* navigation and settings.*/public $requiredblocks = false;//==Following properties are not configurable from theme config.php==/*** @var string The name of this theme. Set automatically when this theme is* loaded. This can not be set in theme config.php*/public $name;/*** @var string The folder where this themes files are stored. This is set* automatically. This can not be set in theme config.php*/public $dir;/*** @var stdClass Theme settings stored in config_plugins table.* This can not be set in theme config.php*/public $settings = null;/*** @var bool If set to true and the theme enables the dock then blocks will be able* to be moved to the special dock*/public $enable_dock = false;/*** @var bool If set to true then this theme will not be shown in the theme selector unless* theme designer mode is turned on.*/public $hidefromselector = false;/*** @var array list of YUI CSS modules to be included on each page. This may be used* to remove cssreset and use cssnormalise module instead.*/public $yuicssmodules = array('cssreset', 'cssfonts', 'cssgrids', 'cssbase');/*** An associative array of block manipulations that should be made if the user is using an rtl language.* The key is the original block region, and the value is the block region to change to.* This is used when displaying blocks for regions only.* @var array*/public $blockrtlmanipulations = array();/*** @var renderer_factory Instance of the renderer_factory implementation* we are using. Implementation detail.*/protected $rf = null;/*** @var array List of parent config objects.**/protected $parent_configs = array();/*** Used to determine whether we can serve SVG images or not.* @var bool*/private $usesvg = null;/*** Whether in RTL mode or not.* @var bool*/protected $rtlmode = false;/*** The SCSS file to compile (without .scss), located in the scss/ folder of the theme.* Or a Closure, which receives the theme_config as argument and must* return the SCSS content.* @var string|Closure*/public $scss = false;/*** Local cache of the SCSS property.* @var false|array*/protected $scsscache = null;/*** The name of the function to call to get the SCSS code to inject.* @var string*/public $extrascsscallback = null;/*** The name of the function to call to get SCSS to prepend.* @var string*/public $prescsscallback = null;/*** Sets the render method that should be used for rendering custom block regions by scripts such as my/index.php* Defaults to {@link core_renderer::blocks_for_region()}* @var string*/public $blockrendermethod = null;/*** Remember the results of icon remapping for the current page.* @var array*/public $remapiconcache = [];/*** The name of the function to call to get precompiled CSS.* @var string*/public $precompiledcsscallback = null;/*** Whether the theme uses course index.* @var bool*/public $usescourseindex = false;/*** Configuration for the page activity header* @var array*/public $activityheaderconfig = [];/*** For backward compatibility with old themes.* BLOCK_ADDBLOCK_POSITION_DEFAULT, BLOCK_ADDBLOCK_POSITION_FLATNAV.* @var int*/public $addblockposition;/*** editor_scss file(s) provided by this theme.* @var array*/public $editor_scss;/*** Name of the class extending \core\output\icon_system.* @var string*/public $iconsystem;/*** Theme defines its own editing mode switch.* @var bool*/public $haseditswitch = false;/*** Allows a theme to customise primary navigation by specifying the list of items to remove.* @var array*/public $removedprimarynavitems = [];/*** Load the config.php file for a particular theme, and return an instance* of this class. (That is, this is a factory method.)** @param string $themename the name of the theme.* @return theme_config an instance of this class.*/public static function load($themename) {global $CFG;// load theme settings from dbtry {$settings = get_config('theme_'.$themename);} catch (dml_exception $e) {// most probably moodle tables not created yet$settings = new stdClass();}if ($config = theme_config::find_theme_config($themename, $settings)) {return new theme_config($config);} else if ($themename == theme_config::DEFAULT_THEME) {throw new coding_exception('Default theme '.theme_config::DEFAULT_THEME.' not available or broken!');} else if ($config = theme_config::find_theme_config($CFG->theme, $settings)) {debugging('This page should be using theme ' . $themename .' which cannot be initialised. Falling back to the site theme ' . $CFG->theme, DEBUG_NORMAL);return new theme_config($config);} else {// bad luck, the requested theme has some problems - admin see details in theme configdebugging('This page should be using theme ' . $themename .' which cannot be initialised. Nor can the site theme ' . $CFG->theme .'. Falling back to ' . theme_config::DEFAULT_THEME, DEBUG_NORMAL);return new theme_config(theme_config::find_theme_config(theme_config::DEFAULT_THEME, $settings));}}/*** Theme diagnostic code. It is very problematic to send debug output* to the actual CSS file, instead this functions is supposed to* diagnose given theme and highlights all potential problems.* This information should be available from the theme selection page* or some other debug page for theme designers.** @param string $themename* @return array description of problems*/public static function diagnose($themename) {//TODO: MDL-21108return array();}/*** Private constructor, can be called only from the factory method.* @param stdClass $config*/private function __construct($config) {global $CFG; //needed for included lib.php files$this->settings = $config->settings;$this->name = $config->name;$this->dir = $config->dir;if ($this->name != self::DEFAULT_THEME) {$baseconfig = self::find_theme_config(self::DEFAULT_THEME, $this->settings);} else {$baseconfig = $config;}// Ensure that each of the configurable properties defined below are also defined at the class level.$configurable = ['parents', 'sheets', 'parents_exclude_sheets', 'plugins_exclude_sheets', 'usefallback','javascripts', 'javascripts_footer', 'parents_exclude_javascripts','layouts', 'enablecourseajax', 'requiredblocks','rendererfactory', 'csspostprocess', 'editor_sheets', 'editor_scss', 'rarrow', 'larrow', 'uarrow', 'darrow','hidefromselector', 'doctype', 'yuicssmodules', 'blockrtlmanipulations', 'blockrendermethod','scss', 'extrascsscallback', 'prescsscallback', 'csstreepostprocessor', 'addblockposition','iconsystem', 'precompiledcsscallback', 'haseditswitch', 'usescourseindex', 'activityheaderconfig','removedprimarynavitems',];foreach ($config as $key=>$value) {if (in_array($key, $configurable)) {$this->$key = $value;}}// verify all parents and load configs and renderersforeach ($this->parents as $parent) {if (!$parent_config = theme_config::find_theme_config($parent, $this->settings)) {// this is not good - better exclude faulty parentscontinue;}$libfile = $parent_config->dir.'/lib.php';if (is_readable($libfile)) {// theme may store various function hereinclude_once($libfile);}$renderersfile = $parent_config->dir.'/renderers.php';if (is_readable($renderersfile)) {// may contain core and plugin renderers and renderer factoryinclude_once($renderersfile);}$this->parent_configs[$parent] = $parent_config;}$libfile = $this->dir.'/lib.php';if (is_readable($libfile)) {// theme may store various function hereinclude_once($libfile);}$rendererfile = $this->dir.'/renderers.php';if (is_readable($rendererfile)) {// may contain core and plugin renderers and renderer factoryinclude_once($rendererfile);} else {// check if renderers.php file is missnamed renderer.phpif (is_readable($this->dir.'/renderer.php')) {debugging('Developer hint: '.$this->dir.'/renderer.php should be renamed to ' . $this->dir."/renderers.php.See: http://docs.moodle.org/dev/Output_renderers#Theme_renderers.", DEBUG_DEVELOPER);}}// cascade all layouts properlyforeach ($baseconfig->layouts as $layout=>$value) {if (!isset($this->layouts[$layout])) {foreach ($this->parent_configs as $parent_config) {if (isset($parent_config->layouts[$layout])) {$this->layouts[$layout] = $parent_config->layouts[$layout];continue 2;}}$this->layouts[$layout] = $value;}}//fix arrows if needed$this->check_theme_arrows();}/*** Let the theme initialise the page object (usually $PAGE).** This may be used for example to request jQuery in add-ons.** @param moodle_page $page*/public function init_page(moodle_page $page) {$themeinitfunction = 'theme_'.$this->name.'_page_init';if (function_exists($themeinitfunction)) {$themeinitfunction($page);}}/*** Checks if arrows $THEME->rarrow, $THEME->larrow, $THEME->uarrow, $THEME->darrow have been set (theme/-/config.php).* If not it applies sensible defaults.** Accessibility: right and left arrow Unicode characters for breadcrumb, calendar,* search forum block, etc. Important: these are 'silent' in a screen-reader* (unlike > »), and must be accompanied by text.*/private function check_theme_arrows() {if (!isset($this->rarrow) and !isset($this->larrow)) {// Default, looks good in Win XP/IE 6, Win/Firefox 1.5, Win/Netscape 8...// Also OK in Win 9x/2K/IE 5.x$this->rarrow = '►';$this->larrow = '◄';$this->uarrow = '▲';$this->darrow = '▼';if (empty($_SERVER['HTTP_USER_AGENT'])) {$uagent = '';} else {$uagent = $_SERVER['HTTP_USER_AGENT'];}if (false !== strpos($uagent, 'Opera')|| false !== strpos($uagent, 'Mac')) {// Looks good in Win XP/Mac/Opera 8/9, Mac/Firefox 2, Camino, Safari.// Not broken in Mac/IE 5, Mac/Netscape 7 (?).$this->rarrow = '▶︎';$this->larrow = '◀︎';}elseif ((false !== strpos($uagent, 'Konqueror'))|| (false !== strpos($uagent, 'Android'))) {// The fonts on Android don't include the characters required for this to work as expected.// So we use the same ones Konqueror uses.$this->rarrow = '→';$this->larrow = '←';$this->uarrow = '↑';$this->darrow = '↓';}elseif (isset($_SERVER['HTTP_ACCEPT_CHARSET'])&& false === stripos($_SERVER['HTTP_ACCEPT_CHARSET'], 'utf-8')) {// (Win/IE 5 doesn't set ACCEPT_CHARSET, but handles Unicode.)// To be safe, non-Unicode browsers!$this->rarrow = '>';$this->larrow = '<';$this->uarrow = '^';$this->darrow = 'v';}// RTL support - in RTL languages, swap r and l arrowsif (right_to_left()) {$t = $this->rarrow;$this->rarrow = $this->larrow;$this->larrow = $t;}}}/*** Returns output renderer prefixes, these are used when looking* for the overridden renderers in themes.** @return array*/public function renderer_prefixes() {global $CFG; // just in case the included files need it$prefixes = array('theme_'.$this->name);foreach ($this->parent_configs as $parent) {$prefixes[] = 'theme_'.$parent->name;}return $prefixes;}/*** Returns the stylesheet URL of this editor content** @param bool $encoded false means use & and true use & in URLs* @return moodle_url*/public function editor_css_url($encoded=true) {global $CFG;$rev = theme_get_revision();$type = 'editor';if (right_to_left()) {$type .= '-rtl';}if ($rev > -1) {$themesubrevision = theme_get_sub_revision_for_theme($this->name);// Provide the sub revision to allow us to invalidate cached theme CSS// on a per theme basis, rather than globally.if ($themesubrevision && $themesubrevision > 0) {$rev .= "_{$themesubrevision}";}$url = new moodle_url("/theme/styles.php");if (!empty($CFG->slasharguments)) {$url->set_slashargument("/{$this->name}/{$rev}/{$type}", 'noparam', true);} else {$url->params(['theme' => $this->name,'rev' => $rev,'type' => $type,]);}} else {$url = new moodle_url('/theme/styles_debug.php', ['theme' => $this->name,'type' => $type,]);}return $url;}/*** Returns the content of the CSS to be used in editor content** @return array*/public function editor_css_files() {$files = array();// First editor plugins.$plugins = core_component::get_plugin_list('editor');foreach ($plugins as $plugin => $fulldir) {$sheetfile = "$fulldir/editor_styles.css";if (is_readable($sheetfile)) {$files['plugin_'.$plugin] = $sheetfile;}$subplugintypes = core_component::get_subplugins("editor_{$plugin}") ?? [];// Fetch sheets for any editor subplugins.foreach ($subplugintypes as $plugintype => $subplugins) {foreach ($subplugins as $subplugin) {$plugindir = core_component::get_plugin_directory($plugintype, $subplugin);$sheetfile = "{$plugindir}/editor_styles.css";if (is_readable($sheetfile)) {$files["{$plugintype}_{$subplugin}"] = $sheetfile;}}}}// Then parent themes - base first, the immediate parent last.foreach (array_reverse($this->parent_configs) as $parent_config) {if (empty($parent_config->editor_sheets)) {continue;}foreach ($parent_config->editor_sheets as $sheet) {$sheetfile = "$parent_config->dir/style/$sheet.css";if (is_readable($sheetfile)) {$files['parent_'.$parent_config->name.'_'.$sheet] = $sheetfile;}}}// Finally this theme.if (!empty($this->editor_sheets)) {foreach ($this->editor_sheets as $sheet) {$sheetfile = "$this->dir/style/$sheet.css";if (is_readable($sheetfile)) {$files['theme_'.$sheet] = $sheetfile;}}}return $files;}/*** Compiles and returns the content of the SCSS to be used in editor content** @return string Compiled CSS from the editor SCSS*/public function editor_scss_to_css() {$css = '';$dir = $this->dir;$filenames = [];// Use editor_scss file(s) provided by this theme if set.if (!empty($this->editor_scss)) {$filenames = $this->editor_scss;} else {// If no editor_scss set, move up theme hierarchy until one is found (if at all).// This is so child themes only need to set editor_scss if an override is required.foreach (array_reverse($this->parent_configs) as $parentconfig) {if (!empty($parentconfig->editor_scss)) {$dir = $parentconfig->dir;$filenames = $parentconfig->editor_scss;// Config found, stop looking.break;}}}if (!empty($filenames)) {$compiler = new core_scss();foreach ($filenames as $filename) {$compiler->set_file("{$dir}/scss/{$filename}.scss");try {$css .= $compiler->to_css();} catch (\Exception $e) {debugging('Error while compiling editor SCSS: ' . $e->getMessage(), DEBUG_DEVELOPER);}}}return $css;}/*** Get the stylesheet URL of this theme.** @param moodle_page $page Not used... deprecated?* @return moodle_url[]*/public function css_urls(moodle_page $page) {global $CFG;$rev = theme_get_revision();$urls = array();$svg = $this->use_svg_icons();$separate = (core_useragent::is_ie() && !core_useragent::check_ie_version('10'));if ($rev > -1) {$filename = right_to_left() ? 'all-rtl' : 'all';$url = new moodle_url("/theme/styles.php");$themesubrevision = theme_get_sub_revision_for_theme($this->name);// Provide the sub revision to allow us to invalidate cached theme CSS// on a per theme basis, rather than globally.if ($themesubrevision && $themesubrevision > 0) {$rev .= "_{$themesubrevision}";}if (!empty($CFG->slasharguments)) {$slashargs = '';if (!$svg) {// We add a simple /_s to the start of the path.// The underscore is used to ensure that it isn't a valid theme name.$slashargs .= '/_s'.$slashargs;}$slashargs .= '/'.$this->name.'/'.$rev.'/'.$filename;if ($separate) {$slashargs .= '/chunk0';}$url->set_slashargument($slashargs, 'noparam', true);} else {$params = array('theme' => $this->name, 'rev' => $rev, 'type' => $filename);if (!$svg) {// We add an SVG param so that we know not to serve SVG images.// We do this because all modern browsers support SVG and this param will one day be removed.$params['svg'] = '0';}if ($separate) {$params['chunk'] = '0';}$url->params($params);}$urls[] = $url;} else {$baseurl = new moodle_url('/theme/styles_debug.php');$css = $this->get_css_files(true);if (!$svg) {// We add an SVG param so that we know not to serve SVG images.// We do this because all modern browsers support SVG and this param will one day be removed.$baseurl->param('svg', '0');}if (right_to_left()) {$baseurl->param('rtl', 1);}if ($separate) {// We might need to chunk long files.$baseurl->param('chunk', '0');}if (core_useragent::is_ie()) {// Lalala, IE does not allow more than 31 linked CSS files from main document.$urls[] = new moodle_url($baseurl, array('theme'=>$this->name, 'type'=>'ie', 'subtype'=>'plugins'));foreach ($css['parents'] as $parent=>$sheets) {// We need to serve parents individually otherwise we may easily exceed the style limit IE imposes (4096).$urls[] = new moodle_url($baseurl, array('theme'=>$this->name,'type'=>'ie', 'subtype'=>'parents', 'sheet'=>$parent));}if ($this->get_scss_property()) {// No need to define the type as IE here.$urls[] = new moodle_url($baseurl, array('theme' => $this->name, 'type' => 'scss'));}$urls[] = new moodle_url($baseurl, array('theme'=>$this->name, 'type'=>'ie', 'subtype'=>'theme'));} else {foreach ($css['plugins'] as $plugin=>$unused) {$urls[] = new moodle_url($baseurl, array('theme'=>$this->name,'type'=>'plugin', 'subtype'=>$plugin));}foreach ($css['parents'] as $parent=>$sheets) {foreach ($sheets as $sheet=>$unused2) {$urls[] = new moodle_url($baseurl, array('theme'=>$this->name,'type'=>'parent', 'subtype'=>$parent, 'sheet'=>$sheet));}}foreach ($css['theme'] as $sheet => $filename) {if ($sheet === self::SCSS_KEY) {// This is the theme SCSS file.$urls[] = new moodle_url($baseurl, array('theme' => $this->name, 'type' => 'scss'));} else {// Sheet first in order to make long urls easier to read.$urls[] = new moodle_url($baseurl, array('sheet'=>$sheet, 'theme'=>$this->name, 'type'=>'theme'));}}}}// Allow themes to change the css url to something like theme/mytheme/mycss.php.component_callback('theme_' . $this->name, 'alter_css_urls', [&$urls]);return $urls;}/*** Get the whole css stylesheet for production mode.** NOTE: this method is not expected to be used from any addons.** @return string CSS markup compressed*/public function get_css_content() {$csscontent = '';foreach ($this->get_css_files(false) as $type => $value) {foreach ($value as $identifier => $val) {if (is_array($val)) {foreach ($val as $v) {$csscontent .= file_get_contents($v) . "\n";}} else {if ($type === 'theme' && $identifier === self::SCSS_KEY) {// We need the content from SCSS because this is the SCSS file from the theme.if ($compiled = $this->get_css_content_from_scss(false)) {$csscontent .= $compiled;} else {// The compiler failed so default back to any precompiled css that might// exist.$csscontent .= $this->get_precompiled_css_content();}} else {$csscontent .= file_get_contents($val) . "\n";}}}}$csscontent = $this->post_process($csscontent);$csscontent = core_minify::css($csscontent);return $csscontent;}/*** Set post processed CSS content cache.** @param string $csscontent The post processed CSS content.* @return bool True if the content was successfully cached.*/public function set_css_content_cache($csscontent) {$cache = cache::make('core', 'postprocessedcss');$key = $this->get_css_cache_key();return $cache->set($key, $csscontent);}/*** Return whether the post processed CSS content has been cached.** @return bool Whether the post-processed CSS is available in the cache.*/public function has_css_cached_content() {$key = $this->get_css_cache_key();$cache = cache::make('core', 'postprocessedcss');return $cache->has($key);}/*** Return cached post processed CSS content.** @return bool|string The cached css content or false if not found.*/public function get_css_cached_content() {$key = $this->get_css_cache_key();$cache = cache::make('core', 'postprocessedcss');return $cache->get($key);}/*** Generate the css content cache key.** @return string The post processed css cache key.*/public function get_css_cache_key() {$nosvg = (!$this->use_svg_icons()) ? 'nosvg_' : '';$rtlmode = ($this->rtlmode == true) ? 'rtl' : 'ltr';return $nosvg . $this->name . '_' . $rtlmode;}/*** Get the theme designer css markup,* the parameters are coming from css_urls().** NOTE: this method is not expected to be used from any addons.** @param string $type* @param string $subtype* @param string $sheet* @return string CSS markup*/public function get_css_content_debug($type, $subtype, $sheet) {if ($type === 'scss') {// The SCSS file of the theme is requested.$csscontent = $this->get_css_content_from_scss(true);if ($csscontent !== false) {return $this->post_process($csscontent);}return '';}$cssfiles = array();$css = $this->get_css_files(true);if ($type === 'ie') {// IE is a sloppy browser with weird limits, sorry.if ($subtype === 'plugins') {$cssfiles = $css['plugins'];} else if ($subtype === 'parents') {if (empty($sheet)) {// Do not bother with the empty parent here.} else {// Build up the CSS for that parent so we can serve it as one file.foreach ($css[$subtype][$sheet] as $parent => $css) {$cssfiles[] = $css;}}} else if ($subtype === 'theme') {$cssfiles = $css['theme'];foreach ($cssfiles as $key => $value) {if (in_array($key, [self::SCSS_KEY])) {// Remove the SCSS file from the theme CSS files.// The SCSS files use the type 'scss', not 'ie'.unset($cssfiles[$key]);}}}} else if ($type === 'plugin') {if (isset($css['plugins'][$subtype])) {$cssfiles[] = $css['plugins'][$subtype];}} else if ($type === 'parent') {if (isset($css['parents'][$subtype][$sheet])) {$cssfiles[] = $css['parents'][$subtype][$sheet];}} else if ($type === 'theme') {if (isset($css['theme'][$sheet])) {$cssfiles[] = $css['theme'][$sheet];}}$csscontent = '';foreach ($cssfiles as $file) {$contents = file_get_contents($file);$contents = $this->post_process($contents);$comment = "/** Path: $type $subtype $sheet.' **/\n";$stats = '';$csscontent .= $comment.$stats.$contents."\n\n";}return $csscontent;}/*** Get the whole css stylesheet for editor iframe.** NOTE: this method is not expected to be used from any addons.** @return string CSS markup*/public function get_css_content_editor() {$css = '';$cssfiles = $this->editor_css_files();// If editor has static CSS, include it.foreach ($cssfiles as $file) {$css .= file_get_contents($file)."\n";}// If editor has SCSS, compile and include it.if (($convertedscss = $this->editor_scss_to_css())) {$css .= $convertedscss;}$output = $this->post_process($css);return $output;}/*** Returns an array of organised CSS files required for this output.** @param bool $themedesigner* @return array nested array of file paths*/protected function get_css_files($themedesigner) {global $CFG;$cache = null;$cachekey = 'cssfiles';if ($themedesigner) {require_once($CFG->dirroot.'/lib/csslib.php');// We need some kind of caching here because otherwise the page navigation becomes// way too slow in theme designer mode. Feel free to create full cache definition later...$cache = cache::make_from_params(cache_store::MODE_APPLICATION, 'core', 'themedesigner', array('theme' => $this->name));if ($files = $cache->get($cachekey)) {if ($files['created'] > time() - THEME_DESIGNER_CACHE_LIFETIME) {unset($files['created']);return $files;}}}$cssfiles = array('plugins'=>array(), 'parents'=>array(), 'theme'=>array());// Get all plugin sheets.$excludes = $this->resolve_excludes('plugins_exclude_sheets');if ($excludes !== true) {foreach (core_component::get_plugin_types() as $type=>$unused) {if ($type === 'theme' || (!empty($excludes[$type]) and $excludes[$type] === true)) {continue;}$plugins = core_component::get_plugin_list($type);foreach ($plugins as $plugin=>$fulldir) {if (!empty($excludes[$type]) and is_array($excludes[$type])and in_array($plugin, $excludes[$type])) {continue;}// Get the CSS from the plugin.$sheetfile = "$fulldir/styles.css";if (is_readable($sheetfile)) {$cssfiles['plugins'][$type.'_'.$plugin] = $sheetfile;}// Create a list of candidate sheets from parents (direct parent last) and current theme.$candidates = array();foreach (array_reverse($this->parent_configs) as $parent_config) {$candidates[] = $parent_config->name;}$candidates[] = $this->name;// Add the sheets found.foreach ($candidates as $candidate) {$sheetthemefile = "$fulldir/styles_{$candidate}.css";if (is_readable($sheetthemefile)) {$cssfiles['plugins'][$type.'_'.$plugin.'_'.$candidate] = $sheetthemefile;}}}}}// Find out wanted parent sheets.$excludes = $this->resolve_excludes('parents_exclude_sheets');if ($excludes !== true) {foreach (array_reverse($this->parent_configs) as $parent_config) { // Base first, the immediate parent last.$parent = $parent_config->name;if (empty($parent_config->sheets) || (!empty($excludes[$parent]) and $excludes[$parent] === true)) {continue;}foreach ($parent_config->sheets as $sheet) {if (!empty($excludes[$parent]) && is_array($excludes[$parent])&& in_array($sheet, $excludes[$parent])) {continue;}// We never refer to the parent LESS files.$sheetfile = "$parent_config->dir/style/$sheet.css";if (is_readable($sheetfile)) {$cssfiles['parents'][$parent][$sheet] = $sheetfile;}}}}// Current theme sheets.// We first add the SCSS file because we want the CSS ones to// be included after the SCSS code.if ($this->get_scss_property()) {$cssfiles['theme'][self::SCSS_KEY] = true;}if (is_array($this->sheets)) {foreach ($this->sheets as $sheet) {$sheetfile = "$this->dir/style/$sheet.css";if (is_readable($sheetfile) && !isset($cssfiles['theme'][$sheet])) {$cssfiles['theme'][$sheet] = $sheetfile;}}}if ($cache) {$files = $cssfiles;$files['created'] = time();$cache->set($cachekey, $files);}return $cssfiles;}/*** Return the CSS content generated from the SCSS file.** @param bool $themedesigner True if theme designer is enabled.* @return bool|string Return false when the compilation failed. Else the compiled string.*/protected function get_css_content_from_scss($themedesigner) {global $CFG;list($paths, $scss) = $this->get_scss_property();if (!$scss) {throw new coding_exception('The theme did not define a SCSS file, or it is not readable.');}// We might need more memory/time to do this, so let's play safe.raise_memory_limit(MEMORY_EXTRA);core_php_time_limit::raise(300);// TODO: MDL-62757 When changing anything in this method please do not forget to check// if the validate() method in class admin_setting_configthemepreset needs updating too.$cachedir = make_localcache_directory('scsscache-' . $this->name, false);$cacheoptions = [];if ($themedesigner) {$cacheoptions = array('cacheDir' => $cachedir,'prefix' => 'scssphp_','forceRefresh' => false,);} else {if (file_exists($cachedir)) {remove_dir($cachedir);}}// Set-up the compiler.$compiler = new core_scss($cacheoptions);if ($this->supports_source_maps($themedesigner)) {// Enable source maps.$compiler->setSourceMapOptions(['sourceMapBasepath' => str_replace('\\', '/', $CFG->dirroot),'sourceMapRootpath' => $CFG->wwwroot . '/']);$compiler->setSourceMap($compiler::SOURCE_MAP_INLINE);}$compiler->prepend_raw_scss($this->get_pre_scss_code());if (is_string($scss)) {$compiler->set_file($scss);} else {$compiler->append_raw_scss($scss($this));$compiler->setImportPaths($paths);}$compiler->append_raw_scss($this->get_extra_scss_code());try {// Compile!$compiled = $compiler->to_css();} catch (\Exception $e) {$compiled = false;debugging('Error while compiling SCSS: ' . $e->getMessage(), DEBUG_DEVELOPER);}// Try to save memory.$compiler = null;unset($compiler);return $compiled;}/*** Return the precompiled CSS if the precompiledcsscallback exists.** @return string Return compiled css.*/public function get_precompiled_css_content() {$configs = array_reverse($this->parent_configs) + [$this];$css = '';foreach ($configs as $config) {if (isset($config->precompiledcsscallback)) {$function = $config->precompiledcsscallback;if (function_exists($function)) {$css .= $function($this);}}}return $css;}/*** Get the icon system to use.** @return string*/public function get_icon_system() {// Getting all the candidate functions.$system = false;if (isset($this->iconsystem) && \core\output\icon_system::is_valid_system($this->iconsystem)) {return $this->iconsystem;}foreach ($this->parent_configs as $parent_config) {if (isset($parent_config->iconsystem) && \core\output\icon_system::is_valid_system($parent_config->iconsystem)) {return $parent_config->iconsystem;}}return \core\output\icon_system::STANDARD;}/*** Return extra SCSS code to add when compiling.** This is intended to be used by themes to inject some SCSS code* before it gets compiled. If you want to inject variables you* should use {@link self::get_scss_variables()}.** @return string The SCSS code to inject.*/public function get_extra_scss_code() {$content = '';// Getting all the candidate functions.$candidates = array();foreach (array_reverse($this->parent_configs) as $parent_config) {if (!isset($parent_config->extrascsscallback)) {continue;}$candidates[] = $parent_config->extrascsscallback;}if (isset($this->extrascsscallback)) {$candidates[] = $this->extrascsscallback;}// Calling the functions.foreach ($candidates as $function) {if (function_exists($function)) {$content .= "\n/** Extra SCSS from $function **/\n" . $function($this) . "\n";}}return $content;}/*** SCSS code to prepend when compiling.** This is intended to be used by themes to inject SCSS code before it gets compiled.** @return string The SCSS code to inject.*/public function get_pre_scss_code() {$content = '';// Getting all the candidate functions.$candidates = array();foreach (array_reverse($this->parent_configs) as $parent_config) {if (!isset($parent_config->prescsscallback)) {continue;}$candidates[] = $parent_config->prescsscallback;}if (isset($this->prescsscallback)) {$candidates[] = $this->prescsscallback;}// Calling the functions.foreach ($candidates as $function) {if (function_exists($function)) {$content .= "\n/** Pre-SCSS from $function **/\n" . $function($this) . "\n";}}return $content;}/*** Get the SCSS property.** This resolves whether a SCSS file (or content) has to be used when generating* the stylesheet for the theme. It will look at parents themes and check the* SCSS properties there.** @return array|false False when SCSS is not used.* An array with the import paths, and the path to the SCSS file or Closure as second.*/public function get_scss_property() {if ($this->scsscache === null) {$configs = [$this] + $this->parent_configs;$scss = null;foreach ($configs as $config) {$path = "{$config->dir}/scss";// We collect the SCSS property until we've found one.if (empty($scss) && !empty($config->scss)) {$candidate = is_string($config->scss) ? "{$path}/{$config->scss}.scss" : $config->scss;if ($candidate instanceof Closure) {$scss = $candidate;} else if (is_string($candidate) && is_readable($candidate)) {$scss = $candidate;}}// We collect the import paths once we've found a SCSS property.if ($scss && is_dir($path)) {$paths[] = $path;}}$this->scsscache = $scss !== null ? [$paths, $scss] : false;}return $this->scsscache;}/*** Generate a URL to the file that serves theme JavaScript files.** If we determine that the theme has no relevant files, then we return* early with a null value.** @param bool $inhead true means head url, false means footer* @return moodle_url|null*/public function javascript_url($inhead) {global $CFG;$rev = theme_get_revision();$params = array('theme'=>$this->name,'rev'=>$rev);$params['type'] = $inhead ? 'head' : 'footer';// Return early if there are no files to serveif (count($this->javascript_files($params['type'])) === 0) {return null;}if (!empty($CFG->slasharguments) and $rev > 0) {$url = new moodle_url("/theme/javascript.php");$url->set_slashargument('/'.$this->name.'/'.$rev.'/'.$params['type'], 'noparam', true);return $url;} else {return new moodle_url('/theme/javascript.php', $params);}}/*** Get the URL's for the JavaScript files used by this theme.* They won't be served directly, instead they'll be mediated through* theme/javascript.php.** @param string $type Either javascripts_footer, or javascripts* @return array*/public function javascript_files($type) {if ($type === 'footer') {$type = 'javascripts_footer';} else {$type = 'javascripts';}$js = array();// find out wanted parent javascripts$excludes = $this->resolve_excludes('parents_exclude_javascripts');if ($excludes !== true) {foreach (array_reverse($this->parent_configs) as $parent_config) { // base first, the immediate parent last$parent = $parent_config->name;if (empty($parent_config->$type)) {continue;}if (!empty($excludes[$parent]) and $excludes[$parent] === true) {continue;}foreach ($parent_config->$type as $javascript) {if (!empty($excludes[$parent]) and is_array($excludes[$parent])and in_array($javascript, $excludes[$parent])) {continue;}$javascriptfile = "$parent_config->dir/javascript/$javascript.js";if (is_readable($javascriptfile)) {$js[] = $javascriptfile;}}}}// current theme javascriptsif (is_array($this->$type)) {foreach ($this->$type as $javascript) {$javascriptfile = "$this->dir/javascript/$javascript.js";if (is_readable($javascriptfile)) {$js[] = $javascriptfile;}}}return $js;}/*** Resolves an exclude setting to the themes setting is applicable or the* setting of its closest parent.** @param string $variable The name of the setting the exclude setting to resolve* @param string $default* @return mixed*/protected function resolve_excludes($variable, $default = null) {$setting = $default;if (is_array($this->{$variable}) or $this->{$variable} === true) {$setting = $this->{$variable};} else {foreach ($this->parent_configs as $parent_config) { // the immediate parent first, base lastif (!isset($parent_config->{$variable})) {continue;}if (is_array($parent_config->{$variable}) or $parent_config->{$variable} === true) {$setting = $parent_config->{$variable};break;}}}return $setting;}/*** Returns the content of the one huge javascript file merged from all theme javascript files.** @param bool $type* @return string*/public function javascript_content($type) {$jsfiles = $this->javascript_files($type);$js = '';foreach ($jsfiles as $jsfile) {$js .= file_get_contents($jsfile)."\n";}return $js;}/*** Post processes CSS.** This method post processes all of the CSS before it is served for this theme.* This is done so that things such as image URL's can be swapped in and to* run any specific CSS post process method the theme has requested.* This allows themes to use CSS settings.** @param string $css The CSS to process.* @return string The processed CSS.*/public function post_process($css) {// now resolve all image locationsif (preg_match_all('/\[\[pix:([a-z0-9_]+\|)?([^\]]+)\]\]/', $css, $matches, PREG_SET_ORDER)) {$replaced = array();foreach ($matches as $match) {if (isset($replaced[$match[0]])) {continue;}$replaced[$match[0]] = true;$imagename = $match[2];$component = rtrim($match[1], '|');$imageurl = $this->image_url($imagename, $component)->out(false);// we do not need full url because the image.php is always in the same dir$imageurl = preg_replace('|^http.?://[^/]+|', '', $imageurl);$css = str_replace($match[0], $imageurl, $css);}}// Now resolve all font locations.if (preg_match_all('/\[\[font:([a-z0-9_]+\|)?([^\]]+)\]\]/', $css, $matches, PREG_SET_ORDER)) {$replaced = array();foreach ($matches as $match) {if (isset($replaced[$match[0]])) {continue;}$replaced[$match[0]] = true;$fontname = $match[2];$component = rtrim($match[1], '|');$fonturl = $this->font_url($fontname, $component)->out(false);// We do not need full url because the font.php is always in the same dir.$fonturl = preg_replace('|^http.?://[^/]+|', '', $fonturl);$css = str_replace($match[0], $fonturl, $css);}}// Now resolve all theme settings or do any other postprocessing.// This needs to be done before calling core parser, since the parser strips [[settings]] tags.$csspostprocess = $this->csspostprocess;if ($csspostprocess && function_exists($csspostprocess)) {$css = $csspostprocess($css, $this);}// Post processing using an object representation of CSS.$treeprocessor = $this->get_css_tree_post_processor();$needsparsing = !empty($treeprocessor) || !empty($this->rtlmode);if ($needsparsing) {// We might need more memory/time to do this, so let's play safe.raise_memory_limit(MEMORY_EXTRA);core_php_time_limit::raise(300);$parser = new core_cssparser($css);$csstree = $parser->parse();unset($parser);if ($this->rtlmode) {$this->rtlize($csstree);}if ($treeprocessor) {$treeprocessor($csstree, $this);}$css = $csstree->render();unset($csstree);}return $css;}/*** Flip a stylesheet to RTL.** @param mixed $csstree The parsed CSS tree structure to flip.* @return void*/protected function rtlize($csstree) {$rtlcss = new core_rtlcss($csstree);$rtlcss->flip();}/*** Return the direct URL for an image from the pix folder.** Use this function sparingly and never for icons. For icons use pix_icon or the pix helper in a mustache template.** @deprecated since Moodle 3.3* @param string $imagename the name of the icon.* @param string $component specification of one plugin like in get_string()* @return moodle_url*/public function pix_url($imagename, $component) {debugging('pix_url is deprecated. Use image_url for images and pix_icon for icons.', DEBUG_DEVELOPER);return $this->image_url($imagename, $component);}/*** Return the direct URL for an image from the pix folder.** Use this function sparingly and never for icons. For icons use pix_icon or the pix helper in a mustache template.** @param string $imagename the name of the icon.* @param string $component specification of one plugin like in get_string()* @return moodle_url*/public function image_url($imagename, $component) {global $CFG;$params = array('theme'=>$this->name);$svg = $this->use_svg_icons();if (empty($component) or $component === 'moodle' or $component === 'core') {$params['component'] = 'core';} else {$params['component'] = $component;}$rev = theme_get_revision();if ($rev != -1) {$params['rev'] = $rev;}$params['image'] = $imagename;$url = new moodle_url("/theme/image.php");if (!empty($CFG->slasharguments) and $rev > 0) {$path = '/'.$params['theme'].'/'.$params['component'].'/'.$params['rev'].'/'.$params['image'];if (!$svg) {// We add a simple /_s to the start of the path.// The underscore is used to ensure that it isn't a valid theme name.$path = '/_s'.$path;}$url->set_slashargument($path, 'noparam', true);} else {if (!$svg) {// We add an SVG param so that we know not to serve SVG images.// We do this because all modern browsers support SVG and this param will one day be removed.$params['svg'] = '0';}$url->params($params);}return $url;}/*** Return the URL for a font** @param string $font the name of the font (including extension).* @param string $component specification of one plugin like in get_string()* @return moodle_url*/public function font_url($font, $component) {global $CFG;$params = array('theme'=>$this->name);if (empty($component) or $component === 'moodle' or $component === 'core') {$params['component'] = 'core';} else {$params['component'] = $component;}$rev = theme_get_revision();if ($rev != -1) {$params['rev'] = $rev;}$params['font'] = $font;$url = new moodle_url("/theme/font.php");if (!empty($CFG->slasharguments) and $rev > 0) {$path = '/'.$params['theme'].'/'.$params['component'].'/'.$params['rev'].'/'.$params['font'];$url->set_slashargument($path, 'noparam', true);} else {$url->params($params);}return $url;}/*** Returns URL to the stored file via pluginfile.php.** Note the theme must also implement pluginfile.php handler,* theme revision is used instead of the itemid.** @param string $setting* @param string $filearea* @return string protocol relative URL or null if not present*/public function setting_file_url($setting, $filearea) {global $CFG;if (empty($this->settings->$setting)) {return null;}$component = 'theme_'.$this->name;$itemid = theme_get_revision();$filepath = $this->settings->$setting;$syscontext = context_system::instance();$url = moodle_url::make_file_url("$CFG->wwwroot/pluginfile.php", "/$syscontext->id/$component/$filearea/$itemid".$filepath);// Now this is tricky because the we can not hardcode http or https here, lets use the relative link.// Note: unfortunately moodle_url does not support //urls yet.$url = preg_replace('|^https?://|i', '//', $url->out(false));return $url;}/*** Serve the theme setting file.** @param string $filearea* @param array $args* @param bool $forcedownload* @param array $options* @return bool may terminate if file not found or donotdie not specified*/public function setting_file_serve($filearea, $args, $forcedownload, $options) {global $CFG;require_once("$CFG->libdir/filelib.php");$syscontext = context_system::instance();$component = 'theme_'.$this->name;$revision = array_shift($args);if ($revision < 0) {$lifetime = 0;} else {$lifetime = 60*60*24*60;// By default, theme files must be cache-able by both browsers and proxies.if (!array_key_exists('cacheability', $options)) {$options['cacheability'] = 'public';}}$fs = get_file_storage();$relativepath = implode('/', $args);$fullpath = "/{$syscontext->id}/{$component}/{$filearea}/0/{$relativepath}";$fullpath = rtrim($fullpath, '/');if ($file = $fs->get_file_by_hash(sha1($fullpath))) {send_stored_file($file, $lifetime, 0, $forcedownload, $options);return true;} else {send_file_not_found();}}/*** Resolves the real image location.** $svg was introduced as an arg in 2.4. It is important because not all supported browsers support the use of SVG* and we need a way in which to turn it off.* By default SVG won't be used unless asked for. This is done for two reasons:* 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* browser supports SVG.* 2. We only serve SVG images from locations we trust. This must NOT include any areas where the image may have been uploaded* by the user due to security concerns.** @param string $image name of image, may contain relative path* @param string $component* @param bool|null $svg Should SVG images also be looked for? If null, falls back to auto-detection of browser support* @return string full file path*/public function resolve_image_location($image, $component, $svg = false) {global $CFG;if (!is_bool($svg)) {// If $svg isn't a bool then we need to decide for ourselves.$svg = $this->use_svg_icons();}if ($component === 'moodle' or $component === 'core' or empty($component)) {if ($imagefile = $this->image_exists("$this->dir/pix_core/$image", $svg)) {return $imagefile;}foreach (array_reverse($this->parent_configs) as $parent_config) { // base first, the immediate parent lastif ($imagefile = $this->image_exists("$parent_config->dir/pix_core/$image", $svg)) {return $imagefile;}}if ($imagefile = $this->image_exists("$CFG->dataroot/pix/$image", $svg)) {return $imagefile;}if ($imagefile = $this->image_exists("$CFG->dirroot/pix/$image", $svg)) {return $imagefile;}return null;} else if ($component === 'theme') { //exceptionif ($image === 'favicon') {return "$this->dir/pix/favicon.ico";}if ($imagefile = $this->image_exists("$this->dir/pix/$image", $svg)) {return $imagefile;}foreach (array_reverse($this->parent_configs) as $parent_config) { // base first, the immediate parent lastif ($imagefile = $this->image_exists("$parent_config->dir/pix/$image", $svg)) {return $imagefile;}}return null;} else {if (strpos($component, '_') === false) {$component = "mod_{$component}";}list($type, $plugin) = explode('_', $component, 2);// In Moodle 4.0 we introduced a new image format.// Support that image format here.$candidates = [$image];if ($type === 'mod') {if ($image === 'icon' || $image === 'monologo') {$candidates = ['monologo', 'icon'];if ($image === 'icon') {debugging("The 'icon' image for activity modules has been replaced with a new 'monologo'. " ."Please update your calling code to fetch the new icon where possible. " ."Called for component {$component}.",DEBUG_DEVELOPER);}}}foreach ($candidates as $image) {if ($imagefile = $this->image_exists("$this->dir/pix_plugins/$type/$plugin/$image", $svg)) {return $imagefile;}// Base first, the immediate parent last.foreach (array_reverse($this->parent_configs) as $parentconfig) {if ($imagefile = $this->image_exists("$parentconfig->dir/pix_plugins/$type/$plugin/$image", $svg)) {return $imagefile;}}if ($imagefile = $this->image_exists("$CFG->dataroot/pix_plugins/$type/$plugin/$image", $svg)) {return $imagefile;}$dir = core_component::get_plugin_directory($type, $plugin);if ($imagefile = $this->image_exists("$dir/pix/$image", $svg)) {return $imagefile;}}return null;}}/*** Resolves the real font location.** @param string $font name of font file* @param string $component* @return string full file path*/public function resolve_font_location($font, $component) {global $CFG;if ($component === 'moodle' or $component === 'core' or empty($component)) {if (file_exists("$this->dir/fonts_core/$font")) {return "$this->dir/fonts_core/$font";}foreach (array_reverse($this->parent_configs) as $parent_config) { // Base first, the immediate parent last.if (file_exists("$parent_config->dir/fonts_core/$font")) {return "$parent_config->dir/fonts_core/$font";}}if (file_exists("$CFG->dataroot/fonts/$font")) {return "$CFG->dataroot/fonts/$font";}if (file_exists("$CFG->dirroot/lib/fonts/$font")) {return "$CFG->dirroot/lib/fonts/$font";}return null;} else if ($component === 'theme') { // Exception.if (file_exists("$this->dir/fonts/$font")) {return "$this->dir/fonts/$font";}foreach (array_reverse($this->parent_configs) as $parent_config) { // Base first, the immediate parent last.if (file_exists("$parent_config->dir/fonts/$font")) {return "$parent_config->dir/fonts/$font";}}return null;} else {if (strpos($component, '_') === false) {$component = 'mod_'.$component;}list($type, $plugin) = explode('_', $component, 2);if (file_exists("$this->dir/fonts_plugins/$type/$plugin/$font")) {return "$this->dir/fonts_plugins/$type/$plugin/$font";}foreach (array_reverse($this->parent_configs) as $parent_config) { // Base first, the immediate parent last.if (file_exists("$parent_config->dir/fonts_plugins/$type/$plugin/$font")) {return "$parent_config->dir/fonts_plugins/$type/$plugin/$font";}}if (file_exists("$CFG->dataroot/fonts_plugins/$type/$plugin/$font")) {return "$CFG->dataroot/fonts_plugins/$type/$plugin/$font";}$dir = core_component::get_plugin_directory($type, $plugin);if (file_exists("$dir/fonts/$font")) {return "$dir/fonts/$font";}return null;}}/*** Return true if we should look for SVG images as well.** @return bool*/public function use_svg_icons() {if ($this->usesvg === null) {$this->usesvg = core_useragent::supports_svg();}return $this->usesvg;}/*** Forces the usesvg setting to either true or false, avoiding any decision making.** This function should only ever be used when absolutely required, and before any generation of image URL's has occurred.* DO NOT ABUSE THIS FUNCTION... not that you'd want to right ;)** @param bool $setting True to force the use of svg when available, null otherwise.*/public function force_svg_use($setting) {$this->usesvg = (bool)$setting;}/*** Set to be in RTL mode.** This will likely be used when post processing the CSS before serving it.** @param bool $inrtl True when in RTL mode.*/public function set_rtl_mode($inrtl = true) {$this->rtlmode = $inrtl;}/*** Checks if source maps are supported** @param bool $themedesigner True if theme designer is enabled.* @return boolean True if source maps are supported.*/public function supports_source_maps($themedesigner): bool {if (empty($this->rtlmode) && $themedesigner) {return true;}return false;}/*** Whether the theme is being served in RTL mode.** @return bool True when in RTL mode.*/public function get_rtl_mode() {return $this->rtlmode;}/*** Checks if file with any image extension exists.** The order to these images was adjusted prior to the release of 2.4* At that point the were the following image counts in Moodle core:** - png = 667 in pix dirs (1499 total)* - gif = 385 in pix dirs (606 total)* - jpg = 62 in pix dirs (74 total)* - jpeg = 0 in pix dirs (1 total)** There is work in progress to move towards SVG presently hence that has been prioritiesed.** @param string $filepath* @param bool $svg If set to true SVG images will also be looked for.* @return string image name with extension*/private static function image_exists($filepath, $svg = false) {if ($svg && file_exists("$filepath.svg")) {return "$filepath.svg";} else if (file_exists("$filepath.png")) {return "$filepath.png";} else if (file_exists("$filepath.gif")) {return "$filepath.gif";} else if (file_exists("$filepath.jpg")) {return "$filepath.jpg";} else if (file_exists("$filepath.jpeg")) {return "$filepath.jpeg";} else {return false;}}/*** Loads the theme config from config.php file.** @param string $themename* @param stdClass $settings from config_plugins table* @param boolean $parentscheck true to also check the parents. .* @return ?stdClass The theme configuration*/private static function find_theme_config($themename, $settings, $parentscheck = true) {// We have to use the variable name $THEME (upper case) because that// is what is used in theme config.php files.if (!$dir = theme_config::find_theme_location($themename)) {return null;}$THEME = new stdClass();$THEME->name = $themename;$THEME->dir = $dir;$THEME->settings = $settings;global $CFG; // just in case somebody tries to use $CFG in theme configinclude("$THEME->dir/config.php");// verify the theme configuration is OKif (!is_array($THEME->parents)) {// parents option is mandatory nowreturn null;} else {// We use $parentscheck to only check the direct parents (avoid infinite loop).if ($parentscheck) {// Find all parent theme configs.foreach ($THEME->parents as $parent) {$parentconfig = theme_config::find_theme_config($parent, $settings, false);if (empty($parentconfig)) {return null;}}}}return $THEME;}/*** Finds the theme location and verifies the theme has all needed files* and is not obsoleted.** @param string $themename* @return string full dir path or null if not found*/private static function find_theme_location($themename) {global $CFG;if (file_exists("$CFG->dirroot/theme/$themename/config.php")) {$dir = "$CFG->dirroot/theme/$themename";} else if (!empty($CFG->themedir) and file_exists("$CFG->themedir/$themename/config.php")) {$dir = "$CFG->themedir/$themename";} else {return null;}if (file_exists("$dir/styles.php")) {//legacy theme - needs to be upgraded - upgrade info is displayed on the admin settings pagereturn null;}return $dir;}/*** Get the renderer for a part of Moodle for this theme.** @param moodle_page $page the page we are rendering* @param string $component the name of part of moodle. E.g. 'core', 'quiz', 'qtype_multichoice'.* @param string $subtype optional subtype such as 'news' resulting to 'mod_forum_news'* @param string $target one of rendering target constants* @return renderer_base the requested renderer.*/public function get_renderer(moodle_page $page, $component, $subtype = null, $target = null) {if (is_null($this->rf)) {$classname = $this->rendererfactory;$this->rf = new $classname($this);}return $this->rf->get_renderer($page, $component, $subtype, $target);}/*** Get the information from {@link $layouts} for this type of page.** @param string $pagelayout the the page layout name.* @return array the appropriate part of {@link $layouts}.*/protected function layout_info_for_page($pagelayout) {if (array_key_exists($pagelayout, $this->layouts)) {return $this->layouts[$pagelayout];} else {debugging('Invalid page layout specified: ' . $pagelayout);return $this->layouts['standard'];}}/*** Given the settings of this theme, and the page pagelayout, return the* full path of the page layout file to use.** Used by {@link core_renderer::header()}.** @param string $pagelayout the the page layout name.* @return string Full path to the lyout file to use*/public function layout_file($pagelayout) {global $CFG;$layoutinfo = $this->layout_info_for_page($pagelayout);$layoutfile = $layoutinfo['file'];if (array_key_exists('theme', $layoutinfo)) {$themes = array($layoutinfo['theme']);} else {$themes = array_merge(array($this->name),$this->parents);}foreach ($themes as $theme) {if ($dir = $this->find_theme_location($theme)) {$path = "$dir/layout/$layoutfile";// Check the template exists, return general base theme template if not.if (is_readable($path)) {return $path;}}}throw new coding_exception('Can not find layout file for: ' . $pagelayout . ' (' . $layoutfile . ')');}/*** Returns auxiliary page layout options specified in layout configuration array.** @param string $pagelayout* @return array*/public function pagelayout_options($pagelayout) {$info = $this->layout_info_for_page($pagelayout);if (!empty($info['options'])) {return $info['options'];}return array();}/*** Inform a block_manager about the block regions this theme wants on this* page layout.** @param string $pagelayout the general type of the page.* @param block_manager $blockmanager the block_manger to set up.*/public function setup_blocks($pagelayout, $blockmanager) {$layoutinfo = $this->layout_info_for_page($pagelayout);if (!empty($layoutinfo['regions'])) {$blockmanager->add_regions($layoutinfo['regions'], false);$blockmanager->set_default_region($layoutinfo['defaultregion']);}}/*** Gets the visible name for the requested block region.** @param string $region The region name to get* @param string $theme The theme the region belongs to (may come from the parent theme)* @return string*/protected function get_region_name($region, $theme) {$stringman = get_string_manager();// Check if the name is defined in the theme.if ($stringman->string_exists('region-' . $region, 'theme_' . $theme)) {return get_string('region-' . $region, 'theme_' . $theme);}// Check the theme parents.foreach ($this->parents as $parentthemename) {if ($stringman->string_exists('region-' . $region, 'theme_' . $parentthemename)) {return get_string('region-' . $region, 'theme_' . $parentthemename);}}// Last resort, try the boost theme for names.return get_string('region-' . $region, 'theme_boost');}/*** Get the list of all block regions known to this theme in all templates.** @return array internal region name => human readable name.*/public function get_all_block_regions() {$regions = array();foreach ($this->layouts as $layoutinfo) {foreach ($layoutinfo['regions'] as $region) {$regions[$region] = $this->get_region_name($region, $this->name);}}return $regions;}/*** Returns the human readable name of the theme** @return string*/public function get_theme_name() {return get_string('pluginname', 'theme_'.$this->name);}/*** Returns the block render method.** It is set by the theme via:* $THEME->blockrendermethod = '...';** It can be one of two values, blocks or blocks_for_region.* It should be set to the method being used by the theme layouts.** @return string*/public function get_block_render_method() {if ($this->blockrendermethod) {// Return the specified block render method.return $this->blockrendermethod;}// Its not explicitly set, check the parent theme configs.foreach ($this->parent_configs as $config) {if (isset($config->blockrendermethod)) {return $config->blockrendermethod;}}// Default it to blocks.return 'blocks';}/*** Get the callable for CSS tree post processing.** @return string|null*/public function get_css_tree_post_processor() {$configs = [$this] + $this->parent_configs;foreach ($configs as $config) {if (!empty($config->csstreepostprocessor) && is_callable($config->csstreepostprocessor)) {return $config->csstreepostprocessor;}}return null;}}/*** This class keeps track of which HTML tags are currently open.** This makes it much easier to always generate well formed XHTML output, even* if execution terminates abruptly. Any time you output some opening HTML* without the matching closing HTML, you should push the necessary close tags* onto the stack.** @copyright 2009 Tim Hunt* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later* @since Moodle 2.0* @package core* @category output*/class xhtml_container_stack {/*** @var array Stores the list of open containers.*/protected $opencontainers = array();/*** @var array In developer debug mode, stores a stack trace of all opens and* closes, so we can output helpful error messages when there is a mismatch.*/protected $log = array();/*** @var boolean Store whether we are developer debug mode. We need this in* several places including in the destructor where we may not have access to $CFG.*/protected $isdebugging;/*** Constructor*/public function __construct() {global $CFG;$this->isdebugging = $CFG->debugdeveloper;}/*** Push the close HTML for a recently opened container onto the stack.** @param string $type The type of container. This is checked when {@link pop()}* is called and must match, otherwise a developer debug warning is output.* @param string $closehtml The HTML required to close the container.*/public function push($type, $closehtml) {$container = new stdClass;$container->type = $type;$container->closehtml = $closehtml;if ($this->isdebugging) {$this->log('Open', $type);}array_push($this->opencontainers, $container);}/*** Pop the HTML for the next closing container from the stack. The $type* must match the type passed when the container was opened, otherwise a* warning will be output.** @param string $type The type of container.* @return ?string the HTML required to close the container.*/public function pop($type) {if (empty($this->opencontainers)) {debugging('<p>There are no more open containers. This suggests there is a nesting problem.</p>' .$this->output_log(), DEBUG_DEVELOPER);return;}$container = array_pop($this->opencontainers);if ($container->type != $type) {debugging('<p>The type of container to be closed (' . $container->type .') does not match the type of the next open container (' . $type .'). This suggests there is a nesting problem.</p>' .$this->output_log(), DEBUG_DEVELOPER);}if ($this->isdebugging) {$this->log('Close', $type);}return $container->closehtml;}/*** Close all but the last open container. This is useful in places like error* handling, where you want to close all the open containers (apart from <body>)* before outputting the error message.** @param bool $shouldbenone assert that the stack should be empty now - causes a* developer debug warning if it isn't.* @return string the HTML required to close any open containers inside <body>.*/public function pop_all_but_last($shouldbenone = false) {if ($shouldbenone && count($this->opencontainers) != 1) {debugging('<p>Some HTML tags were opened in the body of the page but not closed.</p>' .$this->output_log(), DEBUG_DEVELOPER);}$output = '';while (count($this->opencontainers) > 1) {$container = array_pop($this->opencontainers);$output .= $container->closehtml;}return $output;}/*** You can call this function if you want to throw away an instance of this* class without properly emptying the stack (for example, in a unit test).* Calling this method stops the destruct method from outputting a developer* debug warning. After calling this method, the instance can no longer be used.*/public function discard() {$this->opencontainers = null;}/*** Adds an entry to the log.** @param string $action The name of the action* @param string $type The type of action*/protected function log($action, $type) {$this->log[] = '<li>' . $action . ' ' . $type . ' at:' .format_backtrace(debug_backtrace()) . '</li>';}/*** Outputs the log's contents as a HTML list.** @return string HTML list of the log*/protected function output_log() {return '<ul>' . implode("\n", $this->log) . '</ul>';}}