/** * Create a redirect that is also valid CSS * * @param Title $destination * @param string $text ignored * @return CssContent */ public function makeRedirectContent(Title $destination, $text = '') { // The parameters are passed as a string so the / is not url-encoded by wfArrayToCgi $url = $destination->getFullURL('action=raw&ctype=text/css', false, PROTO_RELATIVE); $class = $this->getContentClass(); return new $class('/* #REDIRECT */@import ' . CSSMin::buildUrlValue($url) . ';'); }
/** * Convert an image URI to a base64-encoded data URI. * * @par Example: * @code * .fancy-button { * background-image: embed('../images/button-bg.png'); * } * @endcode * @param array $frame * @param lessc $less * @return string */ public static function embed($frame, $less) { $base = pathinfo($less->parser->sourceName, PATHINFO_DIRNAME); $url = trim($less->compileValue($frame), '"\''); $file = realpath($base . '/' . $url); $data = CSSMin::encodeImageAsDataURI($file); $less->addParsedFile($file); return CSSMin::buildUrlValue($data); }
/** * @param $context ResourceLoaderContext * @return array */ public function getStyles(ResourceLoaderContext $context) { $logo = $this->getConfig()->get('Logo'); $logoHD = $this->getConfig()->get('LogoHD'); $styles = parent::getStyles($context); $styles['all'][] = '.mw-wiki-logo { background-image: ' . CSSMin::buildUrlValue($logo) . '; }'; if ($logoHD) { if (isset($logoHD['1.5x'])) { $styles['(-webkit-min-device-pixel-ratio: 1.5), ' . '(min--moz-device-pixel-ratio: 1.5), ' . '(min-resolution: 1.5dppx), ' . '(min-resolution: 144dpi)'][] = '.mw-wiki-logo { background-image: ' . CSSMin::buildUrlValue($logoHD['1.5x']) . ';' . 'background-size: 135px auto; }'; } if (isset($logoHD['2x'])) { $styles['(-webkit-min-device-pixel-ratio: 2), ' . '(min--moz-device-pixel-ratio: 2),' . '(min-resolution: 2dppx), ' . '(min-resolution: 192dpi)'][] = '.mw-wiki-logo { background-image: ' . CSSMin::buildUrlValue($logoHD['2x']) . ';' . 'background-size: 135px auto; }'; } } return $styles; }
/** * This tests basic functionality of CSSMin::buildUrlValue. * * @dataProvider provideBuildUrlValueCases * @covers CSSMin::buildUrlValue */ public function testBuildUrlValue($message, $input, $expectedOutput) { $this->assertEquals($expectedOutput, CSSMin::buildUrlValue($input), "CSSMin::buildUrlValue: {$message}"); }
/** * Remaps CSS URL paths and automatically embeds data URIs for CSS rules * or url() values preceded by an / * @embed * / comment. * * @param string $source CSS data to remap * @param string $local File path where the source was read from * @param string $remote URL path to the file * @param bool $embedData If false, never do any data URI embedding, * even if / * @embed * / is found. * @return string Remapped CSS data */ public static function remap($source, $local, $remote, $embedData = true) { // High-level overview: // * For each CSS rule in $source that includes at least one url() value: // * Check for an @embed comment at the start indicating that all URIs should be embedded // * For each url() value: // * Check for an @embed comment directly preceding the value // * If either @embed comment exists: // * Embedding the URL as data: URI, if it's possible / allowed // * Otherwise remap the URL to work in generated stylesheets // Guard against trailing slashes, because "some/remote/../foo.png" // resolves to "some/remote/foo.png" on (some?) clients (bug 27052). if (substr($remote, -1) == '/') { $remote = substr($remote, 0, -1); } // Disallow U+007F DELETE, which is illegal anyway, and which // we use for comment placeholders. $source = str_replace("", "?", $source); // Replace all comments by a placeholder so they will not interfere with the remapping. // Warning: This will also catch on anything looking like the start of a comment between // quotation marks (e.g. "foo /* bar"). $comments = array(); $pattern = '/(?!' . CSSMin::EMBED_REGEX . ')(' . CSSMin::COMMENT_REGEX . ')/s'; $source = preg_replace_callback($pattern, function ($match) use(&$comments) { $comments[] = $match[0]; return CSSMin::PLACEHOLDER . (count($comments) - 1) . 'x'; }, $source); // Note: This will not correctly handle cases where ';', '{' or '}' // appears in the rule itself, e.g. in a quoted string. You are advised // not to use such characters in file names. We also match start/end of // the string to be consistent in edge-cases ('@import url(…)'). $pattern = '/(?:^|[;{])\\K[^;{}]*' . CSSMin::URL_REGEX . '[^;}]*(?=[;}]|$)/'; $source = preg_replace_callback($pattern, function ($matchOuter) use($local, $remote, $embedData) { $rule = $matchOuter[0]; // Check for global @embed comment and remove it. Allow other comments to be present // before @embed (they have been replaced with placeholders at this point). $embedAll = false; $rule = preg_replace('/^((?:\\s+|' . CSSMin::PLACEHOLDER . '(\\d+)x)*)' . CSSMin::EMBED_REGEX . '\\s*/', '$1', $rule, 1, $embedAll); // Build two versions of current rule: with remapped URLs // and with embedded data: URIs (where possible). $pattern = '/(?P<embed>' . CSSMin::EMBED_REGEX . '\\s*|)' . CSSMin::URL_REGEX . '/'; $ruleWithRemapped = preg_replace_callback($pattern, function ($match) use($local, $remote) { $remapped = CSSMin::remapOne($match['file'], $match['query'], $local, $remote, false); return CSSMin::buildUrlValue($remapped); }, $rule); if ($embedData) { // Remember the occurring MIME types to avoid fallbacks when embedding some files. $mimeTypes = array(); $ruleWithEmbedded = preg_replace_callback($pattern, function ($match) use($embedAll, $local, $remote, &$mimeTypes) { $embed = $embedAll || $match['embed']; $embedded = CSSMin::remapOne($match['file'], $match['query'], $local, $remote, $embed); $url = $match['file'] . $match['query']; $file = $local . $match['file']; if (!CSSMin::isRemoteUrl($url) && !CSSMin::isLocalUrl($url) && file_exists($file)) { $mimeTypes[CSSMin::getMimeType($file)] = true; } return CSSMin::buildUrlValue($embedded); }, $rule); // Are all referenced images SVGs? $needsEmbedFallback = $mimeTypes !== array('image/svg+xml' => true); } if (!$embedData || $ruleWithEmbedded === $ruleWithRemapped) { // We're not embedding anything, or we tried to but the file is not embeddable return $ruleWithRemapped; } elseif ($embedData && $needsEmbedFallback) { // Build 2 CSS properties; one which uses a data URI in place of the @embed comment, and // the other with a remapped and versioned URL with an Internet Explorer 6 and 7 hack // making it ignored in all browsers that support data URIs return "{$ruleWithEmbedded};{$ruleWithRemapped}!ie"; } else { // Look ma, no fallbacks! This is for files which IE 6 and 7 don't support anyway: SVG. return $ruleWithEmbedded; } }, $source); // Re-insert comments $pattern = '/' . CSSMin::PLACEHOLDER . '(\\d+)x/'; $source = preg_replace_callback($pattern, function ($match) use(&$comments) { return $comments[$match[1]]; }, $source); return $source; }
/** * Remaps CSS URL paths and automatically embeds data URIs for CSS rules or url() values * preceded by an / * @embed * / comment. * * @param string $source CSS data to remap * @param string $local File path where the source was read from * @param string $remote URL path to the file * @param bool $embedData If false, never do any data URI embedding, even if / * @embed * / is found * @return string Remapped CSS data */ public static function remap($source, $local, $remote, $embedData = true) { // High-level overview: // * For each CSS rule in $source that includes at least one url() value: // * Check for an @embed comment at the start indicating that all URIs should be embedded // * For each url() value: // * Check for an @embed comment directly preceding the value // * If either @embed comment exists: // * Embedding the URL as data: URI, if it's possible / allowed // * Otherwise remap the URL to work in generated stylesheets // Guard against trailing slashes, because "some/remote/../foo.png" // resolves to "some/remote/foo.png" on (some?) clients (bug 27052). if (substr($remote, -1) == '/') { $remote = substr($remote, 0, -1); } // Note: This will not correctly handle cases where ';', '{' or '}' appears in the rule itself, // e.g. in a quoted string. You are advised not to use such characters in file names. // We also match start/end of the string to be consistent in edge-cases ('@import url(…)'). $pattern = '/(?:^|[;{])\\K[^;{}]*' . CSSMin::URL_REGEX . '[^;}]*(?=[;}]|$)/'; return preg_replace_callback($pattern, function ($matchOuter) use($local, $remote, $embedData) { $rule = $matchOuter[0]; // Check for global @embed comment and remove it $embedAll = false; $rule = preg_replace('/^(\\s*)' . CSSMin::EMBED_REGEX . '\\s*/', '$1', $rule, 1, $embedAll); // Build two versions of current rule: with remapped URLs and with embedded data: URIs (where possible) $pattern = '/(?P<embed>' . CSSMin::EMBED_REGEX . '\\s*|)' . CSSMin::URL_REGEX . '/'; $ruleWithRemapped = preg_replace_callback($pattern, function ($match) use($local, $remote) { $remapped = CSSMin::remapOne($match['file'], $match['query'], $local, $remote, false); return CSSMin::buildUrlValue($remapped); }, $rule); if ($embedData) { $ruleWithEmbedded = preg_replace_callback($pattern, function ($match) use($embedAll, $local, $remote) { $embed = $embedAll || $match['embed']; $embedded = CSSMin::remapOne($match['file'], $match['query'], $local, $remote, $embed); return CSSMin::buildUrlValue($embedded); }, $rule); } if ($embedData && $ruleWithEmbedded !== $ruleWithRemapped) { // Build 2 CSS properties; one which uses a base64 encoded data URI in place // of the @embed comment to try and retain line-number integrity, and the // other with a remapped an versioned URL and an Internet Explorer hack // making it ignored in all browsers that support data URIs return "{$ruleWithEmbedded};{$ruleWithRemapped}!ie"; } else { // No reason to repeat twice return $ruleWithRemapped; } }, $source); }
/** * Remaps CSS URL paths and automatically embeds data URIs for CSS rules * or url() values preceded by an / * @embed * / comment. * * @param string $source CSS data to remap * @param string $local File path where the source was read from * @param string $remote URL path to the file * @param bool $embedData If false, never do any data URI embedding, * even if / * @embed * / is found. * @return string Remapped CSS data */ public static function remap($source, $local, $remote, $embedData = true) { // High-level overview: // * For each CSS rule in $source that includes at least one url() value: // * Check for an @embed comment at the start indicating that all URIs should be embedded // * For each url() value: // * Check for an @embed comment directly preceding the value // * If either @embed comment exists: // * Embedding the URL as data: URI, if it's possible / allowed // * Otherwise remap the URL to work in generated stylesheets // Guard against trailing slashes, because "some/remote/../foo.png" // resolves to "some/remote/foo.png" on (some?) clients (bug 27052). if (substr($remote, -1) == '/') { $remote = substr($remote, 0, -1); } // Replace all comments by a placeholder so they will not interfere with the remapping. // Warning: This will also catch on anything looking like the start of a comment between // quotation marks (e.g. "foo /* bar"). $comments = array(); $placeholder = uniqid('', true); $pattern = '/(?!' . CSSMin::EMBED_REGEX . ')(' . CSSMin::COMMENT_REGEX . ')/s'; $source = preg_replace_callback($pattern, function ($match) use(&$comments, $placeholder) { $comments[] = $match[0]; return $placeholder . (count($comments) - 1) . 'x'; }, $source); // Note: This will not correctly handle cases where ';', '{' or '}' // appears in the rule itself, e.g. in a quoted string. You are advised // not to use such characters in file names. We also match start/end of // the string to be consistent in edge-cases ('@import url(…)'). $pattern = '/(?:^|[;{])\\K[^;{}]*' . CSSMin::URL_REGEX . '[^;}]*(?=[;}]|$)/'; $source = preg_replace_callback($pattern, function ($matchOuter) use($local, $remote, $embedData, $placeholder) { $rule = $matchOuter[0]; // Check for global @embed comment and remove it. Allow other comments to be present // before @embed (they have been replaced with placeholders at this point). $embedAll = false; $rule = preg_replace('/^((?:\\s+|' . $placeholder . '(\\d+)x)*)' . CSSMin::EMBED_REGEX . '\\s*/', '$1', $rule, 1, $embedAll); // Build two versions of current rule: with remapped URLs // and with embedded data: URIs (where possible). $pattern = '/(?P<embed>' . CSSMin::EMBED_REGEX . '\\s*|)' . CSSMin::URL_REGEX . '/'; $ruleWithRemapped = preg_replace_callback($pattern, function ($match) use($local, $remote) { $remapped = CSSMin::remapOne($match['file'], $match['query'], $local, $remote, false); return CSSMin::buildUrlValue($remapped); }, $rule); if ($embedData) { $ruleWithEmbedded = preg_replace_callback($pattern, function ($match) use($embedAll, $local, $remote) { $embed = $embedAll || $match['embed']; $embedded = CSSMin::remapOne($match['file'], $match['query'], $local, $remote, $embed); return CSSMin::buildUrlValue($embedded); }, $rule); } if ($embedData && $ruleWithEmbedded !== $ruleWithRemapped) { // Build 2 CSS properties; one which uses a base64 encoded data URI in place // of the @embed comment to try and retain line-number integrity, and the // other with a remapped an versioned URL and an Internet Explorer hack // making it ignored in all browsers that support data URIs return "{$ruleWithEmbedded};{$ruleWithRemapped}!ie"; } else { // No reason to repeat twice return $ruleWithRemapped; } }, $source); // Re-insert comments $pattern = '/' . $placeholder . '(\\d+)x/'; $source = preg_replace_callback($pattern, function ($match) use(&$comments) { return $comments[$match[1]]; }, $source); return $source; }