/** * Stores CSS in a file at the given path. * * This function either succeeds or throws an exception. * * @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. */ function css_store_css(theme_config $theme, $csspath, $csscontent, $chunk = false, $chunkurl = null) { global $CFG; clearstatcache(); if (!file_exists(dirname($csspath))) { @mkdir(dirname($csspath), $CFG->directorypermissions, true); } // Prevent serving of incomplete file from concurrent request, // the rename() should be more atomic than fwrite(). ignore_user_abort(true); // 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; } }
/** * Test CSS chunking */ public function test_css_chunking() { // Test with an even number of styles. $css = 'a{}b{}c{}d{}e{}f{}'; $chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2); $this->assertInternalType('array', $chunks); $this->assertCount(3, $chunks); $this->assertArrayHasKey(0, $chunks); $this->assertArrayHasKey(1, $chunks); $this->assertArrayHasKey(2, $chunks); $this->assertSame('a{}b{}', $chunks[0]); $this->assertSame('c{}d{}', $chunks[1]); $this->assertSame("@import url(styles.php?type=test&chunk=1);\n@import url(styles.php?type=test&chunk=2);\ne{}f{}", $chunks[2]); // Test with an odd number of styles. $css = 'a{}b{}c{}d{}e{}'; $chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2); $this->assertInternalType('array', $chunks); $this->assertCount(3, $chunks); $this->assertArrayHasKey(0, $chunks); $this->assertArrayHasKey(1, $chunks); $this->assertArrayHasKey(2, $chunks); $this->assertSame('a{}b{}', $chunks[0]); $this->assertSame('c{}d{}', $chunks[1]); $this->assertSame("@import url(styles.php?type=test&chunk=1);\n@import url(styles.php?type=test&chunk=2);\ne{}", $chunks[2]); // Test well placed commas. $css = 'a,b{}c,d{}e,f{}'; $chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2); $this->assertInternalType('array', $chunks); $this->assertCount(3, $chunks); $this->assertArrayHasKey(0, $chunks); $this->assertArrayHasKey(1, $chunks); $this->assertArrayHasKey(2, $chunks); $this->assertSame('a,b{}', $chunks[0]); $this->assertSame('c,d{}', $chunks[1]); $this->assertSame("@import url(styles.php?type=test&chunk=1);\n@import url(styles.php?type=test&chunk=2);\ne,f{}", $chunks[2]); // Test unfortunately placed commas. $css = 'a{}b,c{color:red;}d{}e{}f{}'; $chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2); $this->assertInternalType('array', $chunks); $this->assertCount(4, $chunks); $this->assertArrayHasKey(0, $chunks); $this->assertArrayHasKey(1, $chunks); $this->assertArrayHasKey(2, $chunks); $this->assertArrayHasKey(3, $chunks); $this->assertSame('a{}', $chunks[0]); $this->assertSame('b,c{color:red;}', $chunks[1]); $this->assertSame('d{}e{}', $chunks[2]); $this->assertSame("@import url(styles.php?type=test&chunk=1);\n@import url(styles.php?type=test&chunk=2);\n@import url(styles.php?type=test&chunk=3);\nf{}", $chunks[3]); // Test unfortunate CSS. $css = 'a,b,c,d,e,f{color:red;}'; $chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2, 0); $this->assertInternalType('array', $chunks); $this->assertCount(1, $chunks); $this->assertArrayHasKey(0, $chunks); $this->assertSame('a,b,c,d,e,f{color:red;}', $chunks[0]); $this->assertDebuggingCalled('Could not find a safe place to split at offset(s): 6. Those were ignored.'); // Test to make sure invalid CSS isn't totally ruined. $css = 'a{},,,e{},'; $chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2); // Believe it or not we want to care what comes out here as this will be parsed correctly // by a browser. $this->assertInternalType('array', $chunks); $this->assertCount(3, $chunks); $this->assertArrayHasKey(0, $chunks); $this->assertArrayHasKey(1, $chunks); $this->assertArrayHasKey(2, $chunks); $this->assertSame('a{}', $chunks[0]); $this->assertSame(',,,e{}', $chunks[1]); $this->assertSame("@import url(styles.php?type=test&chunk=1);\n@import url(styles.php?type=test&chunk=2);\n,", $chunks[2]); $this->assertDebuggingCalled('Could not find a safe place to split at offset(s): 6. Those were ignored.'); // Test utter crap CSS to make sure we don't loop to our deaths. $css = 'a,b,c,d,e,f'; $chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2); $this->assertInternalType('array', $chunks); $this->assertCount(1, $chunks); $this->assertArrayHasKey(0, $chunks); $this->assertSame($css, $chunks[0]); $this->assertDebuggingCalled('Could not find a safe place to split at offset(s): 6. Those were ignored.'); // Test another death situation to make sure we're invincible. $css = 'a,,,,,e'; $chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2); $this->assertInternalType('array', $chunks); $this->assertDebuggingCalled('Could not find a safe place to split at offset(s): 4. Those were ignored.'); // I don't care what the outcome is, I just want to make sure it doesn't die. // Test media queries. $css = '@media (min-width: 980px) { .a,.b{} }'; $chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2); $this->assertCount(1, $chunks); $this->assertSame('@media (min-width: 980px) { .a,.b{} }', $chunks[0]); // Test media queries, with commas. $css = '.a{} @media (min-width: 700px), handheld and (orientation: landscape) { .b{} }'; $chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2); $this->assertCount(1, $chunks); $this->assertSame($css, $chunks[0]); // Test special rules. $css = 'a,b{ background-image: linear-gradient(to bottom, #ffffff, #cccccc);}d,e{}'; $chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2); $this->assertCount(2, $chunks); $this->assertSame('a,b{ background-image: linear-gradient(to bottom, #ffffff, #cccccc);}', $chunks[0]); $this->assertSame("@import url(styles.php?type=test&chunk=1);\nd,e{}", $chunks[1]); // Test media queries with too many selectors. $css = '@media (min-width: 980px) { a,b,c,d{} }'; $chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2); $this->assertCount(1, $chunks); $this->assertSame('@media (min-width: 980px) { a,b,c,d{} }', $chunks[0]); $this->assertDebuggingCalled('Could not find a safe place to split at offset(s): 34. Those were ignored.'); // Complex test. $css = '@media (a) {b{}} c{} d,e{} f,g,h{} i,j{x:a,b,c} k,l{} @media(x){l,m{ y: a,b,c}} n{}'; $chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 3); $this->assertCount(6, $chunks); $this->assertSame('@media (a) {b{}} c{}', $chunks[0]); $this->assertSame(' d,e{}', $chunks[1]); $this->assertSame(' f,g,h{}', $chunks[2]); $this->assertSame(' i,j{x:a,b,c}', $chunks[3]); $this->assertSame(' k,l{}', $chunks[4]); $this->assertSame("@import url(styles.php?type=test&chunk=1);\n@import url(styles.php?type=test&chunk=2);\n@import url(styles.php?type=test&chunk=3);\n@import url(styles.php?type=test&chunk=4);\n@import url(styles.php?type=test&chunk=5);\n @media(x){l,m{ y: a,b,c}} n{}", $chunks[5]); // Multiple offset errors. $css = 'a,b,c{} d,e,f{}'; $chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2); $this->assertCount(2, $chunks); $this->assertSame('a,b,c{}', $chunks[0]); $this->assertSame("@import url(styles.php?type=test&chunk=1);\n d,e,f{}", $chunks[1]); $this->assertDebuggingCalled('Could not find a safe place to split at offset(s): 6, 14. Those were ignored.'); // Test the split according to IE. $css = str_repeat('a{}', 4100); $chunks = css_chunk_by_selector_count($css, 'styles.php?type=test'); $this->assertCount(2, $chunks); $this->assertSame(str_repeat('a{}', 4095), $chunks[0]); $this->assertSame("@import url(styles.php?type=test&chunk=1);\n" . str_repeat('a{}', 5), $chunks[1]); // Test strip out comments. $css = ".a {/** a\nb\nc */} /** a\nb\nc */ .b{} /** .c,.d{} */ e{}"; $chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2); $this->assertCount(2, $chunks); $this->assertSame('.a {} .b{}', $chunks[0]); $this->assertSame("@import url(styles.php?type=test&chunk=1);\n e{}", $chunks[1]); // Test something with unicode characters. $css = 'a,b{} nav a:hover:after { content: "↓"; } b{ color:test;}'; $chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2, true); $this->assertCount(2, $chunks); $this->assertSame('a,b{}', $chunks[0]); $this->assertSame("@import url(styles.php?type=test&chunk=1);\n nav a:hover:after { content: \"↓\"; } b{ color:test;}", $chunks[1]); // Test that if there is broken CSS with too many close brace symbols, // media rules after that point are still kept together. $mediarule = '@media (width=480) {a{}b{}}'; $css = 'c{}}' . $mediarule . 'd{}'; $chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2); $this->assertCount(3, $chunks); $this->assertEquals($mediarule, $chunks[1]); // Test that this still works even with too many close brace symbols // inside a media query (note: that broken media query may be split // after the break, but any following ones should not be). $brokenmediarule = '@media (width=480) {c{}}d{}}'; $css = $brokenmediarule . 'e{}' . $mediarule . 'f{}'; $chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2); $this->assertCount(4, $chunks); $this->assertEquals($mediarule, $chunks[2]); }
/** * Stores CSS in a file at the given path. * * This function either succeeds or throws an exception. * * @param theme_config $theme The theme that the CSS belongs to. * @param string $csspath The path to store the CSS at. * @param array $cssfiles The CSS files to store. * @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. */ function css_store_css(theme_config $theme, $csspath, array $cssfiles, $chunk = false, $chunkurl = null) { global $CFG; // Check if both the CSS optimiser is enabled and the theme supports it. if (!empty($CFG->enablecssoptimiser) && $theme->supportscssoptimisation) { // This is an experimental feature introduced in Moodle 2.3 // The CSS optimiser organises the CSS in order to reduce the overall number // of rules and styles being sent to the client. It does this by collating // the CSS before it is cached removing excess styles and rules and stripping // out any extraneous content such as comments and empty rules. $optimiser = new css_optimiser(); $css = ''; foreach ($cssfiles as $file) { $css .= file_get_contents($file) . "\n"; } $css = $theme->post_process($css); $css = $optimiser->process($css); // If cssoptimisestats is set then stats from the optimisation are collected // and output at the beginning of the CSS if (!empty($CFG->cssoptimiserstats)) { $css = $optimiser->output_stats_css() . $css; } } else { // This is the default behaviour. // The cssoptimise setting was introduced in Moodle 2.3 and will hopefully // in the future be changed from an experimental setting to the default. // The css_minify_css will method will use the Minify library remove // comments, additional whitespace and other minor measures to reduce the // the overall CSS being sent. // However it has the distinct disadvantage of having to minify the CSS // before running the post process functions. Potentially things may break // here if theme designers try to push things with CSS post processing. $css = $theme->post_process(css_minify_css($cssfiles)); } if ($chunk) { // Chunk the CSS if requried. $css = css_chunk_by_selector_count($css, $chunkurl); } else { $css = array($css); } clearstatcache(); if (!file_exists(dirname($csspath))) { @mkdir(dirname($csspath), $CFG->directorypermissions, true); } // Prevent serving of incomplete file from concurrent request, // the rename() should be more atomic than fwrite(). ignore_user_abort(true); $files = count($css); $count = 0; foreach ($css as $content) { if ($files > 1 && $count + 1 !== $files) { // If there is more than one file and this is not the last file. $filename = preg_replace('#\\.css$#', '.' . $count . '.css', $csspath); $count++; } else { $filename = $csspath; } if ($fp = fopen($filename . '.tmp', 'xb')) { fwrite($fp, $content); fclose($fp); rename($filename . '.tmp', $filename); @chmod($filename, $CFG->filepermissions); @unlink($filename . '.tmp'); // just in case anything fails } } ignore_user_abort(false); if (connection_aborted()) { die; } }
/** * Test CSS chunking */ public function test_css_chunking() { // Test with an even number of styles. $css = 'a{}b{}c{}d{}e{}f{}'; $chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2, 0); $this->assertInternalType('array', $chunks); $this->assertCount(3, $chunks); $this->assertArrayHasKey(0, $chunks); $this->assertArrayHasKey(1, $chunks); $this->assertArrayHasKey(2, $chunks); $this->assertSame('a{}b{}', $chunks[0]); $this->assertSame('c{}d{}', $chunks[1]); $this->assertSame("@import url(styles.php?type=test&chunk=1);\n@import url(styles.php?type=test&chunk=2);\ne{}f{}", $chunks[2]); // Test with an odd number of styles. $css = 'a{}b{}c{}d{}e{}'; $chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2, 0); $this->assertInternalType('array', $chunks); $this->assertCount(3, $chunks); $this->assertArrayHasKey(0, $chunks); $this->assertArrayHasKey(1, $chunks); $this->assertArrayHasKey(2, $chunks); $this->assertSame('a{}b{}', $chunks[0]); $this->assertSame('c{}d{}', $chunks[1]); $this->assertSame("@import url(styles.php?type=test&chunk=1);\n@import url(styles.php?type=test&chunk=2);\ne{}", $chunks[2]); // Test buffering. Set a buffer that will reduce the effective sheet size back to two. $css = 'a{}b{}c{}d{}e{}f{}'; $chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 6, 4); $this->assertInternalType('array', $chunks); $this->assertCount(3, $chunks); $this->assertArrayHasKey(0, $chunks); $this->assertArrayHasKey(1, $chunks); $this->assertArrayHasKey(2, $chunks); $this->assertSame('a{}b{}', $chunks[0]); $this->assertSame('c{}d{}', $chunks[1]); $this->assertSame("@import url(styles.php?type=test&chunk=1);\n@import url(styles.php?type=test&chunk=2);\ne{}f{}", $chunks[2]); // Test well placed commas. $css = 'a,b{}c,d{}e,f{}'; $chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2, 0); $this->assertInternalType('array', $chunks); $this->assertCount(3, $chunks); $this->assertArrayHasKey(0, $chunks); $this->assertArrayHasKey(1, $chunks); $this->assertArrayHasKey(2, $chunks); $this->assertSame('a,b{}', $chunks[0]); $this->assertSame('c,d{}', $chunks[1]); $this->assertSame("@import url(styles.php?type=test&chunk=1);\n@import url(styles.php?type=test&chunk=2);\ne,f{}", $chunks[2]); // Test unfortunately placed commas. $css = 'a{}b,c{color:red;}d{}e{}f{}'; $chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2, 0); $this->assertInternalType('array', $chunks); $this->assertCount(3, $chunks); $this->assertArrayHasKey(0, $chunks); $this->assertArrayHasKey(1, $chunks); $this->assertArrayHasKey(2, $chunks); $this->assertSame('a{}b{color:red;}', $chunks[0]); $this->assertSame('c{color:red;}d{}', $chunks[1]); $this->assertSame("@import url(styles.php?type=test&chunk=1);\n@import url(styles.php?type=test&chunk=2);\ne{}f{}", $chunks[2]); // Test unfortunate CSS. $css = 'a,b,c,d,e,f{color:red;}'; $chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2, 0); $this->assertInternalType('array', $chunks); $this->assertCount(3, $chunks); $this->assertArrayHasKey(0, $chunks); $this->assertArrayHasKey(1, $chunks); $this->assertArrayHasKey(2, $chunks); $this->assertSame('a,b{color:red;}', $chunks[0]); $this->assertSame('c,d{color:red;}', $chunks[1]); $this->assertSame("@import url(styles.php?type=test&chunk=1);\n@import url(styles.php?type=test&chunk=2);\ne,f{color:red;}", $chunks[2]); // Test to make sure invalid CSS isn't totally ruined. $css = 'a{},,,e{},'; $chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2, 0); // Believe it or not we want to care what comes out here as this will be parsed correctly // by a browser. $this->assertInternalType('array', $chunks); $this->assertCount(2, $chunks); $this->assertArrayHasKey(0, $chunks); $this->assertArrayHasKey(1, $chunks); $this->assertSame('a{},{}', $chunks[0]); $this->assertSame("@import url(styles.php?type=test&chunk=1);\n,e{}/** Error chunking CSS **/", $chunks[1]); // Test utter crap CSS to make sure we don't loop to our deaths. $css = 'a,b,c,d,e,f'; $chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2, 0); $this->assertInternalType('array', $chunks); $this->assertCount(3, $chunks); $this->assertArrayHasKey(0, $chunks); $this->assertArrayHasKey(1, $chunks); $this->assertArrayHasKey(2, $chunks); $this->assertSame('a,b/** Error chunking CSS **/', $chunks[0]); $this->assertSame('c,d/** Error chunking CSS **/', $chunks[1]); $this->assertSame("@import url(styles.php?type=test&chunk=1);\n@import url(styles.php?type=test&chunk=2);\ne,f", $chunks[2]); // Test another death situation to make sure we're invincible. $css = 'a,,,,,e'; $chunks = css_chunk_by_selector_count($css, 'styles.php?type=test', 2, 0); $this->assertInternalType('array', $chunks); // I don't care what the outcome is, I just want to make sure it doesn't die. }
if ($type === 'editor') { $csscontent = $theme->get_css_content_editor(); css_send_uncached_css($csscontent); } $chunkurl = new moodle_url($CFG->httpswwwroot . '/theme/styles_debug.php', array('theme' => $themename, 'type' => $type, 'subtype' => $subtype, 'sheet' => $sheet, 'usesvg' => $usesvg)); // 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}"; $cache = cache::make_from_params(cache_store::MODE_APPLICATION, 'core', 'themedesigner', array('theme' => $themename)); 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); } } $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);