MDL-55751 themes: remove the css chunker

The css chunker is a workaround a browser bug for old IE versions
This commit is contained in:
Bas Brands 2019-08-05 13:37:14 +02:00
parent a672f021ea
commit 8ea1e39f4c
3 changed files with 15 additions and 283 deletions

View File

@ -39,13 +39,9 @@ if (!defined('THEME_DESIGNER_CACHE_LIFETIME')) {
*
* @param theme_config $theme The theme that the CSS belongs to.
* @param string $csspath The path to store the CSS at.
* @param string $csscontent the complete CSS in one string
* @param bool $chunk If set to true these files will be chunked to ensure
* that no one file contains more than 4095 selectors.
* @param string $chunkurl If the CSS is be chunked then we need to know the URL
* to use for the chunked files.
* @param string $csscontent the complete CSS in one string.
*/
function css_store_css(theme_config $theme, $csspath, $csscontent, $chunk = false, $chunkurl = null) {
function css_store_css(theme_config $theme, $csspath, $csscontent) {
global $CFG;
clearstatcache();
@ -60,24 +56,6 @@ function css_store_css(theme_config $theme, $csspath, $csscontent, $chunk = fals
// First up write out the single file for all those using decent browsers.
css_write_file($csspath, $csscontent);
if ($chunk) {
// If we need to chunk the CSS for browsers that are sub-par.
$css = css_chunk_by_selector_count($csscontent, $chunkurl);
$files = count($css);
$count = 1;
foreach ($css as $content) {
if ($count === $files) {
// If there is more than one file and this IS the last file.
$filename = preg_replace('#\.css$#', '.0.css', $csspath);
} else {
// If there is more than one file and this is not the last file.
$filename = preg_replace('#\.css$#', '.'.$count.'.css', $csspath);
}
$count++;
css_write_file($filename, $content);
}
}
ignore_user_abort(false);
if (connection_aborted()) {
die;
@ -101,179 +79,6 @@ function css_write_file($filename, $content) {
}
}
/**
* Takes CSS and chunks it if the number of selectors within it exceeds $maxselectors.
*
* The chunking will not split a group of selectors, or a media query. That means that
* if n > $maxselectors and there are n selectors grouped together,
* they will not be chunked and you could end up with more selectors than desired.
* The same applies for a media query that has more than n selectors.
*
* Also, as we do not split group of selectors or media queries, the chunking might
* not be as optimal as it could be, having files with less selectors than it could
* potentially contain.
*
* String functions used here are not compliant with unicode characters. But that is
* not an issue as the syntax of CSS is using ASCII codes. Even if we have unicode
* characters in comments, or in the property 'content: ""', it will behave correcly.
*
* Please note that this strips out the comments if chunking happens.
*
* @param string $css The CSS to chunk.
* @param string $importurl The URL to use for import statements.
* @param int $maxselectors The number of selectors to limit a chunk to.
* @param int $buffer Not used any more.
* @return array An array of CSS chunks.
*/
function css_chunk_by_selector_count($css, $importurl, $maxselectors = 4095, $buffer = 50) {
// Check if we need to chunk this CSS file.
$count = substr_count($css, ',') + substr_count($css, '{');
if ($count < $maxselectors) {
// The number of selectors is less then the max - we're fine.
return array($css);
}
$chunks = array(); // The final chunks.
$offsets = array(); // The indexes to chunk at.
$offset = 0; // The current offset.
$selectorcount = 0; // The number of selectors since the last split.
$lastvalidoffset = 0; // The last valid index to split at.
$lastvalidoffsetselectorcount = 0; // The number of selectors used at the time were could split.
$inrule = 0; // The number of rules we are in, should not be greater than 1.
$inmedia = false; // Whether or not we are in a media query.
$mediacoming = false; // Whether or not we are expeting a media query.
$currentoffseterror = null; // Not null when we have recorded an error for the current split.
$offseterrors = array(); // The offsets where we found errors.
// Remove the comments. Because it's easier, safer and probably a lot of other good reasons.
$css = preg_replace('#/\*(.*?)\*/#s', '', $css);
$strlen = strlen($css);
// Walk through the CSS content character by character.
for ($i = 1; $i <= $strlen; $i++) {
$char = $css[$i - 1];
$offset = $i;
// Is that a media query that I see coming towards us?
if ($char === '@') {
if (!$inmedia && substr($css, $offset, 5) === 'media') {
$mediacoming = true;
}
}
// So we are entering a rule or a media query...
if ($char === '{') {
if ($mediacoming) {
$inmedia = true;
$mediacoming = false;
} else {
$inrule++;
$selectorcount++;
}
}
// Let's count the number of selectors, but only if we are not in a rule, or in
// the definition of a media query, as they can contain commas too.
if (!$mediacoming && !$inrule && $char === ',') {
$selectorcount++;
}
// We reached the end of something.
if ($char === '}') {
// Oh, we are in a media query.
if ($inmedia) {
if (!$inrule) {
// This is the end of the media query.
$inmedia = false;
} else {
// We were in a rule, in the media query.
$inrule--;
}
} else {
$inrule--;
// Handle stupid broken CSS where there are too many } brackets,
// as this can cause it to break (with chunking) where it would
// coincidentally have worked otherwise.
if ($inrule < 0) {
$inrule = 0;
}
}
// We are not in a media query, and there is no pending rule, it is safe to split here.
if (!$inmedia && !$inrule) {
$lastvalidoffset = $offset;
$lastvalidoffsetselectorcount = $selectorcount;
}
}
// Alright, this is splitting time...
if ($selectorcount > $maxselectors) {
if (!$lastvalidoffset) {
// We must have reached more selectors into one set than we were allowed. That means that either
// the chunk size value is too small, or that we have a gigantic group of selectors, or that a media
// query contains more selectors than the chunk size. We have to ignore this because we do not
// support split inside a group of selectors or media query.
if ($currentoffseterror === null) {
$currentoffseterror = $offset;
$offseterrors[] = $currentoffseterror;
}
} else {
// We identify the offset to split at and reset the number of selectors found from there.
$offsets[] = $lastvalidoffset;
$selectorcount = $selectorcount - $lastvalidoffsetselectorcount;
$lastvalidoffset = 0;
$currentoffseterror = null;
}
}
}
// Report offset errors.
if (!empty($offseterrors)) {
debugging('Could not find a safe place to split at offset(s): ' . implode(', ', $offseterrors) . '. Those were ignored.',
DEBUG_DEVELOPER);
}
// Now that we have got the offets, we can chunk the CSS.
$offsetcount = count($offsets);
foreach ($offsets as $key => $index) {
$start = 0;
if ($key > 0) {
$start = $offsets[$key - 1];
}
// From somewhere up to the offset.
$chunks[] = substr($css, $start, $index - $start);
}
// Add the last chunk (if there is one), from the last offset to the end of the string.
if (end($offsets) != $strlen) {
$chunks[] = substr($css, end($offsets));
}
// The array $chunks now contains CSS split into perfect sized chunks.
// Import statements can only appear at the very top of a CSS file.
// Imported sheets are applied in the the order they are imported and
// are followed by the contents of the CSS.
// This is terrible for performance.
// It means we must put the import statements at the top of the last chunk
// to ensure that things are always applied in the correct order.
// This way the chunked files are included in the order they were chunked
// followed by the contents of the final chunk in the actual sheet.
$importcss = '';
$slashargs = strpos($importurl, '.php?') === false;
$parts = count($chunks);
for ($i = 1; $i < $parts; $i++) {
if ($slashargs) {
$importcss .= "@import url({$importurl}/chunk{$i});\n";
} else {
$importcss .= "@import url({$importurl}&chunk={$i});\n";
}
}
$importcss .= end($chunks);
$chunks[key($chunks)] = $importcss;
return $chunks;
}
/**
* Sends a cached CSS file
*

View File

@ -44,12 +44,6 @@ if ($slashargument = min_get_slash_argument()) {
$usesvg = true;
}
$chunk = null;
if (preg_match('#/(chunk(\d+)(/|$))#', $slashargument, $matches)) {
$chunk = (int)$matches[2];
$slashargument = str_replace($matches[1], '', $slashargument);
}
list($themename, $rev, $type) = explode('/', $slashargument, 3);
$themename = min_clean_param($themename, 'SAFEDIR');
$rev = min_clean_param($rev, 'RAW');
@ -59,7 +53,6 @@ if ($slashargument = min_get_slash_argument()) {
$themename = min_optional_param('theme', 'standard', 'SAFEDIR');
$rev = min_optional_param('rev', 0, 'RAW');
$type = min_optional_param('type', 'all', 'SAFEDIR');
$chunk = min_optional_param('chunk', null, 'INT');
$usesvg = (bool)min_optional_param('svg', '1', 'INT');
}
@ -74,12 +67,7 @@ if (!is_null($themesubrev)) {
}
// Check that type fits into the expected values.
if ($type === 'editor') {
// The editor CSS is never chunked.
$chunk = null;
} else if ($type === 'all' || $type === 'all-rtl') {
// We're fine.
} else {
if (!in_array($type, ['all', 'all-rtl', 'editor'])) {
css_send_css_not_found();
}
@ -94,17 +82,7 @@ if (file_exists("$CFG->dirroot/theme/$themename/config.php")) {
$candidatedir = "$CFG->localcachedir/theme/$rev/$themename/css";
$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'])) {
// We do not actually need to verify the etag value because our files
// never change in cache because we increment the rev counter.
css_send_unmodified(filemtime($chunkedcandidatesheet), $etag);
}
css_send_cached_css($chunkedcandidatesheet, $etag);
}
$etag = theme_styles_get_etag($themename, $rev, $type, $themesubrev, $usesvg);
// Ok, now we need to start normal moodle script, we need to load all libs and $DB.
define('ABORT_AFTER_CONFIG_CANCEL', true);
@ -133,8 +111,7 @@ if ($themerev <= 0 or $themerev != $rev or $themesubrev != $currentthemesubrev)
$candidatedir = "$CFG->localcachedir/theme/$rev/$themename/css";
$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);
$etag = theme_styles_get_etag($themename, $rev, $type, $themesubrev, $usesvg);
}
make_localcache_directory('theme', false);
@ -143,7 +120,7 @@ if ($type === 'editor') {
$csscontent = $theme->get_css_content_editor();
if ($cache) {
css_store_css($theme, $candidatesheet, $csscontent, false);
css_store_css($theme, $candidatesheet, $csscontent);
css_send_cached_css($candidatesheet, $etag);
} else {
css_send_uncached_css($csscontent);
@ -179,11 +156,10 @@ $lock = $lockfactory->get_lock($themename, $locktimeout);
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);
}
// The content does not exist locally.
// Generate and save it.
$candidatesheet = theme_styles_generate_and_store($theme, $rev, $themesubrev, $candidatedir);
if ($lock) {
$lock->release();
@ -195,10 +171,6 @@ if ($sendaftergeneration || $lock) {
// 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 {
// Real browsers - this is the expected result!
css_send_cached_css($candidatesheet, $etag);
@ -213,7 +185,7 @@ if ($sendaftergeneration || $lock) {
* @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
* @return string The path that the primary CSS was written to
*/
function theme_styles_generate_and_store($theme, $rev, $themesubrev, $candidatedir) {
global $CFG;
@ -232,35 +204,17 @@ function theme_styles_generate_and_store($theme, $rev, $themesubrev, $candidated
}
// 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(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/{$theme->name}/{$rev}/$type";
}
} else {
if ($theme->use_svg_icons()) {
$chunkurl = "{$relroot}/theme/styles.php?theme={$theme->name}&rev={$rev}&type=$type";
} else {
$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);
css_store_css($theme, $candidatesheet, $csscontent);
// 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);
css_store_css($theme, $fallbacksheet, $csscontent);
// Delete older revisions from localcache.
$themecachedirs = glob("{$CFG->localcachedir}/theme/*", GLOB_ONLYDIR);
@ -318,14 +272,12 @@ function theme_styles_fallback_content($theme) {
* @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) {
function theme_styles_get_filename($type, $themesubrev = 0, $usesvg = true) {
$filename = $type;
$filename .= ($themesubrev > 0) ? "_{$themesubrev}" : '';
$filename .= $usesvg ? '' : '-nosvg';
$filename .= $chunk ? ".{$chunk}" : '';
return "{$filename}.css";
}
@ -338,19 +290,14 @@ function theme_styles_get_filename($type, $themesubrev = 0, $usesvg = true, $chu
* @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) {
function theme_styles_get_etag($themename, $rev, $type, $themesubrev, $usesvg) {
$etag = [$rev, $themename, $type, $themesubrev];
if (!$usesvg) {
$etag[] = 'nosvg';
}
if ($chunk) {
$etag[] = "chunk{$chunk}";
}
return sha1(implode('/', $etag));
}

View File

@ -36,7 +36,6 @@ $type = optional_param('type', '', PARAM_SAFEDIR);
$subtype = optional_param('subtype', '', PARAM_SAFEDIR);
$sheet = optional_param('sheet', '', PARAM_SAFEDIR);
$usesvg = optional_param('svg', 1, PARAM_BOOL);
$chunk = optional_param('chunk', null, PARAM_INT);
$rtl = optional_param('rtl', false, PARAM_BOOL);
if (file_exists("$CFG->dirroot/theme/$themename/config.php")) {
@ -56,9 +55,6 @@ if ($type === 'editor') {
css_send_uncached_css($csscontent);
}
$chunkurl = new moodle_url('/theme/styles_debug.php', array('theme' => $themename,
'type' => $type, 'subtype' => $subtype, 'sheet' => $sheet, 'usesvg' => $usesvg, 'rtl' => $rtl));
// 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...
$key = "$type $subtype $sheet $usesvg $rtl";
@ -66,13 +62,6 @@ $cache = cache::make_from_params(cache_store::MODE_APPLICATION, 'core', 'themede
if ($content = $cache->get($key)) {
if ($content['created'] > time() - THEME_DESIGNER_CACHE_LIFETIME) {
$csscontent = $content['data'];
// We need to chunk the content.
if ($chunk !== null) {
$chunks = css_chunk_by_selector_count($csscontent, $chunkurl->out(false));
$csscontent = ($chunk === 0) ? end($chunks) : $chunks[$chunk - 1];
}
css_send_uncached_css($csscontent);
}
}
@ -80,13 +69,4 @@ if ($content = $cache->get($key)) {
$csscontent = $theme->get_css_content_debug($type, $subtype, $sheet);
$cache->set($key, array('data' => $csscontent, 'created' => time()));
// We need to chunk the content.
if ($chunk !== null) {
// The chunks are ordered so that the last chunk is the one containing the @import, and so
// the first one to be included. All the other chunks are set in the array before that one.
// See {@link css_chunk_by_selector_count()} for more details.
$chunks = css_chunk_by_selector_count($csscontent, $chunkurl->out(false));
$csscontent = ($chunk === 0) ? end($chunks) : $chunks[$chunk - 1];
}
css_send_uncached_css($csscontent);