MediaWiki REL1_31
CSSMin.php
Go to the documentation of this file.
1<?php
30class 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" => "\\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 if ( isset( self::$mimeTypes[$ext] ) ) {
206 return self::$mimeTypes[$ext];
207 }
208
209 return mime_content_type( realpath( $file ) );
210 }
211
221 public static function buildUrlValue( $url ) {
222 // The list below has been crafted to match URLs such as:
223 // scheme://user@domain:port/~user/fi%20le.png?query=yes&really=y+s
224 // data:image/png;base64,R0lGODlh/+==
225 if ( preg_match( '!^[\w\d:@/~.%+;,?&=-]+$!', $url ) ) {
226 return "url($url)";
227 } else {
228 return 'url("' . strtr( $url, [ '\\' => '\\\\', '"' => '\\"' ] ) . '")';
229 }
230 }
231
243 public static function remap( $source, $local, $remote, $embedData = true ) {
244 // High-level overview:
245 // * For each CSS rule in $source that includes at least one url() value:
246 // * Check for an @embed comment at the start indicating that all URIs should be embedded
247 // * For each url() value:
248 // * Check for an @embed comment directly preceding the value
249 // * If either @embed comment exists:
250 // * Embedding the URL as data: URI, if it's possible / allowed
251 // * Otherwise remap the URL to work in generated stylesheets
252
253 // Guard against trailing slashes, because "some/remote/../foo.png"
254 // resolves to "some/remote/foo.png" on (some?) clients (T29052).
255 if ( substr( $remote, -1 ) == '/' ) {
256 $remote = substr( $remote, 0, -1 );
257 }
258
259 // Disallow U+007F DELETE, which is illegal anyway, and which
260 // we use for comment placeholders.
261 $source = str_replace( "\x7f", "?", $source );
262
263 // Replace all comments by a placeholder so they will not interfere with the remapping.
264 // Warning: This will also catch on anything looking like the start of a comment between
265 // quotation marks (e.g. "foo /* bar").
266 $comments = [];
267
268 $pattern = '/(?!' . self::EMBED_REGEX . ')(' . self::COMMENT_REGEX . ')/s';
269
270 $source = preg_replace_callback(
271 $pattern,
272 function ( $match ) use ( &$comments ) {
273 $comments[] = $match[ 0 ];
274 return CSSMin::PLACEHOLDER . ( count( $comments ) - 1 ) . 'x';
275 },
276 $source
277 );
278
279 // Note: This will not correctly handle cases where ';', '{' or '}'
280 // appears in the rule itself, e.g. in a quoted string. You are advised
281 // not to use such characters in file names. We also match start/end of
282 // the string to be consistent in edge-cases ('@import url(…)').
283 $pattern = '/(?:^|[;{])\K[^;{}]*' . self::getUrlRegex() . '[^;}]*(?=[;}]|$)/';
284
285 $source = preg_replace_callback(
286 $pattern,
287 function ( $matchOuter ) use ( $local, $remote, $embedData ) {
288 $rule = $matchOuter[0];
289
290 // Check for global @embed comment and remove it. Allow other comments to be present
291 // before @embed (they have been replaced with placeholders at this point).
292 $embedAll = false;
293 $rule = preg_replace(
294 '/^((?:\s+|' .
295 CSSMin::PLACEHOLDER .
296 '(\d+)x)*)' .
298 '\s*/',
299 '$1',
300 $rule,
301 1,
302 $embedAll
303 );
304
305 // Build two versions of current rule: with remapped URLs
306 // and with embedded data: URIs (where possible).
307 $pattern = '/(?P<embed>' . CSSMin::EMBED_REGEX . '\s*|)' . self::getUrlRegex() . '/';
308
309 $ruleWithRemapped = preg_replace_callback(
310 $pattern,
311 function ( $match ) use ( $local, $remote ) {
312 self::processUrlMatch( $match );
313
314 $remapped = CSSMin::remapOne( $match['file'], $match['query'], $local, $remote, false );
315 return CSSMin::buildUrlValue( $remapped );
316 },
317 $rule
318 );
319
320 if ( $embedData ) {
321 // Remember the occurring MIME types to avoid fallbacks when embedding some files.
322 $mimeTypes = [];
323
324 $ruleWithEmbedded = preg_replace_callback(
325 $pattern,
326 function ( $match ) use ( $embedAll, $local, $remote, &$mimeTypes ) {
327 self::processUrlMatch( $match );
328
329 $embed = $embedAll || $match['embed'];
330 $embedded = CSSMin::remapOne(
331 $match['file'],
332 $match['query'],
333 $local,
334 $remote,
335 $embed
336 );
337
338 $url = $match['file'] . $match['query'];
339 $file = "{$local}/{$match['file']}";
340 if (
341 !self::isRemoteUrl( $url ) && !self::isLocalUrl( $url )
342 && file_exists( $file )
343 ) {
344 $mimeTypes[ CSSMin::getMimeType( $file ) ] = true;
345 }
346
347 return CSSMin::buildUrlValue( $embedded );
348 },
349 $rule
350 );
351
352 // Are all referenced images SVGs?
353 $needsEmbedFallback = $mimeTypes !== [ 'image/svg+xml' => true ];
354 }
355
356 if ( !$embedData || $ruleWithEmbedded === $ruleWithRemapped ) {
357 // We're not embedding anything, or we tried to but the file is not embeddable
358 return $ruleWithRemapped;
359 } elseif ( $embedData && $needsEmbedFallback ) {
360 // Build 2 CSS properties; one which uses a data URI in place of the @embed comment, and
361 // the other with a remapped and versioned URL with an Internet Explorer 6 and 7 hack
362 // making it ignored in all browsers that support data URIs
363 return "$ruleWithEmbedded;$ruleWithRemapped!ie";
364 } else {
365 // Look ma, no fallbacks! This is for files which IE 6 and 7 don't support anyway: SVG.
366 return $ruleWithEmbedded;
367 }
368 }, $source );
369
370 // Re-insert comments
371 $pattern = '/' . self::PLACEHOLDER . '(\d+)x/';
372 $source = preg_replace_callback( $pattern, function ( $match ) use ( &$comments ) {
373 return $comments[ $match[1] ];
374 }, $source );
375
376 return $source;
377 }
378
385 protected static function isRemoteUrl( $maybeUrl ) {
386 if ( substr( $maybeUrl, 0, 2 ) === '//' || parse_url( $maybeUrl, PHP_URL_SCHEME ) ) {
387 return true;
388 }
389 return false;
390 }
391
398 protected static function isLocalUrl( $maybeUrl ) {
399 if ( $maybeUrl !== '' && $maybeUrl[0] === '/' && !self::isRemoteUrl( $maybeUrl ) ) {
400 return true;
401 }
402 return false;
403 }
404
408 private static function getUrlRegex() {
409 static $urlRegex;
410 if ( $urlRegex === null ) {
411 // Match these three variants separately to avoid broken urls when
412 // e.g. a double quoted url contains a parenthesis, or when a
413 // single quoted url contains a double quote, etc.
414 // FIXME: Simplify now we only support PHP 7.0.0+
415 // Note: PCRE doesn't support multiple capture groups with the same name by default.
416 // - PCRE 6.7 introduced the "J" modifier (PCRE_INFO_JCHANGED for PCRE_DUPNAMES).
417 // https://secure.php.net/manual/en/reference.pcre.pattern.modifiers.php
418 // However this isn't useful since it just ignores all but the first one.
419 // Also, while the modifier was introduced in PCRE 6.7 (PHP 5.2+) it was
420 // not exposed to public preg_* functions until PHP 5.6.0.
421 // - PCRE 8.36 fixed this to work as expected (e.g. merge conceptually to
422 // only return the one matched in the part that actually matched).
423 // However MediaWiki supports 5.5.9, which has PCRE 8.32
424 // Per https://secure.php.net/manual/en/pcre.installation.php:
425 // - PCRE 8.32 (PHP 5.5.0)
426 // - PCRE 8.34 (PHP 5.5.10, PHP 5.6.0)
427 // - PCRE 8.37 (PHP 5.5.26, PHP 5.6.9, PHP 7.0.0)
428 // Workaround by using different groups and merge via processUrlMatch().
429 // - Using string concatenation for class constant or member assignments
430 // is only supported in PHP 5.6. Use a getter method for now.
431 $urlRegex = '(' .
432 // Unquoted url
433 'url\‍(\s*(?P<file0>[^\s\'"][^\?\‍)]+?)(?P<query0>\?[^\‍)]*?|)\s*\‍)' .
434 // Single quoted url
435 '|url\‍(\s*\'(?P<file1>[^\?\']+?)(?P<query1>\?[^\']*?|)\'\s*\‍)' .
436 // Double quoted url
437 '|url\‍(\s*"(?P<file2>[^\?"]+?)(?P<query2>\?[^"]*?|)"\s*\‍)' .
438 ')';
439 }
440 return $urlRegex;
441 }
442
443 private static function processUrlMatch( array &$match, $flags = 0 ) {
444 if ( $flags & PREG_SET_ORDER ) {
445 // preg_match_all with PREG_SET_ORDER will return each group in each
446 // match array, and if it didn't match, instead of the sub array
447 // being an empty array it is `[ '', -1 ]`...
448 if ( isset( $match['file0'] ) && $match['file0'][1] !== -1 ) {
449 $match['file'] = $match['file0'];
450 $match['query'] = $match['query0'];
451 } elseif ( isset( $match['file1'] ) && $match['file1'][1] !== -1 ) {
452 $match['file'] = $match['file1'];
453 $match['query'] = $match['query1'];
454 } else {
455 if ( !isset( $match['file2'] ) || $match['file2'][1] === -1 ) {
456 throw new Exception( 'URL must be non-empty' );
457 }
458 $match['file'] = $match['file2'];
459 $match['query'] = $match['query2'];
460 }
461 } else {
462 if ( isset( $match['file0'] ) && $match['file0'] !== '' ) {
463 $match['file'] = $match['file0'];
464 $match['query'] = $match['query0'];
465 } elseif ( isset( $match['file1'] ) && $match['file1'] !== '' ) {
466 $match['file'] = $match['file1'];
467 $match['query'] = $match['query1'];
468 } else {
469 if ( !isset( $match['file2'] ) || $match['file2'] === '' ) {
470 throw new Exception( 'URL must be non-empty' );
471 }
472 $match['file'] = $match['file2'];
473 $match['query'] = $match['query2'];
474 }
475 }
476 }
477
488 public static function remapOne( $file, $query, $local, $remote, $embed ) {
489 // The full URL possibly with query, as passed to the 'url()' value in CSS
490 $url = $file . $query;
491
492 // Expand local URLs with absolute paths like /w/index.php to possibly protocol-relative URL, if
493 // wfExpandUrl() is available. (This will not be the case if we're running outside of MW.)
494 if ( self::isLocalUrl( $url ) && function_exists( 'wfExpandUrl' ) ) {
495 return wfExpandUrl( $url, PROTO_RELATIVE );
496 }
497
498 // Pass thru fully-qualified and protocol-relative URLs and data URIs, as well as local URLs if
499 // we can't expand them.
500 // Also skips anchors or the rare `behavior` property specifying application's default behavior
501 if (
502 self::isRemoteUrl( $url ) ||
503 self::isLocalUrl( $url ) ||
504 substr( $url, 0, 1 ) === '#'
505 ) {
506 return $url;
507 }
508
509 if ( $local === false ) {
510 // Assume that all paths are relative to $remote, and make them absolute
511 $url = $remote . '/' . $url;
512 } else {
513 // We drop the query part here and instead make the path relative to $remote
514 $url = "{$remote}/{$file}";
515 // Path to the actual file on the filesystem
516 $localFile = "{$local}/{$file}";
517 if ( file_exists( $localFile ) ) {
518 if ( $embed ) {
519 $data = self::encodeImageAsDataURI( $localFile );
520 if ( $data !== false ) {
521 return $data;
522 }
523 }
524 if ( method_exists( 'OutputPage', 'transformFilePath' ) ) {
525 $url = OutputPage::transformFilePath( $remote, $local, $file );
526 } else {
527 // Add version parameter as the first five hex digits
528 // of the MD5 hash of the file's contents.
529 $url .= '?' . substr( md5_file( $localFile ), 0, 5 );
530 }
531 }
532 // If any of these conditions failed (file missing, we don't want to embed it
533 // or it's not embeddable), return the URL (possibly with ?timestamp part)
534 }
535 if ( function_exists( 'wfRemoveDotSegments' ) ) {
536 $url = wfRemoveDotSegments( $url );
537 }
538 return $url;
539 }
540
547 public static function minify( $css ) {
548 return trim(
549 str_replace(
550 [ '; ', ': ', ' {', '{ ', ', ', '} ', ';}', '( ', ' )', '[ ', ' ]' ],
551 [ ';', ':', '{', '{', ',', '}', '}', '(', ')', '[', ']' ],
552 preg_replace( [ '/\s+/', '/\/\*.*?\*\//s' ], [ ' ', '' ], $css )
553 )
554 );
555 }
556}
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
wfRemoveDotSegments( $urlPath)
Remove all dot-segments in the provided URL path.
Transforms CSS data.
Definition CSSMin.php:30
static serializeStringValue( $value)
Serialize a string (escape and quote) for use as a CSS string value.
Definition CSSMin.php:190
static buildUrlValue( $url)
Build a CSS 'url()' value for the given URL, quoting parentheses (and other funny characters) and esc...
Definition CSSMin.php:221
const EMBED_REGEX
Definition CSSMin.php:40
static encodeImageAsDataURI( $file, $type=null, $ie8Compat=true)
Encode an image file as a data URI.
Definition CSSMin.php:114
static getMimeType( $file)
Definition CSSMin.php:202
static isRemoteUrl( $maybeUrl)
Is this CSS rule referencing a remote URL?
Definition CSSMin.php:385
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 minify( $css)
Removes whitespace from CSS data.
Definition CSSMin.php:547
static isLocalUrl( $maybeUrl)
Is this CSS rule referencing a local URL?
Definition CSSMin.php:398
static getUrlRegex()
Definition CSSMin.php:408
static array $mimeTypes
List of common image files extensions and MIME-types.
Definition CSSMin.php:44
static processUrlMatch(array &$match, $flags=0)
Definition CSSMin.php:443
static remapOne( $file, $query, $local, $remote, $embed)
Remap or embed a CSS URL path.
Definition CSSMin.php:488
const COMMENT_REGEX
Definition CSSMin.php:41
const DATA_URI_SIZE_LIMIT
Internet Explorer data URI length limit.
Definition CSSMin.php:38
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:243
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
const PROTO_RELATIVE
Definition Defines.php:231
the array() calling protocol came about after MediaWiki 1.4rc1.
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:2006
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:1620
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:37
$source
if(!is_readable( $file)) $ext
Definition router.php:55