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

CssEmbed::embedHttpAsset()   C

Complexity

Conditions 12
Paths 10

Size

Total Lines 40
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 40
rs 5.1612
cc 12
eloc 23
nc 10
nop 1

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 == '..') {
352
                array_pop($root_path);
353
            } elseif ($part && $part !== '.') {
354
                $asset_path[] = $part;
355
            }
356
        }
357
        $asset_path = implode('/', $asset_path);
358
        $root_path = empty($root_path) ? '/' : '/' . implode('/', $root_path) . '/';
359
360
        // ... and build the URL
361
        $url = $root_domain . $root_path . $asset_path;
362
        if (!filter_var($url, FILTER_VALIDATE_URL)) {
363
            $this->httpError('Could not resolve "%s" with root "%s"', $path, $this->root_dir);
364
            return false;
365
        }
366
        return $url;
367
    }
368
369
    /**
370
     * Throw an exception if HTTP_URL_ON_ERROR is not set
371
     *
372
     * @param string $msg the message
373
     * @param string $interpolations... strings to interpolate in the error message
0 ignored issues
show
Documentation introduced by
There is no parameter named $interpolations.... Did you maybe mean $interpolations?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

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

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

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

Loading history...
374
     * @throws \InvalidArgmumentException
375
     * @return void
376
     */
377
    protected function httpError($msg, $interpolations)
0 ignored issues
show
Unused Code introduced by
The parameter $msg is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $interpolations is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
378
    {
379
        if ($this->http_flags & self::HTTP_URL_ON_ERROR) {
380
            return;
381
        }
382
        $msg = call_user_func_array('sprintf', func_get_args());
383
        throw new \InvalidArgumentException($msg);
384
    }
385
}
386