Commit 0e9911e1 authored by Andrew Nicols's avatar Andrew Nicols Committed by David Monllao
Browse files

MDL-59148 theme: Use fallback if compiled version unavailable

parent 3b9b4731
......@@ -329,6 +329,26 @@ function css_send_cached_css_content($csscontent, $etag) {
die;
}
/**
* Sends CSS directly and disables all caching.
* The Content-Length of the body is also included, but the script is not ended.
*
* @param string $css The CSS content to send
* @param int $expiry The anticipated expiry of the file
*/
function css_send_temporary_css($css) {
header('Cache-Control: no-cache, no-store, must-revalidate');
header('Pragma: no-cache');
header('Expires: 0');
header('Content-Disposition: inline; filename="styles_debug.php"');
header('Last-Modified: '. gmdate('D, d M Y H:i:s', time()) .' GMT');
header('Accept-Ranges: none');
header('Content-Type: text/css; charset=utf-8');
header('Content-Length: ' . strlen($css));
echo $css;
}
/**
* Sends CSS directly without caching it.
*
......@@ -382,4 +402,4 @@ function css_send_unmodified($lastmodified, $etag) {
function css_send_css_not_found() {
header('HTTP/1.0 404 not found');
die('CSS was not found, sorry.');
}
\ No newline at end of file
}
......@@ -363,6 +363,12 @@ class theme_config {
*/
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.
......@@ -724,7 +730,7 @@ class theme_config {
}
$configurable = array(
'parents', 'sheets', 'parents_exclude_sheets', 'plugins_exclude_sheets',
'parents', 'sheets', 'parents_exclude_sheets', 'plugins_exclude_sheets', 'usefallback',
'javascripts', 'javascripts_footer', 'parents_exclude_javascripts',
'layouts', 'enable_dock', 'enablecourseajax', 'requiredblocks',
'rendererfactory', 'csspostprocess', 'editor_sheets', 'rarrow', 'larrow', 'uarrow', 'darrow',
......@@ -1102,6 +1108,19 @@ class theme_config {
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.
*
......@@ -2218,6 +2237,15 @@ class theme_config {
$this->rtlmode = $inrtl;
}
/**
* 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.
*
......
......@@ -29,6 +29,7 @@ require_once(__DIR__ . '/lib.php');
$THEME->name = 'boost';
$THEME->sheets = [];
$THEME->editor_sheets = [];
$THEME->usefallback = true;
$THEME->scss = function($theme) {
return theme_boost_get_main_scss_content($theme);
};
......
......@@ -28,6 +28,7 @@ $THEME->parents = array('clean', 'bootstrapbase');
$THEME->doctype = 'html5';
$THEME->sheets = array('custom');
$THEME->lessfile = 'moodle';
$THEME->usefallback = true;
$THEME->parents_exclude_sheets = array('bootstrapbase' => array('moodle'), 'clean' => array('custom'));
$THEME->lessvariablescallback = 'theme_more_less_variables';
$THEME->extralesscallback = 'theme_more_extra_less';
......
......@@ -77,6 +77,7 @@ if (is_null($themesubrev)) {
$themesubrev = min_clean_param($themesubrev, 'INT');
}
// Check that type fits into the expected values.
if ($type === 'editor') {
// The editor CSS is never chunked.
$chunk = null;
......@@ -96,25 +97,9 @@ if (file_exists("$CFG->dirroot/theme/$themename/config.php")) {
}
$candidatedir = "$CFG->localcachedir/theme/$rev/$themename/css";
$etag = "$rev/$themename/$type/$themesubrev";
$candidatesheet = "{$candidatedir}/{$type}";
if ($themesubrev > 0) {
$candidatesheet .= "_{$themesubrev}";
}
if (!$usesvg) {
// Add to the sheet name, one day we'll be able to just drop this.
$candidatesheet .= "-nosvg";
$etag .= '/nosvg';
}
if ($chunk !== null) {
$etag .= "/chunk{$chunk}";
$chunkedcandidatesheet = "{$candidatesheet}.{$chunk}.css";
$candidatesheet = "{$candidatesheet}.css";
} else {
$candidatesheet = $chunkedcandidatesheet = "{$candidatesheet}.css";
}
$etag = sha1($etag);
$candidatesheet = "{$candidatedir}/" . theme_styles_get_filename($type, $themesubrev, $usesvg);
$chunkedcandidatesheet = "{$candidatedir}/" . theme_styles_get_filename($type, $themesubrev, $usesvg, $chunk);
$etag = theme_styles_get_etag($themename, $rev, $type, $themesubrev, $usesvg, $chunk);
if (file_exists($chunkedcandidatesheet)) {
if (!empty($_SERVER['HTTP_IF_NONE_MATCH']) || !empty($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
......@@ -144,31 +129,15 @@ $cache = true;
// If the client is requesting a revision that doesn't match both
// the global theme revision and the theme specific revision then
// tell the browser not to cache this style sheet because it's
// likely being regnerated.
// likely being regenerated.
if ($themerev <= 0 or $themerev != $rev or $themesubrev != $currentthemesubrev) {
$rev = $themerev;
$cache = false;
$candidatedir = "$CFG->localcachedir/theme/$rev/$themename/css";
$etag = "$rev/$themename/$type/$themesubrev";
$candidatesheet = "{$candidatedir}/{$type}";
if ($themesubrev > 0) {
$candidatesheet .= "_{$themesubrev}";
}
if (!$usesvg) {
// Add to the sheet name, one day we'll be able to just drop this.
$candidatesheet .= "-nosvg";
$etag .= '/nosvg';
}
if ($chunk !== null) {
$etag .= "/chunk{$chunk}";
$chunkedcandidatesheet = "{$candidatesheet}.{$chunk}.css";
$candidatesheet = "{$candidatesheet}.css";
} else {
$candidatesheet = $chunkedcandidatesheet = "{$candidatesheet}.css";
}
$etag = sha1($etag);
$candidatesheet = "{$candidatedir}/" . theme_styles_get_filename($type, $themesubrev, $usesvg);
$chunkedcandidatesheet = "{$candidatedir}/" . theme_styles_get_filename($type, $themesubrev, $usesvg, $chunk);
$etag = theme_styles_get_etag($themename, $rev, $type, $themesubrev, $usesvg, $chunk);
}
make_localcache_directory('theme', false);
......@@ -177,65 +146,191 @@ if ($type === 'editor') {
$csscontent = $theme->get_css_content_editor();
css_store_css($theme, $candidatesheet, $csscontent, false);
if ($cache) {
css_send_cached_css($candidatesheet, $etag);
} else {
css_send_uncached_css(file_get_contents($candidatesheet));
}
}
if (($fallbacksheet = theme_styles_fallback_content($theme)) && !$theme->has_css_cached_content()) {
// The theme is not yet available and a fallback is available.
// Return the fallback immediately, specifying the Content-Length, then generate in the background.
$css = file_get_contents($fallbacksheet);
css_send_temporary_css($css);
// The fallback content has now been sent.
// There will be an attempt to generate the content, but it should not be served.
// The Content-Length above means that the client will disregard it anyway.
$sendaftergeneration = false;
// There may be another client currently holding a lock and generating the stylesheet.
// Use a very low lock timeout as the connection will be ended immediately afterwards.
$locktimeout = 1;
} else {
// Fetch a lock whilst the CSS is fetched as this can be slow and CPU intensive.
// Each client should wait for one to finish the compilation before starting the compiler.
$lockfactory = \core\lock\lock_config::get_lock_factory('core_theme_get_css_content');
$lock = $lockfactory->get_lock($themename, rand(90, 120));
if (file_exists($chunkedcandidatesheet)) {
// The file was built while we waited for the lock, we release the lock and serve the file.
if ($lock) {
$lock->release();
}
// There is no fallback content to be issued here, therefore the generated content must be output.
$sendaftergeneration = true;
// Use a realistic lock timeout as the intention is to avoid lock contention.
$locktimeout = rand(90, 120);
}
// Attempt to fetch the lock.
$lockfactory = \core\lock\lock_config::get_lock_factory('core_theme_get_css_content');
$lock = $lockfactory->get_lock($themename, $locktimeout);
if ($cache) {
if ($sendaftergeneration || $lock) {
// Either the lock was successful, or the lock was unsuccessful but the content *must* be sent.
if (!file_exists($chunkedcandidatesheet)) {
// The content does not exist locally.
// Generate and save it.
$candidatesheet = theme_styles_generate_and_store($theme, $rev, $themesubrev, $candidatedir);
}
if ($lock) {
$lock->release();
}
if ($sendaftergeneration) {
if (!$cache) {
// Do not pollute browser caches if invalid revision requested,
// let's ignore legacy IE breakage here too.
css_send_uncached_css(file_get_contents($candidatesheet));
} else if ($chunk !== null and file_exists($chunkedcandidatesheet)) {
// Greetings stupid legacy IEs!
css_send_cached_css($chunkedcandidatesheet, $etag);
} else {
css_send_uncached_css(file_get_contents($chunkedcandidatesheet));
// Real browsers - this is the expected result!
css_send_cached_css($candidatesheet, $etag);
}
}
}
/**
* Generate the theme CSS and store it.
*
* @param theme_config $theme The theme to be generated
* @param int $rev The theme revision
* @param int $themesubrev The theme sub-revision
* @param string $candidatedir The directory that it should be stored in
* @return string The path that the primary (non-chunked) CSS was written to
*/
function theme_styles_generate_and_store($theme, $rev, $themesubrev, $candidatedir) {
global $CFG;
// The lock is still held, and the sheet still does not exist.
// Compile the CSS content.
// Generate the content first.
if (!$csscontent = $theme->get_css_cached_content()) {
$csscontent = $theme->get_css_content();
$theme->set_css_content_cache($csscontent);
}
if ($theme->get_rtl_mode()) {
$type = "all-rtl";
} else {
$type = "all";
}
// Determine the candidatesheet path.
// Note: Do not pass any value for chunking as this is calcualted during css storage.
$candidatesheet = "{$candidatedir}/" . theme_styles_get_filename($type, $themesubrev, $theme->use_svg_icons());
// Determine the chunking URL.
// Note, this will be removed when support for IE9 is removed.
$relroot = preg_replace('|^http.?://[^/]+|', '', $CFG->wwwroot);
if (!empty($slashargument)) {
if ($usesvg) {
$chunkurl = "{$relroot}/theme/styles.php/{$themename}/{$rev}/$type";
if (!empty(min_get_slash_argument())) {
if ($theme->use_svg_icons()) {
$chunkurl = "{$relroot}/theme/styles.php/{$theme->name}/{$rev}/$type";
} else {
$chunkurl = "{$relroot}/theme/styles.php/_s/{$themename}/{$rev}/$type";
$chunkurl = "{$relroot}/theme/styles.php/_s/{$theme->name}/{$rev}/$type";
}
} else {
if ($usesvg) {
$chunkurl = "{$relroot}/theme/styles.php?theme={$themename}&rev={$rev}&type=$type";
if ($theme->use_svg_icons()) {
$chunkurl = "{$relroot}/theme/styles.php?theme={$theme->name}&rev={$rev}&type=$type";
} else {
$chunkurl = "{$relroot}/theme/styles.php?theme={$themename}&rev={$rev}&type=$type&svg=0";
$chunkurl = "{$relroot}/theme/styles.php?theme={$theme->name}&rev={$rev}&type=$type&svg=0";
}
}
// Store the CSS.
css_store_css($theme, $candidatesheet, $csscontent, true, $chunkurl);
if ($lock) {
// Now that the CSS has been generated and/or stored, release the lock.
// This will allow waiting clients to use the newly generated and stored CSS.
$lock->release();
// Store the fallback CSS in the temp directory.
// This file is used as a fallback when waiting for a theme to compile and is not versioned in any way.
$fallbacksheet = make_temp_directory("theme/{$theme->name}")
. "/"
. theme_styles_get_filename($type, 0, $theme->use_svg_icons());
css_store_css($theme, $fallbacksheet, $csscontent, true, $chunkurl);
return $candidatesheet;
}
/**
* Fetch the preferred fallback content location if available.
*
* @param theme_config $theme The theme to be generated
* @return string The path to the fallback sheet on disk
*/
function theme_styles_fallback_content($theme) {
global $CFG;
if (!$theme->usefallback) {
// This theme does not support fallbacks.
return false;
}
$type = $theme->get_rtl_mode() ? 'all-rtl' : 'all';
$filename = theme_styles_get_filename($type);
$fallbacksheet = "{$CFG->tempdir}/theme/{$theme->name}/{$filename}";
if (file_exists($fallbacksheet)) {
return $fallbacksheet;
}
return false;
}
if (!$cache) {
// Do not pollute browser caches if invalid revision requested,
// let's ignore legacy IE breakage here too.
css_send_uncached_css($csscontent);
/**
* Get the filename for the specified configuration.
*
* @param string $type The requested sheet type
* @param int $themesubrev The theme sub-revision
* @param bool $usesvg Whether SVGs are allowed
* @param int $chunk The chunk number if specified
* @return string The filename for this sheet
*/
function theme_styles_get_filename($type, $themesubrev = 0, $usesvg = true, $chunk = null) {
$filename = $type;
$filename .= ($themesubrev > 0) ? "_{$themesubrev}" : '';
$filename .= $usesvg ? '' : '-nosvg';
$filename .= $chunk ? ".{$chunk}" : '';
} else if ($chunk !== null and file_exists($chunkedcandidatesheet)) {
// Greetings stupid legacy IEs!
css_send_cached_css($chunkedcandidatesheet, $etag);
return "{$filename}.css";
}
} else {
// Real browsers - this is the expected result!
css_send_cached_css_content($csscontent, $etag);
/**
* Determine the correct etag for the specified configuration.
*
* @param string $themename The name of the theme
* @param int $rev The revision number
* @param string $type The requested sheet type
* @param int $themesubrev The theme sub-revision
* @param bool $usesvg Whether SVGs are allowed
* @param int $chunk The chunk number if specified
* @return string The etag to use for this request
*/
function theme_styles_get_etag($themename, $rev, $type, $themesubrev, $usesvg, $chunk) {
$etag = [$rev, $themename, $type, $themesubrev];
if (!$usesvg) {
$etag[] = 'nosvg';
}
if ($chunk) {
$etag[] = "chunk{$chunk}";
}
return sha1(implode('/', $etag));
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment