Issues (4122)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

includes/libs/CSSMin.php (4 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * Minification of CSS stylesheets.
4
 *
5
 * Copyright 2010 Wikimedia Foundation
6
 *
7
 * Licensed under the Apache License, Version 2.0 (the "License"); you may
8
 * not use this file except in compliance with the License.
9
 * You may obtain a copy of the License at
10
 *
11
 * 		http://www.apache.org/licenses/LICENSE-2.0
12
 *
13
 * Unless required by applicable law or agreed to in writing, software distributed
14
 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
15
 * OF ANY KIND, either express or implied. See the License for the
16
 * specific language governing permissions and limitations under the License.
17
 *
18
 * @file
19
 * @version 0.1.1 -- 2010-09-11
20
 * @author Trevor Parscal <[email protected]>
21
 * @copyright Copyright 2010 Wikimedia Foundation
22
 * @license http://www.apache.org/licenses/LICENSE-2.0
23
 */
24
25
/**
26
 * Transforms CSS data
27
 *
28
 * This class provides minification, URL remapping, URL extracting, and data-URL embedding.
29
 */
30
class CSSMin {
31
32
	/* Constants */
33
34
	/** @var string Strip marker for comments. **/
35
	const PLACEHOLDER = "\x7fPLACEHOLDER\x7f";
36
37
	/**
38
	 * Internet Explorer data URI length limit. See encodeImageAsDataURI().
39
	 */
40
	const DATA_URI_SIZE_LIMIT = 32768;
41
	const URL_REGEX = 'url\(\s*[\'"]?(?P<file>[^\?\)\'"]*?)(?P<query>\?[^\)\'"]*?|)[\'"]?\s*\)';
42
	const EMBED_REGEX = '\/\*\s*\@embed\s*\*\/';
43
	const COMMENT_REGEX = '\/\*.*?\*\/';
44
45
	/* Protected Static Members */
46
47
	/** @var array List of common image files extensions and MIME-types */
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
62
	/**
63
	 * Get a list of local files referenced in a stylesheet (includes non-existent files).
64
	 *
65
	 * @param string $source CSS stylesheet source to process
66
	 * @param string $path File path where the source was read from
67
	 * @return array List of local file references
68
	 */
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::URL_REGEX . '/', $stripped, $matches, $rFlags ) ) {
76
			foreach ( $matches as $match ) {
77
				$url = $match['file'][0];
78
79
				// Skip fully-qualified and protocol-relative URLs and data URIs
80
				if ( substr( $url, 0, 2 ) === '//' || parse_url( $url, PHP_URL_SCHEME ) ) {
81
					break;
82
				}
83
84
				$files[] = $path . $url;
85
			}
86
		}
87
		return $files;
88
	}
89
90
	/**
91
	 * Encode an image file as a data URI.
92
	 *
93
	 * If the image file has a suitable MIME type and size, encode it as a data URI, base64-encoded
94
	 * for binary files or just percent-encoded otherwise. Return false if the image type is
95
	 * unfamiliar or file exceeds the size limit.
96
	 *
97
	 * @param string $file Image file to encode.
98
	 * @param string|null $type File's MIME type or null. If null, CSSMin will
99
	 *     try to autodetect the type.
100
	 * @param bool $ie8Compat By default, a data URI will only be produced if it can be made short
101
	 *     enough to fit in Internet Explorer 8 (and earlier) URI length limit (32,768 bytes). Pass
102
	 *     `false` to remove this limitation.
103
	 * @return string|bool Image contents encoded as a data URI or false.
104
	 */
105
	public static function encodeImageAsDataURI( $file, $type = null, $ie8Compat = true ) {
106
		// Fast-fail for files that definitely exceed the maximum data URI length
107
		if ( $ie8Compat && filesize( $file ) >= self::DATA_URI_SIZE_LIMIT ) {
108
			return false;
109
		}
110
111
		if ( $type === null ) {
112
			$type = self::getMimeType( $file );
113
		}
114
		if ( !$type ) {
115
			return false;
116
		}
117
118
		return self::encodeStringAsDataURI( file_get_contents( $file ), $type, $ie8Compat );
0 ignored issues
show
It seems like $type can also be of type boolean; however, CSSMin::encodeStringAsDataURI() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
119
	}
120
121
	/**
122
	 * Encode file contents as a data URI with chosen MIME type.
123
	 *
124
	 * The URI will be base64-encoded for binary files or just percent-encoded otherwise.
125
	 *
126
	 * @since 1.25
127
	 *
128
	 * @param string $contents File contents to encode.
129
	 * @param string $type File's MIME type.
130
	 * @param bool $ie8Compat See encodeImageAsDataURI().
131
	 * @return string|bool Image contents encoded as a data URI or false.
132
	 */
133
	public static function encodeStringAsDataURI( $contents, $type, $ie8Compat = true ) {
134
		// Try #1: Non-encoded data URI
135
		// The regular expression matches ASCII whitespace and printable characters.
136
		if ( preg_match( '/^[\r\n\t\x20-\x7e]+$/', $contents ) ) {
137
			// Do not base64-encode non-binary files (sane SVGs).
138
			// (This often produces longer URLs, but they compress better, yielding a net smaller size.)
139
			$uri = 'data:' . $type . ',' . rawurlencode( $contents );
140
			if ( !$ie8Compat || strlen( $uri ) < self::DATA_URI_SIZE_LIMIT ) {
141
				return $uri;
142
			}
143
		}
144
145
		// Try #2: Encoded data URI
146
		$uri = 'data:' . $type . ';base64,' . base64_encode( $contents );
147
		if ( !$ie8Compat || strlen( $uri ) < self::DATA_URI_SIZE_LIMIT ) {
148
			return $uri;
149
		}
150
151
		// A data URI couldn't be produced
152
		return false;
153
	}
154
155
	/**
156
	 * Serialize a string (escape and quote) for use as a CSS string value.
157
	 * http://www.w3.org/TR/2013/WD-cssom-20131205/#serialize-a-string
158
	 *
159
	 * @param string $value
160
	 * @return string
161
	 * @throws Exception
162
	 */
163
	public static function serializeStringValue( $value ) {
164
		if ( strstr( $value, "\0" ) ) {
165
			throw new Exception( "Invalid character in CSS string" );
166
		}
167
		$value = strtr( $value, [ '\\' => '\\\\', '"' => '\\"' ] );
168
		$value = preg_replace_callback( '/[\x01-\x1f\x7f-\x9f]/', function ( $match ) {
169
			return '\\' . base_convert( ord( $match[0] ), 10, 16 ) . ' ';
170
		}, $value );
171
		return '"' . $value . '"';
172
	}
173
174
	/**
175
	 * @param string $file
176
	 * @return bool|string
177
	 */
178
	public static function getMimeType( $file ) {
179
		$realpath = realpath( $file );
180
		if (
181
			$realpath
182
			&& function_exists( 'finfo_file' )
183
			&& function_exists( 'finfo_open' )
184
			&& defined( 'FILEINFO_MIME_TYPE' )
185
		) {
186
			return finfo_file( finfo_open( FILEINFO_MIME_TYPE ), $realpath );
187
		}
188
189
		// Infer the MIME-type from the file extension
190
		$ext = strtolower( pathinfo( $file, PATHINFO_EXTENSION ) );
191
		if ( isset( self::$mimeTypes[$ext] ) ) {
192
			return self::$mimeTypes[$ext];
193
		}
194
195
		return false;
196
	}
197
198
	/**
199
	 * Build a CSS 'url()' value for the given URL, quoting parentheses (and other funny characters)
200
	 * and escaping quotes as necessary.
201
	 *
202
	 * See http://www.w3.org/TR/css-syntax-3/#consume-a-url-token
203
	 *
204
	 * @param string $url URL to process
205
	 * @return string 'url()' value, usually just `"url($url)"`, quoted/escaped if necessary
206
	 */
207
	public static function buildUrlValue( $url ) {
208
		// The list below has been crafted to match URLs such as:
209
		//   scheme://user@domain:port/~user/fi%20le.png?query=yes&really=y+s
210
		//   data:image/png;base64,R0lGODlh/+==
211
		if ( preg_match( '!^[\w\d:@/~.%+;,?&=-]+$!', $url ) ) {
212
			return "url($url)";
213
		} else {
214
			return 'url("' . strtr( $url, [ '\\' => '\\\\', '"' => '\\"' ] ) . '")';
215
		}
216
	}
217
218
	/**
219
	 * Remaps CSS URL paths and automatically embeds data URIs for CSS rules
220
	 * or url() values preceded by an / * @embed * / comment.
221
	 *
222
	 * @param string $source CSS data to remap
223
	 * @param string $local File path where the source was read from
224
	 * @param string $remote URL path to the file
225
	 * @param bool $embedData If false, never do any data URI embedding,
226
	 *   even if / * @embed * / is found.
227
	 * @return string Remapped CSS data
228
	 */
229
	public static function remap( $source, $local, $remote, $embedData = true ) {
230
		// High-level overview:
231
		// * For each CSS rule in $source that includes at least one url() value:
232
		//   * Check for an @embed comment at the start indicating that all URIs should be embedded
233
		//   * For each url() value:
234
		//     * Check for an @embed comment directly preceding the value
235
		//     * If either @embed comment exists:
236
		//       * Embedding the URL as data: URI, if it's possible / allowed
237
		//       * Otherwise remap the URL to work in generated stylesheets
238
239
		// Guard against trailing slashes, because "some/remote/../foo.png"
240
		// resolves to "some/remote/foo.png" on (some?) clients (bug 27052).
241
		if ( substr( $remote, -1 ) == '/' ) {
242
			$remote = substr( $remote, 0, -1 );
243
		}
244
245
		// Disallow U+007F DELETE, which is illegal anyway, and which
246
		// we use for comment placeholders.
247
		$source = str_replace( "\x7f", "?", $source );
248
249
		// Replace all comments by a placeholder so they will not interfere with the remapping.
250
		// Warning: This will also catch on anything looking like the start of a comment between
251
		// quotation marks (e.g. "foo /* bar").
252
		$comments = [];
253
254
		$pattern = '/(?!' . CSSMin::EMBED_REGEX . ')(' . CSSMin::COMMENT_REGEX . ')/s';
255
256
		$source = preg_replace_callback(
257
			$pattern,
258
			function ( $match ) use ( &$comments ) {
259
				$comments[] = $match[ 0 ];
260
				return CSSMin::PLACEHOLDER . ( count( $comments ) - 1 ) . 'x';
261
			},
262
			$source
263
		);
264
265
		// Note: This will not correctly handle cases where ';', '{' or '}'
266
		// appears in the rule itself, e.g. in a quoted string. You are advised
267
		// not to use such characters in file names. We also match start/end of
268
		// the string to be consistent in edge-cases ('@import url(…)').
269
		$pattern = '/(?:^|[;{])\K[^;{}]*' . CSSMin::URL_REGEX . '[^;}]*(?=[;}]|$)/';
270
271
		$source = preg_replace_callback(
272
			$pattern,
273
			function ( $matchOuter ) use ( $local, $remote, $embedData ) {
274
				$rule = $matchOuter[0];
275
276
				// Check for global @embed comment and remove it. Allow other comments to be present
277
				// before @embed (they have been replaced with placeholders at this point).
278
				$embedAll = false;
279
				$rule = preg_replace(
280
					'/^((?:\s+|' .
281
						CSSMin::PLACEHOLDER .
282
						'(\d+)x)*)' .
283
						CSSMin::EMBED_REGEX .
284
						'\s*/',
285
					'$1',
286
					$rule,
287
					1,
288
					$embedAll
289
				);
290
291
				// Build two versions of current rule: with remapped URLs
292
				// and with embedded data: URIs (where possible).
293
				$pattern = '/(?P<embed>' . CSSMin::EMBED_REGEX . '\s*|)' . CSSMin::URL_REGEX . '/';
294
295
				$ruleWithRemapped = preg_replace_callback(
296
					$pattern,
297
					function ( $match ) use ( $local, $remote ) {
298
						$remapped = CSSMin::remapOne( $match['file'], $match['query'], $local, $remote, false );
299
300
						return CSSMin::buildUrlValue( $remapped );
301
					},
302
					$rule
303
				);
304
305
				if ( $embedData ) {
306
					// Remember the occurring MIME types to avoid fallbacks when embedding some files.
307
					$mimeTypes = [];
308
309
					$ruleWithEmbedded = preg_replace_callback(
310
						$pattern,
311
						function ( $match ) use ( $embedAll, $local, $remote, &$mimeTypes ) {
312
							$embed = $embedAll || $match['embed'];
0 ignored issues
show
Bug Best Practice introduced by
The expression $embedAll of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
313
							$embedded = CSSMin::remapOne(
314
								$match['file'],
315
								$match['query'],
316
								$local,
317
								$remote,
318
								$embed
319
							);
320
321
							$url = $match['file'] . $match['query'];
322
							$file = "{$local}/{$match['file']}";
323
							if (
324
								!self::isRemoteUrl( $url ) && !self::isLocalUrl( $url )
325
								&& file_exists( $file )
326
							) {
327
								$mimeTypes[ CSSMin::getMimeType( $file ) ] = true;
328
							}
329
330
							return CSSMin::buildUrlValue( $embedded );
331
						},
332
						$rule
333
					);
334
335
					// Are all referenced images SVGs?
336
					$needsEmbedFallback = $mimeTypes !== [ 'image/svg+xml' => true ];
337
				}
338
339
				if ( !$embedData || $ruleWithEmbedded === $ruleWithRemapped ) {
340
					// We're not embedding anything, or we tried to but the file is not embeddable
341
					return $ruleWithRemapped;
342
				} elseif ( $embedData && $needsEmbedFallback ) {
0 ignored issues
show
The variable $needsEmbedFallback does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
343
					// Build 2 CSS properties; one which uses a data URI in place of the @embed comment, and
344
					// the other with a remapped and versioned URL with an Internet Explorer 6 and 7 hack
345
					// making it ignored in all browsers that support data URIs
346
					return "$ruleWithEmbedded;$ruleWithRemapped!ie";
0 ignored issues
show
The variable $ruleWithEmbedded does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
347
				} else {
348
					// Look ma, no fallbacks! This is for files which IE 6 and 7 don't support anyway: SVG.
349
					return $ruleWithEmbedded;
350
				}
351
			}, $source );
352
353
		// Re-insert comments
354
		$pattern = '/' . CSSMin::PLACEHOLDER . '(\d+)x/';
355
		$source = preg_replace_callback( $pattern, function( $match ) use ( &$comments ) {
356
			return $comments[ $match[1] ];
357
		}, $source );
358
359
		return $source;
360
	}
361
362
	/**
363
	 * Is this CSS rule referencing a remote URL?
364
	 *
365
	 * @param string $maybeUrl
366
	 * @return bool
367
	 */
368
	protected static function isRemoteUrl( $maybeUrl ) {
369
		if ( substr( $maybeUrl, 0, 2 ) === '//' || parse_url( $maybeUrl, PHP_URL_SCHEME ) ) {
370
			return true;
371
		}
372
		return false;
373
	}
374
375
	/**
376
	 * Is this CSS rule referencing a local URL?
377
	 *
378
	 * @param string $maybeUrl
379
	 * @return bool
380
	 */
381
	protected static function isLocalUrl( $maybeUrl ) {
382
		if ( $maybeUrl !== '' && $maybeUrl[0] === '/' && !self::isRemoteUrl( $maybeUrl ) ) {
383
			return true;
384
		}
385
		return false;
386
	}
387
388
	/**
389
	 * Remap or embed a CSS URL path.
390
	 *
391
	 * @param string $file URL to remap/embed
392
	 * @param string $query
393
	 * @param string $local File path where the source was read from
394
	 * @param string $remote URL path to the file
395
	 * @param bool $embed Whether to do any data URI embedding
396
	 * @return string Remapped/embedded URL data
397
	 */
398
	public static function remapOne( $file, $query, $local, $remote, $embed ) {
399
		// The full URL possibly with query, as passed to the 'url()' value in CSS
400
		$url = $file . $query;
401
402
		// Expand local URLs with absolute paths like /w/index.php to possibly protocol-relative URL, if
403
		// wfExpandUrl() is available. (This will not be the case if we're running outside of MW.)
404
		if ( self::isLocalUrl( $url ) && function_exists( 'wfExpandUrl' ) ) {
405
			return wfExpandUrl( $url, PROTO_RELATIVE );
406
		}
407
408
		// Pass thru fully-qualified and protocol-relative URLs and data URIs, as well as local URLs if
409
		// we can't expand them.
410
		if ( self::isRemoteUrl( $url ) || self::isLocalUrl( $url ) ) {
411
			return $url;
412
		}
413
414
		if ( $local === false ) {
415
			// Assume that all paths are relative to $remote, and make them absolute
416
			$url = $remote . '/' . $url;
417
		} else {
418
			// We drop the query part here and instead make the path relative to $remote
419
			$url = "{$remote}/{$file}";
420
			// Path to the actual file on the filesystem
421
			$localFile = "{$local}/{$file}";
422
			if ( file_exists( $localFile ) ) {
423
				if ( $embed ) {
424
					$data = self::encodeImageAsDataURI( $localFile );
425
					if ( $data !== false ) {
426
						return $data;
427
					}
428
				}
429
				if ( method_exists( 'OutputPage', 'transformFilePath' ) ) {
430
					$url = OutputPage::transformFilePath( $remote, $local, $file );
431
				} else {
432
					// Add version parameter as the first five hex digits
433
					// of the MD5 hash of the file's contents.
434
					$url .= '?' . substr( md5_file( $localFile ), 0, 5 );
435
				}
436
			}
437
			// If any of these conditions failed (file missing, we don't want to embed it
438
			// or it's not embeddable), return the URL (possibly with ?timestamp part)
439
		}
440
		if ( function_exists( 'wfRemoveDotSegments' ) ) {
441
			$url = wfRemoveDotSegments( $url );
442
		}
443
		return $url;
444
	}
445
446
	/**
447
	 * Removes whitespace from CSS data
448
	 *
449
	 * @param string $css CSS data to minify
450
	 * @return string Minified CSS data
451
	 */
452
	public static function minify( $css ) {
453
		return trim(
454
			str_replace(
455
				[ '; ', ': ', ' {', '{ ', ', ', '} ', ';}' ],
456
				[ ';', ':', '{', '{', ',', '}', '}' ],
457
				preg_replace( [ '/\s+/', '/\/\*.*?\*\//s' ], [ ' ', '' ], $css )
458
			)
459
		);
460
	}
461
}
462