MediaWiki REL1_33
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" => "\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
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
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://secure.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://secure.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(
543 [ '; ', ': ', ' {', '{ ', ', ', '} ', ';}', '( ', ' )', '[ ', ' ]' ],
544 [ ';', ':', '{', '{', ',', '}', '}', '(', ')', '[', ']' ],
545 preg_replace( [ '/\s+/', '/\/\*.*?\*\//s' ], [ ' ', '' ], $css )
546 )
547 );
548 }
549}
and that you know you can do these things To protect your we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights These restrictions translate to certain responsibilities for you if you distribute copies of the or if you modify it For if you distribute copies of such a whether gratis or for a you must give the recipients all the rights that you have You must make sure that receive or can get the source code And you must show them these terms so they know their rights We protect your rights with two and(2) offer you this license which gives you legal permission to copy
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:217
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 string[] $mimeTypes
List of common image files extensions and MIME-types.
Definition CSSMin.php:44
static isRemoteUrl( $maybeUrl)
Is this CSS rule referencing a remote URL?
Definition CSSMin.php:381
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:540
static isLocalUrl( $maybeUrl)
Is this CSS rule referencing a local URL?
Definition CSSMin.php:394
static getUrlRegex()
Definition CSSMin.php:401
static processUrlMatch(array &$match, $flags=0)
Definition CSSMin.php:436
static remapOne( $file, $query, $local, $remote, $embed)
Remap or embed a CSS URL path.
Definition CSSMin.php:481
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: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:2004
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:1617
$data
Utility to generate mapping file used in mw.Title (phpCharToUpper.json)
const PROTO_RELATIVE
Definition Defines.php:230
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a array("192.168.0.1:11211", 2))
$source
if(!is_readable( $file)) $ext
Definition router.php:48