1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* For the full copyright and license information, please view the LICENSE |
4
|
|
|
* file that was distributed with this source code. |
5
|
|
|
* 08/08/12 15:22 |
6
|
|
|
*/ |
7
|
|
|
|
8
|
|
|
namespace CssEmbed; |
9
|
|
|
|
10
|
|
|
/** |
11
|
|
|
* CssEmbed |
12
|
|
|
* |
13
|
|
|
* @author Pierre Tachoire <[email protected]> |
14
|
|
|
*/ |
15
|
|
|
class CssEmbed |
16
|
|
|
{ |
17
|
|
|
|
18
|
|
|
const SEARCH_PATTERN = "%url\\(['\" ]*((?!data:|//)[^'\"#\?: ]+)['\" ]*\\)%U"; |
19
|
|
|
const URI_PATTERN = "url(data:%s;base64,%s)"; |
20
|
|
|
|
21
|
|
|
const HTTP_SEARCH_PATTERN = "%url\\(['\" ]*((?!data:)[^'\" ]+)['\" ]*\\)%U"; |
22
|
|
|
const HTTP_ENABLED = 1; |
23
|
|
|
const HTTP_DEFAULT_HTTPS = 2; |
24
|
|
|
const HTTP_URL_ON_ERROR = 4; |
25
|
|
|
const HTTP_EMBED_FONTS = 8; |
26
|
|
|
const HTTP_EMBED_SVG = 16; |
27
|
|
|
const HTTP_EMBED_SCHEME = 32; |
28
|
|
|
const HTTP_EMBED_URL_ONLY = 64; |
29
|
|
|
|
30
|
|
|
protected $root_dir; |
31
|
|
|
|
32
|
|
|
/** @var integer the http flags */ |
33
|
|
|
protected $http_flags = 0; |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* @param $root_dir |
37
|
|
|
*/ |
38
|
|
|
public function setRootDir($root_dir) |
39
|
|
|
{ |
40
|
|
|
$this->root_dir = $root_dir; |
41
|
|
|
} |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* Allow assets referenced over HTTP to be embedded, or assets in a css |
45
|
|
|
* file loaded over HTTP. Flags: |
46
|
|
|
* |
47
|
|
|
* - CssEmbed::HTTP_ENABLED: enable embedding over http; |
48
|
|
|
* - CssEmbed::HTTP_DEFAULT_HTTPS: for URLs with no scheme, use https to |
49
|
|
|
* instead of http |
50
|
|
|
* - CssEmbed::HTTP_URL_ON_ERROR: if there is an error fetching a remote |
51
|
|
|
* asset, embed the URL instead of throwing an exception |
52
|
|
|
* - CssEmbed::HTTP_EMBED_FONTS: embedding fonts will usually break them |
53
|
|
|
* in most browsers. Enable this flag to force the embed. WARNING: |
54
|
|
|
* this flag is currently not unit tested, but seems to work. |
55
|
|
|
* - CssEmbed::HTTP_EMBED_SVG: SVG is often used as a font face; however |
56
|
|
|
* including these in a stylesheet will cause it to bloat for browsers |
57
|
|
|
* that don't use it. By default SVGs will be replaced with the URL |
58
|
|
|
* to the asset; set this flag to force the embed of SVG files. |
59
|
|
|
* - CssEmbed::HTTP_EMBED_SCHEME: By default, assets that are converted |
60
|
|
|
* to URLs instead of data urls have no scheme (eg, "//example.com"). |
61
|
|
|
* This is better for stylesheets that are maybe served over http or |
62
|
|
|
* https, but it will break stylesheets served from a local HTML file. |
63
|
|
|
* Set this option to force the schema (eg, "http://example.com"). |
64
|
|
|
* - CssEmbed::HTTP_EMBED_URL_ONLY: do not convert assets to data URLs, |
65
|
|
|
* only the fully qualified URL. |
66
|
|
|
* |
67
|
|
|
* |
68
|
|
|
* @param integer $flags |
69
|
|
|
* |
70
|
|
|
* @return void |
71
|
|
|
*/ |
72
|
|
|
public function enableHttp($flags = null) |
73
|
|
|
{ |
74
|
|
|
if (is_null($flags)) { |
75
|
|
|
$flags = CssEmbed::HTTP_ENABLED|CssEmbed::HTTP_URL_ON_ERROR; |
76
|
|
|
|
77
|
|
|
} |
78
|
|
|
$this->http_flags = (int) $flags; |
79
|
|
|
} |
80
|
|
|
|
81
|
|
|
/** |
82
|
|
|
* Set a single http option flag. See `enableHttp` for a description of |
83
|
|
|
* available flags. |
84
|
|
|
* |
85
|
|
|
* @param integer $flag |
86
|
|
|
* |
87
|
|
|
* @return void |
88
|
|
|
*/ |
89
|
|
|
public function setHttpFlag($flag) |
90
|
|
|
{ |
91
|
|
|
$this->http_flags |= $flag; |
92
|
|
|
} |
93
|
|
|
|
94
|
|
|
/** |
95
|
|
|
* unset a single http option flag. See `enableHttp` for a description of |
96
|
|
|
* available flags. |
97
|
|
|
* |
98
|
|
|
* @param integer $flag |
99
|
|
|
* |
100
|
|
|
* @return void |
101
|
|
|
*/ |
102
|
|
|
public function unsetHttpFlag($flag) |
103
|
|
|
{ |
104
|
|
|
$this->http_flags = $this->http_flags & (~ $flag); |
105
|
|
|
} |
106
|
|
|
|
107
|
|
|
/** |
108
|
|
|
* @param $css_file |
109
|
|
|
* @return null|string |
110
|
|
|
* @throws \InvalidArgumentException |
111
|
|
|
*/ |
112
|
|
|
public function embedCss($css_file) |
113
|
|
|
{ |
114
|
|
|
$this->setRootDir(dirname($css_file)); |
115
|
|
|
$return = null; |
116
|
|
|
$handle = fopen($css_file, "r"); |
117
|
|
|
if ($handle === false) { |
118
|
|
|
throw new \InvalidArgumentException(sprintf('Cannot read file %s', $css_file)); |
119
|
|
|
} |
120
|
|
|
while (($line = fgets($handle)) !== false) { |
121
|
|
|
$return .= $this->embedString($line); |
122
|
|
|
} |
123
|
|
|
fclose($handle); |
124
|
|
|
|
125
|
|
|
return $return; |
126
|
|
|
} |
127
|
|
|
|
128
|
|
|
/** |
129
|
|
|
* @param $content |
130
|
|
|
* @return mixed |
131
|
|
|
*/ |
132
|
|
|
public function embedString($content) |
133
|
|
|
{ |
134
|
|
|
if ($this->http_flags & self::HTTP_ENABLED) { |
135
|
|
|
return preg_replace_callback( |
136
|
|
|
self::HTTP_SEARCH_PATTERN, |
137
|
|
|
array($this, 'httpEnabledReplace'), |
138
|
|
|
$content |
139
|
|
|
); |
140
|
|
|
} |
141
|
|
|
return preg_replace_callback(self::SEARCH_PATTERN, array($this, 'replace'), $content); |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
|
145
|
|
|
/** |
146
|
|
|
* @param $matches |
147
|
|
|
* @return string |
148
|
|
|
*/ |
149
|
|
|
protected function replace($matches) |
150
|
|
|
{ |
151
|
|
|
return $this->embedFile($this->root_dir . DIRECTORY_SEPARATOR . $matches[1]); |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
/** |
155
|
|
|
* @param $file |
156
|
|
|
* @return string |
157
|
|
|
*/ |
158
|
|
|
protected function embedFile($file) |
159
|
|
|
{ |
160
|
|
|
return sprintf(self::URI_PATTERN, $this->mimeType($file), $this->base64($file)); |
161
|
|
|
} |
162
|
|
|
|
163
|
|
|
/** |
164
|
|
|
* @param $file |
165
|
|
|
* @return string |
166
|
|
|
*/ |
167
|
|
|
protected function mimeType($file) |
168
|
|
|
{ |
169
|
|
|
if (function_exists('mime_content_type')) { |
170
|
|
|
return mime_content_type($file); |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
if ($info = @getimagesize($file)) { |
174
|
|
|
return($info['mime']); |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
return 'application/octet-stream'; |
178
|
|
|
} |
179
|
|
|
|
180
|
|
|
/** |
181
|
|
|
* @param $file |
182
|
|
|
* @return string |
183
|
|
|
* @throws \InvalidArgumentException |
184
|
|
|
*/ |
185
|
|
|
protected function base64($file) |
186
|
|
|
{ |
187
|
|
|
if (is_file($file) === false || is_readable($file) === false) { |
188
|
|
|
throw new \InvalidArgumentException(sprintf('Cannot read file %s', $file)); |
189
|
|
|
} |
190
|
|
|
|
191
|
|
|
return base64_encode(file_get_contents($file)); |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
/** |
195
|
|
|
* @param $matches |
196
|
|
|
* @return string |
197
|
|
|
*/ |
198
|
|
|
protected function httpEnabledReplace($matches) |
199
|
|
|
{ |
200
|
|
|
// fall back to default functionality for non-remote assets |
201
|
|
|
if (!$this->isHttpAsset($matches[1])) { |
202
|
|
|
if (preg_match('/[#\?:]/', $matches[1])) { |
203
|
|
|
return $matches[0]; |
204
|
|
|
} |
205
|
|
|
return $this->replace($matches); |
206
|
|
|
} |
207
|
|
|
if ($asset_url = $this->resolveHttpAssetUrl($this->root_dir, $matches[1])) { |
208
|
|
|
if ($replacement = $this->embedHttpAsset($asset_url)) { |
209
|
|
|
return $replacement; |
210
|
|
|
} |
211
|
|
|
return $this->embedHttpAssetUrl($asset_url); |
212
|
|
|
} |
213
|
|
|
return $matches[0]; |
214
|
|
|
} |
215
|
|
|
|
216
|
|
|
/** |
217
|
|
|
* Get the contents of a URL and return it as a data uri within url() |
218
|
|
|
* |
219
|
|
|
* @param string $url the URL to the file to embed |
220
|
|
|
* @return string|bool the string for the CSS url property, or FALSE if the |
221
|
|
|
* url could not/should not be embedded. |
222
|
|
|
*/ |
223
|
|
|
protected function embedHttpAsset($url) |
224
|
|
|
{ |
225
|
|
|
if ($this->http_flags & self::HTTP_EMBED_URL_ONLY) { |
226
|
|
|
return false; |
227
|
|
|
} |
228
|
|
|
if (false === ($content = @file_get_contents($url))) { |
229
|
|
|
$this->httpError('Cannot read url %s', $url); |
230
|
|
|
return false; |
231
|
|
|
} |
232
|
|
|
if (!empty($http_response_header)) { |
233
|
|
|
foreach ($http_response_header as $header) { |
234
|
|
|
$header = strtolower($header); |
235
|
|
|
if (strpos($header, 'content-type:') === 0) { |
236
|
|
|
$mime = trim(substr($header, strlen('content-type:'))); |
237
|
|
|
} |
238
|
|
|
} |
239
|
|
|
} |
240
|
|
|
if (empty($mime)) { |
241
|
|
|
$this->httpError('No mime type sent with "%s"', $url); |
242
|
|
|
return false; |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
// handle a special case: fonts will usually break if embedded, but |
246
|
|
|
// user can force |
247
|
|
|
$embed_fonts = ($this->http_flags & self::HTTP_EMBED_FONTS); |
248
|
|
|
$is_font = strpos($mime, 'font') !== false; |
249
|
|
|
if ($is_font && !$embed_fonts) { |
250
|
|
|
return false; |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
// another special case: SVG is often a font and will cause the |
254
|
|
|
// stylesheet to bloat if it's embeded for browsers that don't use it. |
255
|
|
|
$embed_svg = ($this->http_flags & self::HTTP_EMBED_SVG); |
256
|
|
|
$is_svg = strpos($mime, 'svg') !== false; |
257
|
|
|
if ($is_svg && !($embed_svg || $embed_fonts)) { |
258
|
|
|
return false; |
259
|
|
|
} |
260
|
|
|
|
261
|
|
|
return sprintf(self::URI_PATTERN, $mime, base64_encode($content)); |
262
|
|
|
} |
263
|
|
|
|
264
|
|
|
/** |
265
|
|
|
* For URLs that could not/should not be embedded, embed the resolved URL |
266
|
|
|
* instead. |
267
|
|
|
* |
268
|
|
|
* @param string $url |
269
|
|
|
* @return string |
270
|
|
|
*/ |
271
|
|
|
protected function embedHttpAssetUrl($url) |
272
|
|
|
{ |
273
|
|
|
if (!($this->http_flags & self::HTTP_EMBED_SCHEME)) { |
274
|
|
|
$url = preg_replace('/^https?:/', '', $url); |
275
|
|
|
} |
276
|
|
|
return sprintf("url('%s')", $url); |
277
|
|
|
} |
278
|
|
|
|
279
|
|
|
/** |
280
|
|
|
* Check if an asset is remote or local |
281
|
|
|
* |
282
|
|
|
* @param string $path the path specified in the CSS file |
283
|
|
|
* |
284
|
|
|
* @return bool |
285
|
|
|
*/ |
286
|
|
|
protected function isHttpAsset($path) |
287
|
|
|
{ |
288
|
|
|
// if the root directory is remote, all assets are remote |
289
|
|
|
$schemes = array('http://', 'https://', '//'); |
290
|
|
|
foreach ($schemes as $scheme) { |
291
|
|
|
if (strpos($this->root_dir, $scheme) === 0) { |
292
|
|
|
return true; |
293
|
|
|
} |
294
|
|
|
} |
295
|
|
|
// check for remote embedded assets |
296
|
|
|
foreach ($schemes as $scheme) { |
297
|
|
|
if (strpos($path, $scheme) === 0) { |
298
|
|
|
return true; |
299
|
|
|
} |
300
|
|
|
} |
301
|
|
|
// absolutes should be remote |
302
|
|
|
if (strpos($path, '/') === 0) { |
303
|
|
|
return true; |
304
|
|
|
} |
305
|
|
|
// otherwise, it's a local asset |
306
|
|
|
return false; |
307
|
|
|
} |
308
|
|
|
|
309
|
|
|
/** |
310
|
|
|
* Resolve the URL to an http asset |
311
|
|
|
* |
312
|
|
|
* @param string $root_url the root URL |
313
|
|
|
* @param string |
314
|
|
|
*/ |
315
|
|
|
protected function resolveHttpAssetUrl($root_url, $path) |
316
|
|
|
{ |
317
|
|
|
$default_scheme = ($this->http_flags & self::HTTP_DEFAULT_HTTPS) |
318
|
|
|
? 'https:' |
319
|
|
|
: 'http:' |
320
|
|
|
; |
321
|
|
|
|
322
|
|
|
// case 1: path is already fully qualified url |
323
|
|
|
if (strpos($path, '//') === 0) { |
324
|
|
|
$path = $default_scheme . $path; |
325
|
|
|
} |
326
|
|
|
if (preg_match('/^https?:\/\//', $path)) { |
327
|
|
|
if (!filter_var($path, FILTER_VALIDATE_URL)) { |
328
|
|
|
$this->httpError('Invalid asset url "%s"', $path); |
329
|
|
|
return false; |
330
|
|
|
} |
331
|
|
|
return $path; |
332
|
|
|
} |
333
|
|
|
|
334
|
|
|
if (strpos($root_url, '//') === 0) { |
335
|
|
|
$root_url = $default_scheme . $root_url; |
336
|
|
|
} |
337
|
|
|
$root_domain = preg_replace('#^(https?://[^/]+).*#', '$1', $root_url); |
338
|
|
|
$root_path = substr($root_url, strlen($root_domain)); |
339
|
|
|
|
340
|
|
|
// case 2: asset is absolute path |
341
|
|
|
if (strpos($path, '/') === 0) { |
342
|
|
|
return $root_domain . $path; |
343
|
|
|
} |
344
|
|
|
|
345
|
|
|
// case 3: asset is relative path |
346
|
|
|
// remove directory transversal (file_get_contents seems to choke on it) |
347
|
|
|
$path = explode('/', $path); |
348
|
|
|
$root_path = array_filter(explode('/', $root_path)); |
349
|
|
|
$asset_path = array(); |
350
|
|
|
while (NULL !== ($part = array_shift($path))) { |
351
|
|
|
if (!$part || $part === '.') { |
|
|
|
|
352
|
|
|
// drop the empty part |
353
|
|
|
} elseif ($part == '..') { |
354
|
|
|
array_pop($root_path); |
355
|
|
|
} else { |
356
|
|
|
$asset_path[] = $part; |
357
|
|
|
} |
358
|
|
|
} |
359
|
|
|
$asset_path = implode('/', $asset_path); |
360
|
|
|
$root_path = empty($root_path) ? '/' : '/' . implode('/', $root_path) . '/'; |
361
|
|
|
|
362
|
|
|
// ... and build the URL |
363
|
|
|
$url = $root_domain . $root_path . $asset_path; |
364
|
|
|
if (!filter_var($url, FILTER_VALIDATE_URL)) { |
365
|
|
|
$this->httpError('Could not resolve "%s" with root "%s"', $path, $this->root_dir); |
366
|
|
|
return false; |
367
|
|
|
} |
368
|
|
|
return $url; |
369
|
|
|
} |
370
|
|
|
|
371
|
|
|
/** |
372
|
|
|
* Throw an exception if HTTP_URL_ON_ERROR is not set |
373
|
|
|
* |
374
|
|
|
* @param string $msg the message |
375
|
|
|
* @param string $interpolations... strings to interpolate in the error message |
|
|
|
|
376
|
|
|
* @throws \InvalidArgmumentException |
377
|
|
|
* @return void |
378
|
|
|
*/ |
379
|
|
|
protected function httpError($msg, $interpolations) |
|
|
|
|
380
|
|
|
{ |
381
|
|
|
if ($this->http_flags & self::HTTP_URL_ON_ERROR) { |
382
|
|
|
return; |
383
|
|
|
} |
384
|
|
|
$msg = call_user_func_array('sprintf', func_get_args()); |
385
|
|
|
throw new \InvalidArgumentException($msg); |
386
|
|
|
} |
387
|
|
|
} |
388
|
|
|
|
This check looks for the bodies of
if
statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.These
if
bodies can be removed. If you have an empty if but statements in theelse
branch, consider inverting the condition.could be turned into
This is much more concise to read.