MediaWiki  master
CSSMin.php
Go to the documentation of this file.
1 <?php
30 class CSSMin {
31 
33  const PLACEHOLDER = "\x7fPLACEHOLDER\x7f";
34 
38  const DATA_URI_SIZE_LIMIT = 32768;
39 
40  const EMBED_REGEX = '\/\*\s*\@embed\s*\*\/';
41  const COMMENT_REGEX = '\/\*.*?\*\/';
42 
44  protected static $mimeTypes = [
45  'gif' => 'image/gif',
46  'jpe' => 'image/jpeg',
47  'jpeg' => 'image/jpeg',
48  'jpg' => 'image/jpeg',
49  'png' => 'image/png',
50  'tif' => 'image/tiff',
51  'tiff' => 'image/tiff',
52  'xbm' => 'image/x-xbitmap',
53  'svg' => 'image/svg+xml',
54  ];
55 
63  public static function getLocalFileReferences( $source, $path ) {
64  $stripped = preg_replace( '/' . self::COMMENT_REGEX . '/s', '', $source );
65  $path = rtrim( $path, '/' ) . '/';
66  $files = [];
67 
68  $rFlags = PREG_OFFSET_CAPTURE | PREG_SET_ORDER;
69  if ( preg_match_all( '/' . self::getUrlRegex() . '/', $stripped, $matches, $rFlags ) ) {
70  foreach ( $matches as $match ) {
71  self::processUrlMatch( $match, $rFlags );
72  $url = $match['file'][0];
73 
74  // Skip fully-qualified and protocol-relative URLs and data URIs
75  if (
76  substr( $url, 0, 2 ) === '//' ||
77  parse_url( $url, PHP_URL_SCHEME )
78  ) {
79  break;
80  }
81 
82  // Strip trailing anchors - T115436
83  $anchor = strpos( $url, '#' );
84  if ( $anchor !== false ) {
85  $url = substr( $url, 0, $anchor );
86 
87  // '#some-anchors' is not a file
88  if ( $url === '' ) {
89  break;
90  }
91  }
92 
93  $files[] = $path . $url;
94  }
95  }
96  return $files;
97  }
98 
114  public static function encodeImageAsDataURI( $file, $type = null, $ie8Compat = true ) {
115  // Fast-fail for files that definitely exceed the maximum data URI length
116  if ( $ie8Compat && filesize( $file ) >= self::DATA_URI_SIZE_LIMIT ) {
117  return false;
118  }
119 
120  if ( $type === null ) {
121  $type = self::getMimeType( $file );
122  }
123  if ( !$type ) {
124  return false;
125  }
126 
127  return self::encodeStringAsDataURI( file_get_contents( $file ), $type, $ie8Compat );
128  }
129 
142  public static function encodeStringAsDataURI( $contents, $type, $ie8Compat = true ) {
143  // Try #1: Non-encoded data URI
144 
145  // Remove XML declaration, it's not needed with data URI usage
146  $contents = preg_replace( "/<\\?xml.*?\\?>/", '', $contents );
147  // The regular expression matches ASCII whitespace and printable characters.
148  if ( preg_match( '/^[\r\n\t\x20-\x7e]+$/', $contents ) ) {
149  // Do not base64-encode non-binary files (sane SVGs).
150  // (This often produces longer URLs, but they compress better, yielding a net smaller size.)
151  $encoded = rawurlencode( $contents );
152  // Unencode some things that don't need to be encoded, to make the encoding smaller
153  $encoded = strtr( $encoded, [
154  '%20' => ' ', // Unencode spaces
155  '%2F' => '/', // Unencode slashes
156  '%3A' => ':', // Unencode colons
157  '%3D' => '=', // Unencode equals signs
158  '%0A' => ' ', // Change newlines to spaces
159  '%0D' => ' ', // Change carriage returns to spaces
160  '%09' => ' ', // Change tabs to spaces
161  ] );
162  // Consolidate runs of multiple spaces in a row
163  $encoded = preg_replace( '/ {2,}/', ' ', $encoded );
164  // Remove leading and trailing spaces
165  $encoded = preg_replace( '/^ | $/', '', $encoded );
166 
167  $uri = 'data:' . $type . ',' . $encoded;
168  if ( !$ie8Compat || strlen( $uri ) < self::DATA_URI_SIZE_LIMIT ) {
169  return $uri;
170  }
171  }
172 
173  // Try #2: Encoded data URI
174  $uri = 'data:' . $type . ';base64,' . base64_encode( $contents );
175  if ( !$ie8Compat || strlen( $uri ) < self::DATA_URI_SIZE_LIMIT ) {
176  return $uri;
177  }
178 
179  // A data URI couldn't be produced
180  return false;
181  }
182 
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 ) . ' ';
194  }, $value );
195  return '"' . $value . '"';
196  }
197 
202  public static function getMimeType( $file ) {
203  // Infer the MIME-type from the file extension
204  $ext = strtolower( pathinfo( $file, PATHINFO_EXTENSION ) );
205  return self::$mimeTypes[$ext] ?? mime_content_type( realpath( $file ) );
206  }
207 
217  public static function buildUrlValue( $url ) {
218  // The list below has been crafted to match URLs such as:
219  // scheme://user@domain:port/~user/fi%20le.png?query=yes&really=y+s
220  // data:image/png;base64,R0lGODlh/+==
221  if ( preg_match( '!^[\w\d:@/~.%+;,?&=-]+$!', $url ) ) {
222  return "url($url)";
223  } else {
224  return 'url("' . strtr( $url, [ '\\' => '\\\\', '"' => '\\"' ] ) . '")';
225  }
226  }
227 
239  public static function remap( $source, $local, $remote, $embedData = true ) {
240  // High-level overview:
241  // * For each CSS rule in $source that includes at least one url() value:
242  // * Check for an @embed comment at the start indicating that all URIs should be embedded
243  // * For each url() value:
244  // * Check for an @embed comment directly preceding the value
245  // * If either @embed comment exists:
246  // * Embedding the URL as data: URI, if it's possible / allowed
247  // * Otherwise remap the URL to work in generated stylesheets
248 
249  // Guard against trailing slashes, because "some/remote/../foo.png"
250  // resolves to "some/remote/foo.png" on (some?) clients (T29052).
251  if ( substr( $remote, -1 ) == '/' ) {
252  $remote = substr( $remote, 0, -1 );
253  }
254 
255  // Disallow U+007F DELETE, which is illegal anyway, and which
256  // we use for comment placeholders.
257  $source = str_replace( "\x7f", "?", $source );
258 
259  // Replace all comments by a placeholder so they will not interfere with the remapping.
260  // Warning: This will also catch on anything looking like the start of a comment between
261  // quotation marks (e.g. "foo /* bar").
262  $comments = [];
263 
264  $pattern = '/(?!' . self::EMBED_REGEX . ')(' . self::COMMENT_REGEX . ')/s';
265 
266  $source = preg_replace_callback(
267  $pattern,
268  function ( $match ) use ( &$comments ) {
269  $comments[] = $match[ 0 ];
270  return CSSMin::PLACEHOLDER . ( count( $comments ) - 1 ) . 'x';
271  },
272  $source
273  );
274 
275  // Note: This will not correctly handle cases where ';', '{' or '}'
276  // appears in the rule itself, e.g. in a quoted string. You are advised
277  // not to use such characters in file names. We also match start/end of
278  // the string to be consistent in edge-cases ('@import url(…)').
279  $pattern = '/(?:^|[;{])\K[^;{}]*' . self::getUrlRegex() . '[^;}]*(?=[;}]|$)/';
280 
281  $source = preg_replace_callback(
282  $pattern,
283  function ( $matchOuter ) use ( $local, $remote, $embedData ) {
284  $rule = $matchOuter[0];
285 
286  // Check for global @embed comment and remove it. Allow other comments to be present
287  // before @embed (they have been replaced with placeholders at this point).
288  $embedAll = false;
289  $rule = preg_replace(
290  '/^((?:\s+|' .
291  CSSMin::PLACEHOLDER .
292  '(\d+)x)*)' .
294  '\s*/',
295  '$1',
296  $rule,
297  1,
298  $embedAll
299  );
300 
301  // Build two versions of current rule: with remapped URLs
302  // and with embedded data: URIs (where possible).
303  $pattern = '/(?P<embed>' . CSSMin::EMBED_REGEX . '\s*|)' . self::getUrlRegex() . '/';
304 
305  $ruleWithRemapped = preg_replace_callback(
306  $pattern,
307  function ( $match ) use ( $local, $remote ) {
308  self::processUrlMatch( $match );
309 
310  $remapped = CSSMin::remapOne( $match['file'], $match['query'], $local, $remote, false );
311  return CSSMin::buildUrlValue( $remapped );
312  },
313  $rule
314  );
315 
316  if ( $embedData ) {
317  // Remember the occurring MIME types to avoid fallbacks when embedding some files.
318  $mimeTypes = [];
319 
320  $ruleWithEmbedded = preg_replace_callback(
321  $pattern,
322  function ( $match ) use ( $embedAll, $local, $remote, &$mimeTypes ) {
323  self::processUrlMatch( $match );
324 
325  $embed = $embedAll || $match['embed'];
326  $embedded = CSSMin::remapOne(
327  $match['file'],
328  $match['query'],
329  $local,
330  $remote,
331  $embed
332  );
333 
334  $url = $match['file'] . $match['query'];
335  $file = "{$local}/{$match['file']}";
336  if (
337  !self::isRemoteUrl( $url ) && !self::isLocalUrl( $url )
338  && file_exists( $file )
339  ) {
340  $mimeTypes[ CSSMin::getMimeType( $file ) ] = true;
341  }
342 
343  return CSSMin::buildUrlValue( $embedded );
344  },
345  $rule
346  );
347 
348  // Are all referenced images SVGs?
349  $needsEmbedFallback = $mimeTypes !== [ 'image/svg+xml' => true ];
350  }
351 
352  if ( !$embedData || $ruleWithEmbedded === $ruleWithRemapped ) {
353  // We're not embedding anything, or we tried to but the file is not embeddable
354  return $ruleWithRemapped;
355  } elseif ( $embedData && $needsEmbedFallback ) {
356  // Build 2 CSS properties; one which uses a data URI in place of the @embed comment, and
357  // the other with a remapped and versioned URL with an Internet Explorer 6 and 7 hack
358  // making it ignored in all browsers that support data URIs
359  return "$ruleWithEmbedded;$ruleWithRemapped!ie";
360  } else {
361  // Look ma, no fallbacks! This is for files which IE 6 and 7 don't support anyway: SVG.
362  return $ruleWithEmbedded;
363  }
364  }, $source );
365 
366  // Re-insert comments
367  $pattern = '/' . self::PLACEHOLDER . '(\d+)x/';
368  $source = preg_replace_callback( $pattern, function ( $match ) use ( &$comments ) {
369  return $comments[ $match[1] ];
370  }, $source );
371 
372  return $source;
373  }
374 
381  protected static function isRemoteUrl( $maybeUrl ) {
382  if ( substr( $maybeUrl, 0, 2 ) === '//' || parse_url( $maybeUrl, PHP_URL_SCHEME ) ) {
383  return true;
384  }
385  return false;
386  }
387 
394  protected static function isLocalUrl( $maybeUrl ) {
395  return isset( $maybeUrl[1] ) && $maybeUrl[0] === '/' && $maybeUrl[1] !== '/';
396  }
397 
401  private static function getUrlRegex() {
402  static $urlRegex;
403  if ( $urlRegex === null ) {
404  // Match these three variants separately to avoid broken urls when
405  // e.g. a double quoted url contains a parenthesis, or when a
406  // single quoted url contains a double quote, etc.
407  // FIXME: Simplify now we only support PHP 7.0.0+
408  // Note: PCRE doesn't support multiple capture groups with the same name by default.
409  // - PCRE 6.7 introduced the "J" modifier (PCRE_INFO_JCHANGED for PCRE_DUPNAMES).
410  // https://www.php.net/manual/en/reference.pcre.pattern.modifiers.php
411  // However this isn't useful since it just ignores all but the first one.
412  // Also, while the modifier was introduced in PCRE 6.7 (PHP 5.2+) it was
413  // not exposed to public preg_* functions until PHP 5.6.0.
414  // - PCRE 8.36 fixed this to work as expected (e.g. merge conceptually to
415  // only return the one matched in the part that actually matched).
416  // However MediaWiki supports 5.5.9, which has PCRE 8.32
417  // Per https://www.php.net/manual/en/pcre.installation.php:
418  // - PCRE 8.32 (PHP 5.5.0)
419  // - PCRE 8.34 (PHP 5.5.10, PHP 5.6.0)
420  // - PCRE 8.37 (PHP 5.5.26, PHP 5.6.9, PHP 7.0.0)
421  // Workaround by using different groups and merge via processUrlMatch().
422  // - Using string concatenation for class constant or member assignments
423  // is only supported in PHP 5.6. Use a getter method for now.
424  $urlRegex = '(' .
425  // Unquoted url
426  'url\(\s*(?P<file0>[^\s\'"][^\?\)]+?)(?P<query0>\?[^\)]*?|)\s*\)' .
427  // Single quoted url
428  '|url\(\s*\'(?P<file1>[^\?\']+?)(?P<query1>\?[^\']*?|)\'\s*\)' .
429  // Double quoted url
430  '|url\(\s*"(?P<file2>[^\?"]+?)(?P<query2>\?[^"]*?|)"\s*\)' .
431  ')';
432  }
433  return $urlRegex;
434  }
435 
436  private static function processUrlMatch( array &$match, $flags = 0 ) {
437  if ( $flags & PREG_SET_ORDER ) {
438  // preg_match_all with PREG_SET_ORDER will return each group in each
439  // match array, and if it didn't match, instead of the sub array
440  // being an empty array it is `[ '', -1 ]`...
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'];
447  } else {
448  if ( !isset( $match['file2'] ) || $match['file2'][1] === -1 ) {
449  throw new Exception( 'URL must be non-empty' );
450  }
451  $match['file'] = $match['file2'];
452  $match['query'] = $match['query2'];
453  }
454  } else {
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'];
461  } else {
462  if ( !isset( $match['file2'] ) || $match['file2'] === '' ) {
463  throw new Exception( 'URL must be non-empty' );
464  }
465  $match['file'] = $match['file2'];
466  $match['query'] = $match['query2'];
467  }
468  }
469  }
470 
481  public static function remapOne( $file, $query, $local, $remote, $embed ) {
482  // The full URL possibly with query, as passed to the 'url()' value in CSS
483  $url = $file . $query;
484 
485  // Expand local URLs with absolute paths like /w/index.php to possibly protocol-relative URL, if
486  // wfExpandUrl() is available. (This will not be the case if we're running outside of MW.)
487  if ( self::isLocalUrl( $url ) && function_exists( 'wfExpandUrl' ) ) {
488  return wfExpandUrl( $url, PROTO_RELATIVE );
489  }
490 
491  // Pass thru fully-qualified and protocol-relative URLs and data URIs, as well as local URLs if
492  // we can't expand them.
493  // Also skips anchors or the rare `behavior` property specifying application's default behavior
494  if (
495  self::isRemoteUrl( $url ) ||
496  self::isLocalUrl( $url ) ||
497  substr( $url, 0, 1 ) === '#'
498  ) {
499  return $url;
500  }
501 
502  if ( $local === false ) {
503  // Assume that all paths are relative to $remote, and make them absolute
504  $url = $remote . '/' . $url;
505  } else {
506  // We drop the query part here and instead make the path relative to $remote
507  $url = "{$remote}/{$file}";
508  // Path to the actual file on the filesystem
509  $localFile = "{$local}/{$file}";
510  if ( file_exists( $localFile ) ) {
511  if ( $embed ) {
512  $data = self::encodeImageAsDataURI( $localFile );
513  if ( $data !== false ) {
514  return $data;
515  }
516  }
517  if ( class_exists( OutputPage::class ) ) {
518  $url = OutputPage::transformFilePath( $remote, $local, $file );
519  } else {
520  // Add version parameter as the first five hex digits
521  // of the MD5 hash of the file's contents.
522  $url .= '?' . substr( md5_file( $localFile ), 0, 5 );
523  }
524  }
525  // If any of these conditions failed (file missing, we don't want to embed it
526  // or it's not embeddable), return the URL (possibly with ?timestamp part)
527  }
528  if ( function_exists( 'wfRemoveDotSegments' ) ) {
529  $url = wfRemoveDotSegments( $url );
530  }
531  return $url;
532  }
533 
540  public static function minify( $css ) {
541  return trim(
542  str_replace(
543  [ '; ', ': ', ' {', '{ ', ', ', '} ', ';}', '( ', ' )', '[ ', ' ]' ],
544  [ ';', ':', '{', '{', ',', '}', '}', '(', ')', '[', ']' ],
545  preg_replace( [ '/\s+/', '/\/\*.*?\*\//s' ], [ ' ', '' ], $css )
546  )
547  );
548  }
549 }
static isLocalUrl( $maybeUrl)
Is this CSS rule referencing a local URL?
Definition: CSSMin.php:394
static getUrlRegex()
Definition: CSSMin.php:401
return true to allow those checks to and false if checking is done remove or add to the links of a group of changes in EnhancedChangesList Hook subscribers can return false to omit this line from recentchanges use this to change the tables headers change it to an object instance and return false override the list derivative used $groups Array of ChangesListFilterGroup objects(added in 1.34) 'FileDeleteComplete' null for the local 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:1529
static remapOne( $file, $query, $local, $remote, $embed)
Remap or embed a CSS URL path.
Definition: CSSMin.php:481
static serializeStringValue( $value)
Serialize a string (escape and quote) for use as a CSS string value.
Definition: CSSMin.php:190
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Definition: router.php:42
const EMBED_REGEX
Definition: CSSMin.php:40
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
static isRemoteUrl( $maybeUrl)
Is this CSS rule referencing a remote URL?
Definition: CSSMin.php:381
$source
$value
static minify( $css)
Removes whitespace from CSS data.
Definition: CSSMin.php:540
static getMimeType( $file)
Definition: CSSMin.php:202
const COMMENT_REGEX
Definition: CSSMin.php:41
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:239
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:1972
wfRemoveDotSegments( $urlPath)
Remove all dot-segments in the provided URL path.
$css
static buildUrlValue( $url)
Build a CSS &#39;url()&#39; value for the given URL, quoting parentheses (and other funny characters) and esc...
Definition: CSSMin.php:217
static string [] static getLocalFileReferences( $source, $path)
Get a list of local files referenced in a stylesheet (includes non-existent files).
Definition: CSSMin.php:63
static encodeStringAsDataURI( $contents, $type, $ie8Compat=true)
Encode file contents as a data URI with chosen MIME type.
Definition: CSSMin.php:142
static encodeImageAsDataURI( $file, $type=null, $ie8Compat=true)
Encode an image file as a data URI.
Definition: CSSMin.php:114
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not null
Definition: hooks.txt:767
const PROTO_RELATIVE
Definition: Defines.php:201
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
static transformFilePath( $remotePathPrefix, $localPath, $file)
Utility method for transformResourceFilePath().
static string [] $mimeTypes
List of common image files extensions and MIME-types.
Definition: CSSMin.php:44
static processUrlMatch(array &$match, $flags=0)
Definition: CSSMin.php:436
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
you have access to all of the normal MediaWiki so you can get a DB use the etc For full docs on the Maintenance class
Definition: maintenance.txt:52
if(!is_readable( $file)) $ext
Definition: router.php:48
const DATA_URI_SIZE_LIMIT
Internet Explorer data URI length limit.
Definition: CSSMin.php:38
Transforms CSS data.
Definition: CSSMin.php:30
$matches