[ Index ] |
PHP Cross Reference of MediaWiki-1.24.0 |
[Summary view] [Print] [Text view]
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 /** 35 * Maximum file size to still qualify for in-line embedding as a data-URI 36 * 37 * 24,576 is used because Internet Explorer has a 32,768 byte limit for data URIs, 38 * which when base64 encoded will result in a 1/3 increase in size. 39 */ 40 const EMBED_SIZE_LIMIT = 24576; 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 = array( 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 * Gets a list of local file paths which are referenced in a CSS style sheet 64 * 65 * This function will always return an empty array if the second parameter is not given or null 66 * for backwards-compatibility. 67 * 68 * @param string $source CSS data to remap 69 * @param string $path File path where the source was read from (optional) 70 * @return array List of local file references 71 */ 72 public static function getLocalFileReferences( $source, $path = null ) { 73 if ( $path === null ) { 74 return array(); 75 } 76 77 $path = rtrim( $path, '/' ) . '/'; 78 $files = array(); 79 80 $rFlags = PREG_OFFSET_CAPTURE | PREG_SET_ORDER; 81 if ( preg_match_all( '/' . self::URL_REGEX . '/', $source, $matches, $rFlags ) ) { 82 foreach ( $matches as $match ) { 83 $url = $match['file'][0]; 84 85 // Skip fully-qualified and protocol-relative URLs and data URIs 86 if ( substr( $url, 0, 2 ) === '//' || parse_url( $url, PHP_URL_SCHEME ) ) { 87 break; 88 } 89 90 $file = $path . $url; 91 // Skip non-existent files 92 if ( file_exists( $file ) ) { 93 break; 94 } 95 96 $files[] = $file; 97 } 98 } 99 return $files; 100 } 101 102 /** 103 * Encode an image file as a base64 data URI. 104 * If the image file has a suitable MIME type and size, encode it as a 105 * base64 data URI. Return false if the image type is unfamiliar or exceeds 106 * the size limit. 107 * 108 * @param string $file Image file to encode. 109 * @param string|null $type File's MIME type or null. If null, CSSMin will 110 * try to autodetect the type. 111 * @param int|bool $sizeLimit If the size of the target file is greater than 112 * this value, decline to encode the image file and return false 113 * instead. If $sizeLimit is false, no limit is enforced. 114 * @return string|bool: Image contents encoded as a data URI or false. 115 */ 116 public static function encodeImageAsDataURI( $file, $type = null, 117 $sizeLimit = self::EMBED_SIZE_LIMIT 118 ) { 119 if ( $sizeLimit !== false && filesize( $file ) >= $sizeLimit ) { 120 return false; 121 } 122 if ( $type === null ) { 123 $type = self::getMimeType( $file ); 124 } 125 if ( !$type ) { 126 return false; 127 } 128 $data = base64_encode( file_get_contents( $file ) ); 129 return 'data:' . $type . ';base64,' . $data; 130 } 131 132 /** 133 * @param $file string 134 * @return bool|string 135 */ 136 public static function getMimeType( $file ) { 137 $realpath = realpath( $file ); 138 if ( 139 $realpath 140 && function_exists( 'finfo_file' ) 141 && function_exists( 'finfo_open' ) 142 && defined( 'FILEINFO_MIME_TYPE' ) 143 ) { 144 return finfo_file( finfo_open( FILEINFO_MIME_TYPE ), $realpath ); 145 } 146 147 // Infer the MIME-type from the file extension 148 $ext = strtolower( pathinfo( $file, PATHINFO_EXTENSION ) ); 149 if ( isset( self::$mimeTypes[$ext] ) ) { 150 return self::$mimeTypes[$ext]; 151 } 152 153 return false; 154 } 155 156 /** 157 * Build a CSS 'url()' value for the given URL, quoting parentheses (and other funny characters) 158 * and escaping quotes as necessary. 159 * 160 * See http://www.w3.org/TR/css-syntax-3/#consume-a-url-token 161 * 162 * @param string $url URL to process 163 * @return string 'url()' value, usually just `"url($url)"`, quoted/escaped if necessary 164 */ 165 public static function buildUrlValue( $url ) { 166 // The list below has been crafted to match URLs such as: 167 // scheme://user@domain:port/~user/fi%20le.png?query=yes&really=y+s 168 // data:image/png;base64,R0lGODlh/+== 169 if ( preg_match( '!^[\w\d:@/~.%+;,?&=-]+$!', $url ) ) { 170 return "url($url)"; 171 } else { 172 return 'url("' . strtr( $url, array( '\\' => '\\\\', '"' => '\\"' ) ) . '")'; 173 } 174 } 175 176 /** 177 * Remaps CSS URL paths and automatically embeds data URIs for CSS rules 178 * or url() values preceded by an / * @embed * / comment. 179 * 180 * @param string $source CSS data to remap 181 * @param string $local File path where the source was read from 182 * @param string $remote URL path to the file 183 * @param bool $embedData If false, never do any data URI embedding, 184 * even if / * @embed * / is found. 185 * @return string Remapped CSS data 186 */ 187 public static function remap( $source, $local, $remote, $embedData = true ) { 188 // High-level overview: 189 // * For each CSS rule in $source that includes at least one url() value: 190 // * Check for an @embed comment at the start indicating that all URIs should be embedded 191 // * For each url() value: 192 // * Check for an @embed comment directly preceding the value 193 // * If either @embed comment exists: 194 // * Embedding the URL as data: URI, if it's possible / allowed 195 // * Otherwise remap the URL to work in generated stylesheets 196 197 // Guard against trailing slashes, because "some/remote/../foo.png" 198 // resolves to "some/remote/foo.png" on (some?) clients (bug 27052). 199 if ( substr( $remote, -1 ) == '/' ) { 200 $remote = substr( $remote, 0, -1 ); 201 } 202 203 // Replace all comments by a placeholder so they will not interfere with the remapping. 204 // Warning: This will also catch on anything looking like the start of a comment between 205 // quotation marks (e.g. "foo /* bar"). 206 $comments = array(); 207 $placeholder = uniqid( '', true ); 208 209 $pattern = '/(?!' . CSSMin::EMBED_REGEX . ')(' . CSSMin::COMMENT_REGEX . ')/s'; 210 211 $source = preg_replace_callback( 212 $pattern, 213 function ( $match ) use ( &$comments, $placeholder ) { 214 $comments[] = $match[ 0 ]; 215 return $placeholder . ( count( $comments ) - 1 ) . 'x'; 216 }, 217 $source 218 ); 219 220 // Note: This will not correctly handle cases where ';', '{' or '}' 221 // appears in the rule itself, e.g. in a quoted string. You are advised 222 // not to use such characters in file names. We also match start/end of 223 // the string to be consistent in edge-cases ('@import url(…)'). 224 $pattern = '/(?:^|[;{])\K[^;{}]*' . CSSMin::URL_REGEX . '[^;}]*(?=[;}]|$)/'; 225 226 $source = preg_replace_callback( 227 $pattern, 228 function ( $matchOuter ) use ( $local, $remote, $embedData, $placeholder ) { 229 $rule = $matchOuter[0]; 230 231 // Check for global @embed comment and remove it. Allow other comments to be present 232 // before @embed (they have been replaced with placeholders at this point). 233 $embedAll = false; 234 $rule = preg_replace( '/^((?:\s+|' . $placeholder . '(\d+)x)*)' . CSSMin::EMBED_REGEX . '\s*/', '$1', $rule, 1, $embedAll ); 235 236 // Build two versions of current rule: with remapped URLs 237 // and with embedded data: URIs (where possible). 238 $pattern = '/(?P<embed>' . CSSMin::EMBED_REGEX . '\s*|)' . CSSMin::URL_REGEX . '/'; 239 240 $ruleWithRemapped = preg_replace_callback( 241 $pattern, 242 function ( $match ) use ( $local, $remote ) { 243 $remapped = CSSMin::remapOne( $match['file'], $match['query'], $local, $remote, false ); 244 245 return CSSMin::buildUrlValue( $remapped ); 246 }, 247 $rule 248 ); 249 250 if ( $embedData ) { 251 $ruleWithEmbedded = preg_replace_callback( 252 $pattern, 253 function ( $match ) use ( $embedAll, $local, $remote ) { 254 $embed = $embedAll || $match['embed']; 255 $embedded = CSSMin::remapOne( 256 $match['file'], 257 $match['query'], 258 $local, 259 $remote, 260 $embed 261 ); 262 263 return CSSMin::buildUrlValue( $embedded ); 264 }, 265 $rule 266 ); 267 } 268 269 if ( $embedData && $ruleWithEmbedded !== $ruleWithRemapped ) { 270 // Build 2 CSS properties; one which uses a base64 encoded data URI in place 271 // of the @embed comment to try and retain line-number integrity, and the 272 // other with a remapped an versioned URL and an Internet Explorer hack 273 // making it ignored in all browsers that support data URIs 274 return "$ruleWithEmbedded;$ruleWithRemapped!ie"; 275 } else { 276 // No reason to repeat twice 277 return $ruleWithRemapped; 278 } 279 }, $source ); 280 281 // Re-insert comments 282 $pattern = '/' . $placeholder . '(\d+)x/'; 283 $source = preg_replace_callback( $pattern, function( $match ) use ( &$comments ) { 284 return $comments[ $match[1] ]; 285 }, $source ); 286 287 return $source; 288 289 } 290 291 /** 292 * Remap or embed a CSS URL path. 293 * 294 * @param string $file URL to remap/embed 295 * @param string $query 296 * @param string $local File path where the source was read from 297 * @param string $remote URL path to the file 298 * @param bool $embed Whether to do any data URI embedding 299 * @return string Remapped/embedded URL data 300 */ 301 public static function remapOne( $file, $query, $local, $remote, $embed ) { 302 // The full URL possibly with query, as passed to the 'url()' value in CSS 303 $url = $file . $query; 304 305 // Skip fully-qualified and protocol-relative URLs and data URIs 306 if ( substr( $url, 0, 2 ) === '//' || parse_url( $url, PHP_URL_SCHEME ) ) { 307 return $url; 308 } 309 310 // URLs with absolute paths like /w/index.php need to be expanded 311 // to absolute URLs but otherwise left alone 312 if ( $url !== '' && $url[0] === '/' ) { 313 // Replace the file path with an expanded (possibly protocol-relative) URL 314 // ...but only if wfExpandUrl() is even available. 315 // This will not be the case if we're running outside of MW 316 if ( function_exists( 'wfExpandUrl' ) ) { 317 return wfExpandUrl( $url, PROTO_RELATIVE ); 318 } else { 319 return $url; 320 } 321 } 322 323 if ( $local === false ) { 324 // Assume that all paths are relative to $remote, and make them absolute 325 return $remote . '/' . $url; 326 } else { 327 // We drop the query part here and instead make the path relative to $remote 328 $url = "{$remote}/{$file}"; 329 // Path to the actual file on the filesystem 330 $localFile = "{$local}/{$file}"; 331 if ( file_exists( $localFile ) ) { 332 // Add version parameter as a time-stamp in ISO 8601 format, 333 // using Z for the timezone, meaning GMT 334 $url .= '?' . gmdate( 'Y-m-d\TH:i:s\Z', round( filemtime( $localFile ), -2 ) ); 335 if ( $embed ) { 336 $data = self::encodeImageAsDataURI( $localFile ); 337 if ( $data !== false ) { 338 return $data; 339 } 340 } 341 } 342 // If any of these conditions failed (file missing, we don't want to embed it 343 // or it's not embeddable), return the URL (possibly with ?timestamp part) 344 return $url; 345 } 346 } 347 348 /** 349 * Removes whitespace from CSS data 350 * 351 * @param string $css CSS data to minify 352 * @return string Minified CSS data 353 */ 354 public static function minify( $css ) { 355 return trim( 356 str_replace( 357 array( '; ', ': ', ' {', '{ ', ', ', '} ', ';}' ), 358 array( ';', ':', '{', '{', ',', '}', '}' ), 359 preg_replace( array( '/\s+/', '/\/\*.*?\*\//s' ), array( ' ', '' ), $css ) 360 ) 361 ); 362 } 363 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 14:03:12 2014 | Cross-referenced by PHPXref 0.7.1 |