Completed
Pull Request — master (#17)
by
unknown
02:36
created

CssEmbed::assetIsEmbeddable()   B

Complexity

Conditions 6
Paths 3

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 16
rs 8.8571
cc 6
eloc 10
nc 3
nop 1
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";
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
19
    const SEARCH_PATTERN = "%url\\(['\" ]*((?!data:)[^'\" ]+)['\" ]*\\)%U";
20
    const DATA_URI_PATTERN = "url(data:%s;base64,%s)";
21
    const URL_URI_PATTERN = "url('%s')";
22
    const MIME_MAGIC_URL = 'http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types';
23
    const EMBED_FONTS = 1;
24
    const EMBED_SVG = 2;
25
    const URL_ON_ERROR = 4;
26
    const HTTP_DEFAULT_HTTPS = 1;
27
    const HTTP_EMBED_SCHEME = 2;
28
    const HTTP_EMBED_URL_ONLY = 4;
29
30
    /** @var string the root directory for finding assets */
31
    protected $root_dir;
32
33
    /** @var string the path to the local mime.magic database */
34
    protected $mime_magic_path = null;
35
36
    /** @var integer flags that modify behavior, embed SVG by default for BC */
37
    protected $flags = 2;
38
39
    /** @var bool enable HTTP asset fetching */
40
    protected $http_enabled = false;
41
42
    /** @var integer flags that modify behavior in HTTP only */
43
    protected $http_flags = 0;
44
45
    /**
46
     * @param $root_dir
47
     */
48
    public function setRootDir($root_dir)
49
    {
50
        $this->root_dir = $root_dir;
51
    }
52
53
    /**
54
     * Set embedding options. Flags:
55
     *
56
     *     - CssEmbed::EMBED_FONTS: embedding fonts will usually break them
57
     *       in most browsers.  Enable this flag to force the embed. WARNING:
58
     *       this flag is currently not unit tested, but seems to work.
59
     *     - CssEmbed::EMBED_SVG: SVG is often used as a font face; however
60
     *       including these in a stylesheet will cause it to bloat for browsers
61
     *       that don't use it.  SVGs will be embedded by default.
62
     *     - CssEmbed::URL_ON_ERROR: if there is an error fetching an asset,
63
     *       embed a URL (or best guess at URL) instead of throwing an exception
64
     *
65
     * @param integer $flags
66
     *
67
     * @return void
68
     */
69
    public function setOptions($flags)
70
    {
71
        $this->flags = $flags;
72
    }
73
74
    /**
75
     * Enable embedding assets over HTTP, or processing stylesheets from HTTP
76
     * locations. Available flags:
77
     *
78
     *     - CssEmbed::HTTP_DEFAULT_HTTPS: when HTTP assets are enabled, use
79
     *       HTTPS for URLs with no scheme
80
     *     - CssEmbed::HTTP_EMBED_SCHEME: By default, assets that are converted
81
     *       to URLs instead of data urls have no scheme (eg, "//example.com").
82
     *       This is better for stylesheets that are maybe served over http or
83
     *       https, but it will break stylesheets served from a local HTML file.
84
     *       Set this option to force the schema (eg, "http://example.com").
85
     *     - CssEmbed::HTTP_EMBED_URL_ONLY: do not convert assets to data URLs,
86
     *       only the fully qualified URL.
87
     *
88
     * @note this method will turn the options URL_ON_ERROR on and EMBED_SVG
89
     * off. You will need to use setOptions() after this method to change that.
90
     *
91
     * @param bool $enable
92
     * @param int $http_flags flags that modify HTTP behaviour
0 ignored issues
show
Bug introduced by
There is no parameter named $http_flags. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
93
     * @return void
94
     */
95
    public function enableHttp($enable = true, $flags = 0)
96
    {
97
        $this->http_enabled = (bool) $enable;
98
        $this->flags = $this->flags|self::URL_ON_ERROR;
99
        $this->flags = $this->flags & (~ self::EMBED_SVG);
100
        $this->http_flags = (int) $flags;
101
    }
102
103
    /**
104
     * Enable the functionality to compare mimes against a custom mime.types file.
105
     *
106
     * @param string $path the path to the mime types file
107
     * @param bool $create download and save the Apache mime types file if the
108
     * specified path does not exist
109
     * @throws \InvalidArgumentException if the mime file does not exist and
110
     * cannot be created.
111
     * @return void
112
     */
113
    public function enableEnhancedMimeTypes(
114
        $path = '/tmp/cssembed.mime.types',
115
        $create = true
116
    ) {
117
        if (!file_exists($path) && $create) {
118
            if ($mime_types = @file_get_contents(self::MIME_MAGIC_URL)) {
119
                // special case: woff2 is too new
120
                if (strpos($mime_types, 'woff2') === false) {
121
                    $mime_types .= "\napplication/font-woff2 woff2";
122
                }
123
                file_put_contents($path, $mime_types);
124
                clearstatcache();
125
            }
126
        }
127
        if (!file_exists($path)) {
128
            $msg = sprintf('mime.types does not exist and cannot be created: "%s"', $path);
129
            throw new \InvalidArgumentException($msg);            
130
        }
131
        if (!is_readable($path) || !is_file($path)) {
132
            $msg = sprintf('Invalid mime.types file: "%s"', $path);
133
            throw new \InvalidArgumentException($msg);        
134
        }
135
        $this->mime_magic_path = $path;
136
    }
137
138
    /**
139
     * @param $css_file
140
     * @return null|string
141
     * @throws \InvalidArgumentException
142
     */
143
    public function embedCss($css_file)
144
    {
145
        $this->setRootDir(dirname($css_file));
146
        $content = @file_get_contents($css_file);
147
        if ($content === false) {
148
            throw new \InvalidArgumentException(sprintf('Cannot read file %s', $css_file));
149
        }
150
        return $this->embedString($content);
151
    }
152
153
    /**
154
     * @param $content
155
     * @return mixed
156
     */
157
    public function embedString($content)
158
    {
159
        return preg_replace_callback(
160
            self::SEARCH_PATTERN,
161
            array($this, 'replace'),
162
            $content
163
        );
164
    }
165
166
    /**
167
     * preg_replace_callback callback for embedString.
168
     *
169
     * @param array $matches
170
     * @return string
171
     */
172
    protected function replace($matches)
173
    {
174
        if ($asset = $this->fetchAsset($matches[1])) {
175
            if ($this->assetIsEmbeddable($asset)) {
176
                return sprintf(
177
                    self::DATA_URI_PATTERN,
178
                    $asset['mime'],
179
                    base64_encode($asset['content'])
180
                );
181
            }
182
        }
183
        if ($url = $this->fetchAssetUrl($matches[1])) {
184
            return sprintf(self::URL_URI_PATTERN, $url);
185
        }
186
        return $matches[0];    
187
    }
188
189
    /**
190
     * Fetch an asset
191
     *
192
     * @param string $path the asset path
193
     * @return array|false an array with keys 'content' for the file content
194
     * and 'mime' for the mime type, or FALSE on error
195
     */
196
    protected function fetchAsset($path)
197
    {
198
        $asset = false;
199
        if ($this->isHttpAsset($path)) {
200
            if ($url = $this->resolveAssetUrl($path)) {
201
                $asset = $this->fetchHttpAsset($url);
202
            }
203
        } else {
204
            if ($absolute_path = $this->resolveAssetPath($path)) {
205
                $asset = $this->fetchLocalAsset($absolute_path);
206
            }
207
        }
208
        return $asset;
209
    }
210
    
211
    /**
212
     * Get the URL to an asset as it would be embedded in a stylesheet
213
     *
214
     * @param string $path the path to the asset as it appears in the stylesheet
215
     * @return string $url the URL to the asset
216
     */
217
    protected function fetchAssetUrl($path)
218
    {
219
        if (!$this->isHttpAsset($path)) {
220
            return $path;
221
        }
222
        $url = $this->resolveAssetUrl($path); 
223
        if (!($this->http_flags & self::HTTP_EMBED_SCHEME)) {
224
            $url = preg_replace('/^https?:/', '', $url);
225
        }
226
        return $url;
227
    }
228
229
    /**
230
     * Fetch an asset stored locally in the filesystem
231
     *
232
     * @param string $absolute_path the absolute path to the asset
233
     * @return array same as fetchAsset
234
     */
235
    protected function fetchLocalAsset($absolute_path)
236
    {
237
        if (!is_file($absolute_path) || !is_readable($absolute_path)) {
238
            $this->error('Cannot read file %s', $absolute_path);
239
            return false;
240
        }
241
        $content = file_get_contents($absolute_path);
242
243
        $mime = $this->detectMime($absolute_path);
244
245
        if (!$mime && function_exists('mime_content_type')) {
246
            $mime = @mime_content_type($absolute_path);
247
        }
248
249
        if (!$mime && $info = @getimagesize($absolute_path)) {
250
            $mime = $info['mime'];
251
        }
252
253
        if (!$mime) {
254
            $mime = 'application/octet-stream';
255
        }
256
257
        return compact('content', 'mime');
258
    }
259
260
    /**
261
     * Fetch an asset stored remotely over HTTP
262
     *
263
     * @param string $url the url to the asset
264
     * @return array same as fetchAsset
265
     */
266
    protected function fetchHttpAsset($url)
267
    {
268
        if ($this->http_flags & self::HTTP_EMBED_URL_ONLY) {
269
            return false;
270
        }
271
        if (false === ($content = @file_get_contents($url))) {
272
            $this->error('Cannot read url %s', $url);
273
            return false;
274
        }
275
        if (!empty($http_response_header)) {
276
            foreach ($http_response_header as $header) {
277
                $header = strtolower($header);
278
                if (strpos($header, 'content-type:') === 0) {
279
                    $mime = trim(substr($header, strlen('content-type:')));
280
                }
281
            }
282
        }
283
        if (empty($mime)) {
284
            $this->error('No mime type sent with "%s"', $url);
285
            return false;
286
        }
287
        return compact('content', 'mime');
288
    }
289
290
    /**
291
     * Check if a successfully fetched an asset is of a type that can be
292
     * embedded given the current options.
293
     *
294
     * @param array $asset the return value of fetchAsset
295
     * @return boolean
296
     */
297
    protected function assetIsEmbeddable(array $asset)
298
    {
299
        $embed_fonts = ($this->flags & self::EMBED_FONTS);
300
        $is_font = strpos($asset['mime'], 'font') !== false;
301
        if ($is_font && !$embed_fonts) {
302
            return false;
303
        }
304
        
305
        $embed_svg = ($this->flags & self::EMBED_SVG);
306
        $is_svg = strpos($asset['mime'], 'svg') !== false;
307
        if ($is_svg && !($embed_svg || $embed_fonts)) {
308
            return false;
309
        }
310
        
311
        return true;
312
    }
313
314
    /**
315
     * Check if an asset is remote or local
316
     *
317
     * @param string $path the path specified in the CSS file
318
     *
319
     * @return bool
320
     */
321
    protected function isHttpAsset($path)
322
    {
323
        if (!$this->http_enabled) {
324
            return false;
325
        }
326
        // if the root directory is remote, all assets are remote
327
        $schemes = array('http://', 'https://', '//');
328
        foreach ($schemes as $scheme) {
329
            if (strpos($this->root_dir, $scheme) === 0) {
330
                return true;
331
            }
332
        }
333
        // check for remote embedded assets
334
        $schemes[] = '/'; // absolutes should be remote
335
        foreach ($schemes as $scheme) {
336
            if (strpos($path, $scheme) === 0) {
337
                return true;
338
            }
339
        }
340
        // otherwise, it's a local asset
341
        return false;
342
    }
343
344
    /**
345
     * Resolve the absolute path to a local asset
346
     *
347
     * @param string $path the path to the asset, relative to root_dir
348
     * @return string|boolean the absolute path, or false if not found
349
     */
350
    protected function resolveAssetPath($path)
351
    {
352
        if (preg_match('/[:\?#]/', $path)) {
353
            return false;
354
        }
355
        return realpath($this->root_dir . DIRECTORY_SEPARATOR . $path);
356
    }
357
358
    /**
359
     * Resolve the URL to an http asset
360
     *
361
     * @param string $root_url the root URL
0 ignored issues
show
Bug introduced by
There is no parameter named $root_url. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
362
     * @param string
363
     */
364
    protected function resolveAssetUrl($path)
365
    {
366
        $root_url = $this->root_dir;
367
        $default_scheme = ($this->http_flags & self::HTTP_DEFAULT_HTTPS)
368
                        ? 'https:'
369
                        : 'http:'
370
                        ;
371
372
        // case 1: path is already fully qualified url
373
        if (strpos($path, '//') === 0) {
374
            $path = $default_scheme . $path;
375
        }
376
        if (preg_match('/^https?:\/\//', $path)) {
377
            if (!filter_var($path, FILTER_VALIDATE_URL)) {
378
                $this->error('Invalid asset url "%s"', $path);
379
                return false;
380
            }
381
            return $path;
382
        }
383
384
        if (strpos($root_url, '//') === 0) {
385
            $root_url = $default_scheme . $root_url;
386
        }
387
        $root_domain = preg_replace('#^(https?://[^/]+).*#', '$1', $root_url);
388
        $root_path = substr($root_url, strlen($root_domain));
389
390
        // case 2: asset is absolute path
391
        if (strpos($path, '/') === 0) {
392
            return $root_domain . $path;
393
        }
394
395
        // case 3: asset is relative path        
396
        // remove directory transversal (file_get_contents seems to choke on it)
397
        $path = explode('/', $path);
398
        $root_path = array_filter(explode('/', $root_path));
399
        $asset_path = array();
400
        while (NULL !== ($part = array_shift($path))) {
401
            if ($part == '..') {
402
                array_pop($root_path);
403
            } elseif ($part && $part !== '.') {
404
                $asset_path[] = $part;
405
            }
406
        }
407
        $asset_path = implode('/', $asset_path);
408
        $root_path = empty($root_path) ? '/' : '/' . implode('/', $root_path) . '/';
409
410
        // ... and build the URL
411
        $url = $root_domain . $root_path . $asset_path;
412
        if (!filter_var($url, FILTER_VALIDATE_URL)) {
413
            $this->error('Could not resolve "%s" with root "%s"', $path, $this->root_dir);
414
            return false;
415
        }
416
        return $url;
417
    }
418
419
    /**
420
     * Check the file mime type against the mime.types file
421
     *
422
     * @param string $path the path to the file
423
     * @return string the mime, or false if it could not be identified
424
     */
425
    protected function detectMime($path)
426
    {
427
        if (!$this->mime_magic_path) {
428
            return false;
429
        }
430
        $ext = strtolower((string) pathinfo($path, PATHINFO_EXTENSION));
431
        if (!$ext) {
432
            return false;
433
        }
434
        $mime_types = file($this->mime_magic_path);
435
        foreach ($mime_types as $line) {
436
            if ($mime = $this->compareMime($ext, $line)) {
437
                return $mime;
438
            }
439
        }
440
        return false;
441
    }
442
443
    /**
444
     * Compare an extention against the a line in the mime.types
445
     *
446
     * @param string $ext the file extension
447
     * @param string $line the line from the mime.types file
448
     * @return string|bool the mime type if there is a match, false if not
449
     */
450
    protected function compareMime($ext, $line)
451
    {
452
        if (strpos($line, '#') === 0) {
453
            return false;
454
        }
455
        $line = preg_replace('/\s+/', ' ', $line);
456
        $line = array_filter(explode(' ', $line));
457
        $mime = array_shift($line);
458
        return in_array($ext, $line) ? $mime : false;
459
    }
460
461
    /**
462
     * Throw an exception if URL_ON_ERROR is not set
463
     *
464
     * @param string $message OPTIONAL the message
0 ignored issues
show
Bug introduced by
There is no parameter named $message. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
465
     * @param string $interpolations,... unlimited OPTIONAL strings to
0 ignored issues
show
Bug introduced by
There is no parameter named $interpolations,.... Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
466
     * interpolate in the error message
467
     * @throws \InvalidArgmumentException
468
     * @return void
469
     */
470
    protected function error()
471
    {
472
        if ($this->flags & self::URL_ON_ERROR) {
473
            return;
474
        }
475
        $args = func_get_args();
476
        if (empty($args)) {
477
            $args[] = 'Unknown Error';
478
        }
479
        $msg = count($args) > 1
480
             ? call_user_func_array('sprintf', $args)
481
             : array_shift($args)
482
             ;
483
        throw new \InvalidArgumentException($msg);
484
    }
485
}
486