33 const PLACEHOLDER =
"\x7fPLACEHOLDER\x7f";
38 const DATA_URI_SIZE_LIMIT = 32768;
40 const EMBED_REGEX =
'\/\*\s*\@embed\s*\*\/';
41 const COMMENT_REGEX =
'\/\*.*?\*\/';
44 protected static $mimeTypes = [
46 'jpe' =>
'image/jpeg',
47 'jpeg' =>
'image/jpeg',
48 'jpg' =>
'image/jpeg',
50 'tif' =>
'image/tiff',
51 'tiff' =>
'image/tiff',
52 'xbm' =>
'image/x-xbitmap',
53 'svg' =>
'image/svg+xml',
63 public static function getLocalFileReferences(
$source,
$path ) {
64 $stripped = preg_replace(
'/' . self::COMMENT_REGEX .
'/s',
'',
$source );
68 $rFlags = PREG_OFFSET_CAPTURE | PREG_SET_ORDER;
69 if ( preg_match_all(
'/' . self::getUrlRegex() .
'/', $stripped,
$matches, $rFlags ) ) {
71 self::processUrlMatch( $match, $rFlags );
72 $url = $match[
'file'][0];
76 substr( $url, 0, 2 ) ===
'//' ||
77 parse_url( $url, PHP_URL_SCHEME )
83 $anchor = strpos( $url,
'#' );
84 if ( $anchor !==
false ) {
85 $url = substr( $url, 0, $anchor );
93 $files[] =
$path . $url;
114 public static function encodeImageAsDataURI(
$file,
$type =
null, $ie8Compat =
true ) {
116 if ( $ie8Compat && filesize(
$file ) >= self::DATA_URI_SIZE_LIMIT ) {
120 if (
$type ===
null ) {
127 return self::encodeStringAsDataURI( file_get_contents(
$file ),
$type, $ie8Compat );
142 public static function encodeStringAsDataURI( $contents,
$type, $ie8Compat =
true ) {
146 $contents = preg_replace(
"/<\\?xml.*?\\?>/",
'', $contents );
148 if ( preg_match(
'/^[\r\n\t\x20-\x7e]+$/', $contents ) ) {
151 $encoded = rawurlencode( $contents );
153 $encoded = strtr( $encoded, [
163 $encoded = preg_replace(
'/ {2,}/',
' ', $encoded );
165 $encoded = preg_replace(
'/^ | $/',
'', $encoded );
167 $uri =
'data:' .
$type .
',' . $encoded;
168 if ( !$ie8Compat || strlen( $uri ) < self::DATA_URI_SIZE_LIMIT ) {
174 $uri =
'data:' .
$type .
';base64,' . base64_encode( $contents );
175 if ( !$ie8Compat || strlen( $uri ) < self::DATA_URI_SIZE_LIMIT ) {
190 public static function serializeStringValue( $value ) {
191 $value = strtr( $value, [
"\0" =>
"\u{FFFD}",
'\\' =>
'\\\\',
'"' =>
'\\"' ] );
192 $value = preg_replace_callback(
'/[\x01-\x1f\x7f]/',
function ( $match ) {
193 return '\\' . base_convert( ord( $match[0] ), 10, 16 ) .
' ';
195 return '"' . $value .
'"';
202 public static function getMimeType(
$file ) {
204 $ext = strtolower( pathinfo(
$file, PATHINFO_EXTENSION ) );
205 return self::$mimeTypes[
$ext] ?? mime_content_type( realpath(
$file ) );
217 public static function buildUrlValue( $url ) {
221 if ( preg_match(
'!^[\w\d:@/~.%+;,?&=-]+$!', $url ) ) {
224 return 'url("' . strtr( $url, [
'\\' =>
'\\\\',
'"' =>
'\\"' ] ) .
'")';
239 public static function remap(
$source, $local, $remote, $embedData =
true ) {
251 if ( substr( $remote, -1 ) ==
'/' ) {
252 $remote = substr( $remote, 0, -1 );
264 $pattern =
'/(?!' . self::EMBED_REGEX .
')(' . self::COMMENT_REGEX .
')/s';
266 $source = preg_replace_callback(
268 function ( $match ) use ( &$comments ) {
269 $comments[] = $match[ 0 ];
270 return CSSMin::PLACEHOLDER . ( count( $comments ) - 1 ) .
'x';
279 $pattern =
'/(?:^|[;{])\K[^;{}]*' . self::getUrlRegex() .
'[^;}]*(?=[;}]|$)/';
281 $source = preg_replace_callback(
283 function ( $matchOuter ) use ( $local, $remote, $embedData ) {
284 $rule = $matchOuter[0];
289 $rule = preg_replace(
291 CSSMin::PLACEHOLDER .
293 CSSMin::EMBED_REGEX .
303 $pattern =
'/(?P<embed>' . CSSMin::EMBED_REGEX .
'\s*|)' . self::getUrlRegex() .
'/';
305 $ruleWithRemapped = preg_replace_callback(
307 function ( $match ) use ( $local, $remote ) {
308 self::processUrlMatch( $match );
310 $remapped = CSSMin::remapOne( $match[
'file'], $match[
'query'], $local, $remote,
false );
311 return CSSMin::buildUrlValue( $remapped );
320 $ruleWithEmbedded = preg_replace_callback(
322 function ( $match ) use ( $embedAll, $local, $remote, &$mimeTypes ) {
323 self::processUrlMatch( $match );
325 $embed = $embedAll || $match[
'embed'];
326 $embedded = CSSMin::remapOne(
334 $url = $match[
'file'] . $match[
'query'];
335 $file =
"{$local}/{$match['file']}";
337 !self::isRemoteUrl( $url ) && !self::isLocalUrl( $url )
338 && file_exists(
$file )
340 $mimeTypes[ CSSMin::getMimeType(
$file ) ] =
true;
343 return CSSMin::buildUrlValue( $embedded );
349 $needsEmbedFallback = $mimeTypes !== [
'image/svg+xml' =>
true ];
352 if ( !$embedData || $ruleWithEmbedded === $ruleWithRemapped ) {
354 return $ruleWithRemapped;
355 } elseif ( $embedData && $needsEmbedFallback ) {
359 return "$ruleWithEmbedded;$ruleWithRemapped!ie";
362 return $ruleWithEmbedded;
367 $pattern =
'/' . self::PLACEHOLDER .
'(\d+)x/';
368 $source = preg_replace_callback( $pattern,
function ( $match ) use ( &$comments ) {
369 return $comments[ $match[1] ];
381 protected static function isRemoteUrl( $maybeUrl ) {
382 if ( substr( $maybeUrl, 0, 2 ) ===
'//' || parse_url( $maybeUrl, PHP_URL_SCHEME ) ) {
394 protected static function isLocalUrl( $maybeUrl ) {
395 return isset( $maybeUrl[1] ) && $maybeUrl[0] ===
'/' && $maybeUrl[1] !==
'/';
401 private static function getUrlRegex() {
403 if ( $urlRegex ===
null ) {
426 'url\(\s*(?P<file0>[^\s\'"][^\?\)]+?)(?P<query0>\?[^\)]*?|)\s*\)' .
428 '|url\(\s*\'(?P<file1>[^\?\']+?)(?P<query1>\?[^\']*?|)\'\s*\)' .
430 '|url\(\s*"(?P<file2>[^\?"]+?)(?P<query2>\?[^"]*?|)"\s*\)' .
436 private static function processUrlMatch( array &$match, $flags = 0 ) {
437 if ( $flags & PREG_SET_ORDER ) {
441 if ( isset( $match[
'file0'] ) && $match[
'file0'][1] !== -1 ) {
442 $match[
'file'] = $match[
'file0'];
443 $match[
'query'] = $match[
'query0'];
444 } elseif ( isset( $match[
'file1'] ) && $match[
'file1'][1] !== -1 ) {
445 $match[
'file'] = $match[
'file1'];
446 $match[
'query'] = $match[
'query1'];
448 if ( !isset( $match[
'file2'] ) || $match[
'file2'][1] === -1 ) {
449 throw new Exception(
'URL must be non-empty' );
451 $match[
'file'] = $match[
'file2'];
452 $match[
'query'] = $match[
'query2'];
455 if ( isset( $match[
'file0'] ) && $match[
'file0'] !==
'' ) {
456 $match[
'file'] = $match[
'file0'];
457 $match[
'query'] = $match[
'query0'];
458 } elseif ( isset( $match[
'file1'] ) && $match[
'file1'] !==
'' ) {
459 $match[
'file'] = $match[
'file1'];
460 $match[
'query'] = $match[
'query1'];
462 if ( !isset( $match[
'file2'] ) || $match[
'file2'] ===
'' ) {
463 throw new Exception(
'URL must be non-empty' );
465 $match[
'file'] = $match[
'file2'];
466 $match[
'query'] = $match[
'query2'];
481 public static function remapOne(
$file, $query, $local, $remote, $embed ) {
483 $url =
$file . $query;
487 if ( self::isLocalUrl( $url ) && function_exists(
'wfExpandUrl' ) ) {
495 self::isRemoteUrl( $url ) ||
496 self::isLocalUrl( $url ) ||
497 substr( $url, 0, 1 ) ===
'#'
502 if ( $local ===
false ) {
504 $url = $remote .
'/' . $url;
507 $url =
"{$remote}/{$file}";
509 $localFile =
"{$local}/{$file}";
510 if ( file_exists( $localFile ) ) {
512 $data = self::encodeImageAsDataURI( $localFile );
513 if ( $data !==
false ) {
517 if ( class_exists( OutputPage::class ) ) {
518 $url = OutputPage::transformFilePath( $remote, $local,
$file );
522 $url .=
'?' . substr( md5_file( $localFile ), 0, 5 );
528 if ( function_exists(
'wfRemoveDotSegments' ) ) {
540 public static function minify( $css ) {
543 [
'; ',
': ',
' {',
'{ ',
', ',
'} ',
';}',
'( ',
' )',
'[ ',
' ]' ],
544 [
';',
':',
'{',
'{',
',',
'}',
'}',
'(',
')',
'[',
']' ],
545 preg_replace( [
'/\s+/',
'/\/\*.*?\*\//s' ], [
' ',
'' ], $css )