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 <tparscal@wikimedia.org> |
---|
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 | |
---|
43 | /* Protected Static Members */ |
---|
44 | |
---|
45 | /** @var array List of common image files extensions and mime-types */ |
---|
46 | protected static $mimeTypes = array( |
---|
47 | 'gif' => 'image/gif', |
---|
48 | 'jpe' => 'image/jpeg', |
---|
49 | 'jpeg' => 'image/jpeg', |
---|
50 | 'jpg' => 'image/jpeg', |
---|
51 | 'png' => 'image/png', |
---|
52 | 'tif' => 'image/tiff', |
---|
53 | 'tiff' => 'image/tiff', |
---|
54 | 'xbm' => 'image/x-xbitmap', |
---|
55 | ); |
---|
56 | |
---|
57 | /* Static Methods */ |
---|
58 | |
---|
59 | /** |
---|
60 | * Gets a list of local file paths which are referenced in a CSS style sheet |
---|
61 | * |
---|
62 | * @param $source string CSS data to remap |
---|
63 | * @param $path string File path where the source was read from (optional) |
---|
64 | * @return array List of local file references |
---|
65 | */ |
---|
66 | public static function getLocalFileReferences( $source, $path = null ) { |
---|
67 | $files = array(); |
---|
68 | $rFlags = PREG_OFFSET_CAPTURE | PREG_SET_ORDER; |
---|
69 | if ( preg_match_all( '/' . self::URL_REGEX . '/', $source, $matches, $rFlags ) ) { |
---|
70 | foreach ( $matches as $match ) { |
---|
71 | $file = ( isset( $path ) |
---|
72 | ? rtrim( $path, '/' ) . '/' |
---|
73 | : '' ) . "{$match['file'][0]}"; |
---|
74 | |
---|
75 | // Only proceed if we can access the file |
---|
76 | if ( !is_null( $path ) && file_exists( $file ) ) { |
---|
77 | $files[] = $file; |
---|
78 | } |
---|
79 | } |
---|
80 | } |
---|
81 | return $files; |
---|
82 | } |
---|
83 | |
---|
84 | /** |
---|
85 | * @param $file string |
---|
86 | * @return bool|string |
---|
87 | */ |
---|
88 | protected static function getMimeType( $file ) { |
---|
89 | $realpath = realpath( $file ); |
---|
90 | // Try a couple of different ways to get the mime-type of a file, in order of |
---|
91 | // preference |
---|
92 | if ( |
---|
93 | $realpath |
---|
94 | && function_exists( 'finfo_file' ) |
---|
95 | && function_exists( 'finfo_open' ) |
---|
96 | && defined( 'FILEINFO_MIME_TYPE' ) |
---|
97 | ) { |
---|
98 | // As of PHP 5.3, this is how you get the mime-type of a file; it uses the Fileinfo |
---|
99 | // PECL extension |
---|
100 | return finfo_file( finfo_open( FILEINFO_MIME_TYPE ), $realpath ); |
---|
101 | } elseif ( function_exists( 'mime_content_type' ) ) { |
---|
102 | // Before this was deprecated in PHP 5.3, this was how you got the mime-type of a file |
---|
103 | return mime_content_type( $file ); |
---|
104 | } else { |
---|
105 | // Worst-case scenario has happened, use the file extension to infer the mime-type |
---|
106 | $ext = strtolower( pathinfo( $file, PATHINFO_EXTENSION ) ); |
---|
107 | if ( isset( self::$mimeTypes[$ext] ) ) { |
---|
108 | return self::$mimeTypes[$ext]; |
---|
109 | } |
---|
110 | } |
---|
111 | return false; |
---|
112 | } |
---|
113 | |
---|
114 | /** |
---|
115 | * Remaps CSS URL paths and automatically embeds data URIs for URL rules |
---|
116 | * preceded by an /* @embed * / comment |
---|
117 | * |
---|
118 | * @param $source string CSS data to remap |
---|
119 | * @param $local string File path where the source was read from |
---|
120 | * @param $remote string URL path to the file |
---|
121 | * @param $embedData bool If false, never do any data URI embedding, even if / * @embed * / is found |
---|
122 | * @return string Remapped CSS data |
---|
123 | */ |
---|
124 | public static function remap( $source, $local, $remote, $embedData = true ) { |
---|
125 | $pattern = '/((?P<embed>\s*\/\*\s*\@embed\s*\*\/)(?P<pre>[^\;\}]*))?' . |
---|
126 | self::URL_REGEX . '(?P<post>[^;]*)[\;]?/'; |
---|
127 | $offset = 0; |
---|
128 | while ( preg_match( $pattern, $source, $match, PREG_OFFSET_CAPTURE, $offset ) ) { |
---|
129 | // Skip fully-qualified URLs and data URIs |
---|
130 | $urlScheme = parse_url( $match['file'][0], PHP_URL_SCHEME ); |
---|
131 | if ( $urlScheme ) { |
---|
132 | // Move the offset to the end of the match, leaving it alone |
---|
133 | $offset = $match[0][1] + strlen( $match[0][0] ); |
---|
134 | continue; |
---|
135 | } |
---|
136 | // URLs with absolute paths like /w/index.php need to be expanded |
---|
137 | // to absolute URLs but otherwise left alone |
---|
138 | if ( $match['file'][0] !== '' && $match['file'][0][0] === '/' ) { |
---|
139 | // Replace the file path with an expanded (possibly protocol-relative) URL |
---|
140 | // ...but only if wfExpandUrl() is even available. |
---|
141 | // This will not be the case if we're running outside of MW |
---|
142 | $lengthIncrease = 0; |
---|
143 | if ( function_exists( 'wfExpandUrl' ) ) { |
---|
144 | $expanded = wfExpandUrl( $match['file'][0], PROTO_RELATIVE ); |
---|
145 | $origLength = strlen( $match['file'][0] ); |
---|
146 | $lengthIncrease = strlen( $expanded ) - $origLength; |
---|
147 | $source = substr_replace( $source, $expanded, |
---|
148 | $match['file'][1], $origLength |
---|
149 | ); |
---|
150 | } |
---|
151 | // Move the offset to the end of the match, leaving it alone |
---|
152 | $offset = $match[0][1] + strlen( $match[0][0] ) + $lengthIncrease; |
---|
153 | continue; |
---|
154 | } |
---|
155 | // Shortcuts |
---|
156 | $embed = $match['embed'][0]; |
---|
157 | $pre = $match['pre'][0]; |
---|
158 | $post = $match['post'][0]; |
---|
159 | $query = $match['query'][0]; |
---|
160 | $url = "{$remote}/{$match['file'][0]}"; |
---|
161 | $file = "{$local}/{$match['file'][0]}"; |
---|
162 | // bug 27052 - Guard against double slashes, because foo//../bar |
---|
163 | // apparently resolves to foo/bar on (some?) clients |
---|
164 | $url = preg_replace( '#([^:])//+#', '\1/', $url ); |
---|
165 | $replacement = false; |
---|
166 | if ( $local !== false && file_exists( $file ) ) { |
---|
167 | // Add version parameter as a time-stamp in ISO 8601 format, |
---|
168 | // using Z for the timezone, meaning GMT |
---|
169 | $url .= '?' . gmdate( 'Y-m-d\TH:i:s\Z', round( filemtime( $file ), -2 ) ); |
---|
170 | // Embedding requires a bit of extra processing, so let's skip that if we can |
---|
171 | if ( $embedData && $embed ) { |
---|
172 | $type = self::getMimeType( $file ); |
---|
173 | // Detect when URLs were preceeded with embed tags, and also verify file size is |
---|
174 | // below the limit |
---|
175 | if ( |
---|
176 | $type |
---|
177 | && $match['embed'][1] > 0 |
---|
178 | && filesize( $file ) < self::EMBED_SIZE_LIMIT |
---|
179 | ) { |
---|
180 | // Strip off any trailing = symbols (makes browsers freak out) |
---|
181 | $data = base64_encode( file_get_contents( $file ) ); |
---|
182 | // Build 2 CSS properties; one which uses a base64 encoded data URI in place |
---|
183 | // of the @embed comment to try and retain line-number integrity, and the |
---|
184 | // other with a remapped an versioned URL and an Internet Explorer hack |
---|
185 | // making it ignored in all browsers that support data URIs |
---|
186 | $replacement = "{$pre}url(data:{$type};base64,{$data}){$post};"; |
---|
187 | $replacement .= "{$pre}url({$url}){$post}!ie;"; |
---|
188 | } |
---|
189 | } |
---|
190 | if ( $replacement === false ) { |
---|
191 | // Assume that all paths are relative to $remote, and make them absolute |
---|
192 | $replacement = "{$embed}{$pre}url({$url}){$post};"; |
---|
193 | } |
---|
194 | } elseif ( $local === false ) { |
---|
195 | // Assume that all paths are relative to $remote, and make them absolute |
---|
196 | $replacement = "{$embed}{$pre}url({$url}{$query}){$post};"; |
---|
197 | } |
---|
198 | if ( $replacement !== false ) { |
---|
199 | // Perform replacement on the source |
---|
200 | $source = substr_replace( |
---|
201 | $source, $replacement, $match[0][1], strlen( $match[0][0] ) |
---|
202 | ); |
---|
203 | // Move the offset to the end of the replacement in the source |
---|
204 | $offset = $match[0][1] + strlen( $replacement ); |
---|
205 | continue; |
---|
206 | } |
---|
207 | // Move the offset to the end of the match, leaving it alone |
---|
208 | $offset = $match[0][1] + strlen( $match[0][0] ); |
---|
209 | } |
---|
210 | return $source; |
---|
211 | } |
---|
212 | |
---|
213 | /** |
---|
214 | * Removes whitespace from CSS data |
---|
215 | * |
---|
216 | * @param $css string CSS data to minify |
---|
217 | * @return string Minified CSS data |
---|
218 | */ |
---|
219 | public static function minify( $css ) { |
---|
220 | return trim( |
---|
221 | str_replace( |
---|
222 | array( '; ', ': ', ' {', '{ ', ', ', '} ', ';}' ), |
---|
223 | array( ';', ':', '{', '{', ',', '}', '}' ), |
---|
224 | preg_replace( array( '/\s+/', '/\/\*.*?\*\//s' ), array( ' ', '' ), $css ) |
---|
225 | ) |
---|
226 | ); |
---|
227 | } |
---|
228 | } |
---|