MediaWiki  1.30.0
CSSMin.php
Go to the documentation of this file.
1 <?php
30 class CSSMin {
31 
32  /* Constants */
33 
35  const PLACEHOLDER = "\x7fPLACEHOLDER\x7f";
36 
40  const DATA_URI_SIZE_LIMIT = 32768;
41 
42  const EMBED_REGEX = '\/\*\s*\@embed\s*\*\/';
43  const COMMENT_REGEX = '\/\*.*?\*\/';
44 
45  /* Protected Static Members */
46 
48  protected static $mimeTypes = [
49  'gif' => 'image/gif',
50  'jpe' => 'image/jpeg',
51  'jpeg' => 'image/jpeg',
52  'jpg' => 'image/jpeg',
53  'png' => 'image/png',
54  'tif' => 'image/tiff',
55  'tiff' => 'image/tiff',
56  'xbm' => 'image/x-xbitmap',
57  'svg' => 'image/svg+xml',
58  ];
59 
60  /* Static Methods */
61 
69  public static function getLocalFileReferences( $source, $path ) {
70  $stripped = preg_replace( '/' . self::COMMENT_REGEX . '/s', '', $source );
71  $path = rtrim( $path, '/' ) . '/';
72  $files = [];
73 
74  $rFlags = PREG_OFFSET_CAPTURE | PREG_SET_ORDER;
75  if ( preg_match_all( '/' . self::getUrlRegex() . '/', $stripped, $matches, $rFlags ) ) {
76  foreach ( $matches as $match ) {
77  self::processUrlMatch( $match, $rFlags );
78  $url = $match['file'][0];
79 
80  // Skip fully-qualified and protocol-relative URLs and data URIs
81  // Also skips the rare `behavior` property specifying application's default behavior
82  if (
83  substr( $url, 0, 2 ) === '//' ||
84  parse_url( $url, PHP_URL_SCHEME ) ||
85  substr( $url, 0, 9 ) === '#default#'
86  ) {
87  break;
88  }
89 
90  $files[] = $path . $url;
91  }
92  }
93  return $files;
94  }
95 
111  public static function encodeImageAsDataURI( $file, $type = null, $ie8Compat = true ) {
112  // Fast-fail for files that definitely exceed the maximum data URI length
113  if ( $ie8Compat && filesize( $file ) >= self::DATA_URI_SIZE_LIMIT ) {
114  return false;
115  }
116 
117  if ( $type === null ) {
118  $type = self::getMimeType( $file );
119  }
120  if ( !$type ) {
121  return false;
122  }
123 
124  return self::encodeStringAsDataURI( file_get_contents( $file ), $type, $ie8Compat );
125  }
126 
139  public static function encodeStringAsDataURI( $contents, $type, $ie8Compat = true ) {
140  // Try #1: Non-encoded data URI
141  // The regular expression matches ASCII whitespace and printable characters.
142  if ( preg_match( '/^[\r\n\t\x20-\x7e]+$/', $contents ) ) {
143  // Do not base64-encode non-binary files (sane SVGs).
144  // (This often produces longer URLs, but they compress better, yielding a net smaller size.)
145  $encoded = rawurlencode( $contents );
146  // Unencode some things that don't need to be encoded, to make the encoding smaller
147  $encoded = strtr( $encoded, [
148  '%20' => ' ', // Unencode spaces
149  '%2F' => '/', // Unencode slashes
150  '%3A' => ':', // Unencode colons
151  '%3D' => '=', // Unencode equals signs
152  ] );
153  $uri = 'data:' . $type . ',' . $encoded;
154  if ( !$ie8Compat || strlen( $uri ) < self::DATA_URI_SIZE_LIMIT ) {
155  return $uri;
156  }
157  }
158 
159  // Try #2: Encoded data URI
160  $uri = 'data:' . $type . ';base64,' . base64_encode( $contents );
161  if ( !$ie8Compat || strlen( $uri ) < self::DATA_URI_SIZE_LIMIT ) {
162  return $uri;
163  }
164 
165  // A data URI couldn't be produced
166  return false;
167  }
168 
177  public static function serializeStringValue( $value ) {
178  if ( strstr( $value, "\0" ) ) {
179  throw new Exception( "Invalid character in CSS string" );
180  }
181  $value = strtr( $value, [ '\\' => '\\\\', '"' => '\\"' ] );
182  $value = preg_replace_callback( '/[\x01-\x1f\x7f-\x9f]/', function ( $match ) {
183  return '\\' . base_convert( ord( $match[0] ), 10, 16 ) . ' ';
184  }, $value );
185  return '"' . $value . '"';
186  }
187 
192  public static function getMimeType( $file ) {
193  // Infer the MIME-type from the file extension
194  $ext = strtolower( pathinfo( $file, PATHINFO_EXTENSION ) );
195  if ( isset( self::$mimeTypes[$ext] ) ) {
196  return self::$mimeTypes[$ext];
197  }
198 
199  return mime_content_type( realpath( $file ) );
200  }
201 
211  public static function buildUrlValue( $url ) {
212  // The list below has been crafted to match URLs such as:
213  // scheme://user@domain:port/~user/fi%20le.png?query=yes&really=y+s
214  // data:image/png;base64,R0lGODlh/+==
215  if ( preg_match( '!^[\w\d:@/~.%+;,?&=-]+$!', $url ) ) {
216  return "url($url)";
217  } else {
218  return 'url("' . strtr( $url, [ '\\' => '\\\\', '"' => '\\"' ] ) . '")';
219  }
220  }
221 
233  public static function remap( $source, $local, $remote, $embedData = true ) {
234  // High-level overview:
235  // * For each CSS rule in $source that includes at least one url() value:
236  // * Check for an @embed comment at the start indicating that all URIs should be embedded
237  // * For each url() value:
238  // * Check for an @embed comment directly preceding the value
239  // * If either @embed comment exists:
240  // * Embedding the URL as data: URI, if it's possible / allowed
241  // * Otherwise remap the URL to work in generated stylesheets
242 
243  // Guard against trailing slashes, because "some/remote/../foo.png"
244  // resolves to "some/remote/foo.png" on (some?) clients (T29052).
245  if ( substr( $remote, -1 ) == '/' ) {
246  $remote = substr( $remote, 0, -1 );
247  }
248 
249  // Disallow U+007F DELETE, which is illegal anyway, and which
250  // we use for comment placeholders.
251  $source = str_replace( "\x7f", "?", $source );
252 
253  // Replace all comments by a placeholder so they will not interfere with the remapping.
254  // Warning: This will also catch on anything looking like the start of a comment between
255  // quotation marks (e.g. "foo /* bar").
256  $comments = [];
257 
258  $pattern = '/(?!' . self::EMBED_REGEX . ')(' . self::COMMENT_REGEX . ')/s';
259 
260  $source = preg_replace_callback(
261  $pattern,
262  function ( $match ) use ( &$comments ) {
263  $comments[] = $match[ 0 ];
264  return CSSMin::PLACEHOLDER . ( count( $comments ) - 1 ) . 'x';
265  },
266  $source
267  );
268 
269  // Note: This will not correctly handle cases where ';', '{' or '}'
270  // appears in the rule itself, e.g. in a quoted string. You are advised
271  // not to use such characters in file names. We also match start/end of
272  // the string to be consistent in edge-cases ('@import url(…)').
273  $pattern = '/(?:^|[;{])\K[^;{}]*' . self::getUrlRegex() . '[^;}]*(?=[;}]|$)/';
274 
275  $source = preg_replace_callback(
276  $pattern,
277  function ( $matchOuter ) use ( $local, $remote, $embedData ) {
278  $rule = $matchOuter[0];
279 
280  // Check for global @embed comment and remove it. Allow other comments to be present
281  // before @embed (they have been replaced with placeholders at this point).
282  $embedAll = false;
283  $rule = preg_replace(
284  '/^((?:\s+|' .
285  CSSMin::PLACEHOLDER .
286  '(\d+)x)*)' .
288  '\s*/',
289  '$1',
290  $rule,
291  1,
292  $embedAll
293  );
294 
295  // Build two versions of current rule: with remapped URLs
296  // and with embedded data: URIs (where possible).
297  $pattern = '/(?P<embed>' . CSSMin::EMBED_REGEX . '\s*|)' . self::getUrlRegex() . '/';
298 
299  $ruleWithRemapped = preg_replace_callback(
300  $pattern,
301  function ( $match ) use ( $local, $remote ) {
302  self::processUrlMatch( $match );
303 
304  $remapped = CSSMin::remapOne( $match['file'], $match['query'], $local, $remote, false );
305  return CSSMin::buildUrlValue( $remapped );
306  },
307  $rule
308  );
309 
310  if ( $embedData ) {
311  // Remember the occurring MIME types to avoid fallbacks when embedding some files.
312  $mimeTypes = [];
313 
314  $ruleWithEmbedded = preg_replace_callback(
315  $pattern,
316  function ( $match ) use ( $embedAll, $local, $remote, &$mimeTypes ) {
317  self::processUrlMatch( $match );
318 
319  $embed = $embedAll || $match['embed'];
320  $embedded = CSSMin::remapOne(
321  $match['file'],
322  $match['query'],
323  $local,
324  $remote,
325  $embed
326  );
327 
328  $url = $match['file'] . $match['query'];
329  $file = "{$local}/{$match['file']}";
330  if (
331  !self::isRemoteUrl( $url ) && !self::isLocalUrl( $url )
332  && file_exists( $file )
333  ) {
334  $mimeTypes[ CSSMin::getMimeType( $file ) ] = true;
335  }
336 
337  return CSSMin::buildUrlValue( $embedded );
338  },
339  $rule
340  );
341 
342  // Are all referenced images SVGs?
343  $needsEmbedFallback = $mimeTypes !== [ 'image/svg+xml' => true ];
344  }
345 
346  if ( !$embedData || $ruleWithEmbedded === $ruleWithRemapped ) {
347  // We're not embedding anything, or we tried to but the file is not embeddable
348  return $ruleWithRemapped;
349  } elseif ( $embedData && $needsEmbedFallback ) {
350  // Build 2 CSS properties; one which uses a data URI in place of the @embed comment, and
351  // the other with a remapped and versioned URL with an Internet Explorer 6 and 7 hack
352  // making it ignored in all browsers that support data URIs
353  return "$ruleWithEmbedded;$ruleWithRemapped!ie";
354  } else {
355  // Look ma, no fallbacks! This is for files which IE 6 and 7 don't support anyway: SVG.
356  return $ruleWithEmbedded;
357  }
358  }, $source );
359 
360  // Re-insert comments
361  $pattern = '/' . self::PLACEHOLDER . '(\d+)x/';
362  $source = preg_replace_callback( $pattern, function ( $match ) use ( &$comments ) {
363  return $comments[ $match[1] ];
364  }, $source );
365 
366  return $source;
367  }
368 
375  protected static function isRemoteUrl( $maybeUrl ) {
376  if ( substr( $maybeUrl, 0, 2 ) === '//' || parse_url( $maybeUrl, PHP_URL_SCHEME ) ) {
377  return true;
378  }
379  return false;
380  }
381 
388  protected static function isLocalUrl( $maybeUrl ) {
389  if ( $maybeUrl !== '' && $maybeUrl[0] === '/' && !self::isRemoteUrl( $maybeUrl ) ) {
390  return true;
391  }
392  return false;
393  }
394 
398  private static function getUrlRegex() {
399  static $urlRegex;
400  if ( $urlRegex === null ) {
401  // Match these three variants separately to avoid broken urls when
402  // e.g. a double quoted url contains a parenthesis, or when a
403  // single quoted url contains a double quote, etc.
404  // Note: PCRE doesn't support multiple capture groups with the same name by default.
405  // - PCRE 6.7 introduced the "J" modifier (PCRE_INFO_JCHANGED for PCRE_DUPNAMES).
406  // https://secure.php.net/manual/en/reference.pcre.pattern.modifiers.php
407  // However this isn't useful since it just ignores all but the first one.
408  // Also, while the modifier was introduced in PCRE 6.7 (PHP 5.2+) it was
409  // not exposed to public preg_* functions until PHP 5.6.0.
410  // - PCRE 8.36 fixed this to work as expected (e.g. merge conceptually to
411  // only return the one matched in the part that actually matched).
412  // However MediaWiki supports 5.5.9, which has PCRE 8.32
413  // Per https://secure.php.net/manual/en/pcre.installation.php:
414  // - PCRE 8.32 (PHP 5.5.0)
415  // - PCRE 8.34 (PHP 5.5.10, PHP 5.6.0)
416  // - PCRE 8.37 (PHP 5.5.26, PHP 5.6.9, PHP 7.0.0)
417  // Workaround by using different groups and merge via processUrlMatch().
418  // - Using string concatenation for class constant or member assignments
419  // is only supported in PHP 5.6. Use a getter method for now.
420  $urlRegex = '(' .
421  // Unquoted url
422  'url\(\s*(?P<file0>[^\'"][^\?\)]*?)(?P<query0>\?[^\)]*?|)\s*\)' .
423  // Single quoted url
424  '|url\(\s*\'(?P<file1>[^\?\']*?)(?P<query1>\?[^\']*?|)\'\s*\)' .
425  // Double quoted url
426  '|url\(\s*"(?P<file2>[^\?"]*?)(?P<query2>\?[^"]*?|)"\s*\)' .
427  ')';
428  }
429  return $urlRegex;
430  }
431 
432  private static function processUrlMatch( array &$match, $flags = 0 ) {
433  if ( $flags & PREG_SET_ORDER ) {
434  // preg_match_all with PREG_SET_ORDER will return each group in each
435  // match array, and if it didn't match, instead of the sub array
436  // being an empty array it is `[ '', -1 ]`...
437  if ( isset( $match['file0'] ) && $match['file0'][1] !== -1 ) {
438  $match['file'] = $match['file0'];
439  $match['query'] = $match['query0'];
440  } elseif ( isset( $match['file1'] ) && $match['file1'][1] !== -1 ) {
441  $match['file'] = $match['file1'];
442  $match['query'] = $match['query1'];
443  } else {
444  $match['file'] = $match['file2'];
445  $match['query'] = $match['query2'];
446  }
447  } else {
448  if ( isset( $match['file0'] ) && $match['file0'] !== '' ) {
449  $match['file'] = $match['file0'];
450  $match['query'] = $match['query0'];
451  } elseif ( isset( $match['file1'] ) && $match['file1'] !== '' ) {
452  $match['file'] = $match['file1'];
453  $match['query'] = $match['query1'];
454  } else {
455  $match['file'] = $match['file2'];
456  $match['query'] = $match['query2'];
457  }
458  }
459  }
460 
471  public static function remapOne( $file, $query, $local, $remote, $embed ) {
472  // The full URL possibly with query, as passed to the 'url()' value in CSS
473  $url = $file . $query;
474 
475  // Expand local URLs with absolute paths like /w/index.php to possibly protocol-relative URL, if
476  // wfExpandUrl() is available. (This will not be the case if we're running outside of MW.)
477  if ( self::isLocalUrl( $url ) && function_exists( 'wfExpandUrl' ) ) {
478  return wfExpandUrl( $url, PROTO_RELATIVE );
479  }
480 
481  // Pass thru fully-qualified and protocol-relative URLs and data URIs, as well as local URLs if
482  // we can't expand them.
483  // Also skips the rare `behavior` property specifying application's default behavior
484  if (
485  self::isRemoteUrl( $url ) ||
486  self::isLocalUrl( $url ) ||
487  substr( $url, 0, 9 ) === '#default#'
488  ) {
489  return $url;
490  }
491 
492  if ( $local === false ) {
493  // Assume that all paths are relative to $remote, and make them absolute
494  $url = $remote . '/' . $url;
495  } else {
496  // We drop the query part here and instead make the path relative to $remote
497  $url = "{$remote}/{$file}";
498  // Path to the actual file on the filesystem
499  $localFile = "{$local}/{$file}";
500  if ( file_exists( $localFile ) ) {
501  if ( $embed ) {
502  $data = self::encodeImageAsDataURI( $localFile );
503  if ( $data !== false ) {
504  return $data;
505  }
506  }
507  if ( method_exists( 'OutputPage', 'transformFilePath' ) ) {
508  $url = OutputPage::transformFilePath( $remote, $local, $file );
509  } else {
510  // Add version parameter as the first five hex digits
511  // of the MD5 hash of the file's contents.
512  $url .= '?' . substr( md5_file( $localFile ), 0, 5 );
513  }
514  }
515  // If any of these conditions failed (file missing, we don't want to embed it
516  // or it's not embeddable), return the URL (possibly with ?timestamp part)
517  }
518  if ( function_exists( 'wfRemoveDotSegments' ) ) {
519  $url = wfRemoveDotSegments( $url );
520  }
521  return $url;
522  }
523 
530  public static function minify( $css ) {
531  return trim(
532  str_replace(
533  [ '; ', ': ', ' {', '{ ', ', ', '} ', ';}' ],
534  [ ';', ':', '{', '{', ',', '}', '}' ],
535  preg_replace( [ '/\s+/', '/\/\*.*?\*\//s' ], [ ' ', '' ], $css )
536  )
537  );
538  }
539 }
CSSMin\isRemoteUrl
static isRemoteUrl( $maybeUrl)
Is this CSS rule referencing a remote URL?
Definition: CSSMin.php:375
CSSMin\buildUrlValue
static buildUrlValue( $url)
Build a CSS 'url()' value for the given URL, quoting parentheses (and other funny characters) and esc...
Definition: CSSMin.php:211
CSSMin\minify
static minify( $css)
Removes whitespace from CSS data.
Definition: CSSMin.php:530
CSSMin\encodeStringAsDataURI
static encodeStringAsDataURI( $contents, $type, $ie8Compat=true)
Encode file contents as a data URI with chosen MIME type.
Definition: CSSMin.php:139
captcha-old.count
count
Definition: captcha-old.py:249
CSSMin\remap
static remap( $source, $local, $remote, $embedData=true)
Remaps CSS URL paths and automatically embeds data URIs for CSS rules or url() values preceded by an ...
Definition: CSSMin.php:233
wfRemoveDotSegments
wfRemoveDotSegments( $urlPath)
Remove all dot-segments in the provided URL path.
Definition: GlobalFunctions.php:714
CSSMin\$mimeTypes
static array $mimeTypes
List of common image files extensions and MIME-types.
Definition: CSSMin.php:48
use
as see the revision history and available at free of to any person obtaining a copy of this software and associated documentation to deal in the Software without including without limitation the rights to use
Definition: MIT-LICENSE.txt:10
CSSMin\serializeStringValue
static serializeStringValue( $value)
Serialize a string (escape and quote) for use as a CSS string value.
Definition: CSSMin.php:177
CSSMin\encodeImageAsDataURI
static encodeImageAsDataURI( $file, $type=null, $ie8Compat=true)
Encode an image file as a data URI.
Definition: CSSMin.php:111
php
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:35
CSSMin\EMBED_REGEX
const EMBED_REGEX
Definition: CSSMin.php:42
$query
null for the wiki Added should default to null in handler for backwards compatibility add a value to it if you want to add a cookie that have to vary cache options can modify $query
Definition: hooks.txt:1581
$css
$css
Definition: styleTest.css.php:50
$matches
$matches
Definition: NoLocalSettings.php:24
CSSMin\processUrlMatch
static processUrlMatch(array &$match, $flags=0)
Definition: CSSMin.php:432
CSSMin\getUrlRegex
static getUrlRegex()
Definition: CSSMin.php:398
CSSMin\isLocalUrl
static isLocalUrl( $maybeUrl)
Is this CSS rule referencing a local URL?
Definition: CSSMin.php:388
$value
$value
Definition: styleTest.css.php:45
CSSMin\getLocalFileReferences
static getLocalFileReferences( $source, $path)
Get a list of local files referenced in a stylesheet (includes non-existent files).
Definition: CSSMin.php:69
PROTO_RELATIVE
const PROTO_RELATIVE
Definition: Defines.php:222
CSSMin\DATA_URI_SIZE_LIMIT
const DATA_URI_SIZE_LIMIT
Internet Explorer data URI length limit.
Definition: CSSMin.php:40
CSSMin\COMMENT_REGEX
const COMMENT_REGEX
Definition: CSSMin.php:43
$ext
$ext
Definition: NoLocalSettings.php:25
CSSMin\getMimeType
static getMimeType( $file)
Definition: CSSMin.php:192
CSSMin
Transforms CSS data.
Definition: CSSMin.php:30
$path
$path
Definition: NoLocalSettings.php:26
as
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
Definition: distributors.txt:9
$source
$source
Definition: mwdoc-filter.php:46
true
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return true
Definition: hooks.txt:1965
CSSMin\remapOne
static remapOne( $file, $query, $local, $remote, $embed)
Remap or embed a CSS URL path.
Definition: CSSMin.php:471
$flags
it s the revision text itself In either if gzip is the revision text is gzipped $flags
Definition: hooks.txt:2801
wfExpandUrl
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
Definition: GlobalFunctions.php:586
array
the array() calling protocol came about after MediaWiki 1.4rc1.
OutputPage\transformFilePath
static transformFilePath( $remotePathPrefix, $localPath, $file)
Utility method for transformResourceFilePath().
Definition: OutputPage.php:3819
$type
$type
Definition: testCompression.php:48