Issues (6)

Security Analysis    no request data  

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/CSS.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * CSS Minifier
4
 *
5
 * Please report bugs on https://github.com/matthiasmullie/minify/issues
6
 *
7
 * @author Matthias Mullie <[email protected]>
8
 * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved
9
 * @license MIT License
10
 */
11
12
namespace MatthiasMullie\Minify;
13
14
use MatthiasMullie\Minify\Exceptions\FileImportException;
15
use MatthiasMullie\PathConverter\ConverterInterface;
16
use MatthiasMullie\PathConverter\Converter;
17
18
/**
19
 * CSS minifier
20
 *
21
 * Please report bugs on https://github.com/matthiasmullie/minify/issues
22
 *
23
 * @package Minify
24
 * @author Matthias Mullie <[email protected]>
25
 * @author Tijs Verkoyen <[email protected]>
26
 * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved
27
 * @license MIT License
28
 */
29
class CSS extends Minify
30
{
31
    /**
32
     * @var int maximum inport size in kB
33
     */
34
    protected $maxImportSize = 5;
35
36
    /**
37
     * @var string[] valid import extensions
38
     */
39
    protected $importExtensions = array(
40
        'gif' => 'data:image/gif',
41
        'png' => 'data:image/png',
42
        'jpe' => 'data:image/jpeg',
43
        'jpg' => 'data:image/jpeg',
44
        'jpeg' => 'data:image/jpeg',
45
        'svg' => 'data:image/svg+xml',
46
        'woff' => 'data:application/x-font-woff',
47
        'tif' => 'image/tiff',
48
        'tiff' => 'image/tiff',
49
        'xbm' => 'image/x-xbitmap',
50
    );
51
52
    /**
53
     * Set the maximum size if files to be imported.
54
     *
55
     * Files larger than this size (in kB) will not be imported into the CSS.
56
     * Importing files into the CSS as data-uri will save you some connections,
57
     * but we should only import relatively small decorative images so that our
58
     * CSS file doesn't get too bulky.
59
     *
60
     * @param int $size Size in kB
61
     */
62
    public function setMaxImportSize($size)
63
    {
64
        $this->maxImportSize = $size;
65
    }
66
67
    /**
68
     * Set the type of extensions to be imported into the CSS (to save network
69
     * connections).
70
     * Keys of the array should be the file extensions & respective values
71
     * should be the data type.
72
     *
73
     * @param string[] $extensions Array of file extensions
74
     */
75
    public function setImportExtensions(array $extensions)
76
    {
77
        $this->importExtensions = $extensions;
78
    }
79
80
    /**
81
     * Move any import statements to the top.
82
     *
83
     * @param string $content Nearly finished CSS content
84
     *
85
     * @return string
86
     */
87
    protected function moveImportsToTop($content)
88
    {
89
        if (preg_match_all('/(;?)(@import (?<url>url\()?(?P<quotes>["\']?).+?(?P=quotes)(?(url)\)));?/', $content, $matches)) {
90
            // remove from content
91
            foreach ($matches[0] as $import) {
92
                $content = str_replace($import, '', $content);
93
            }
94
95
            // add to top
96
            $content = implode(';', $matches[2]).';'.trim($content, ';');
97
        }
98
99
        return $content;
100
    }
101
102
    /**
103
     * Combine CSS from import statements.
104
     *
105
     * @import's will be loaded and their content merged into the original file,
106
     * to save HTTP requests.
107
     *
108
     * @param string   $source  The file to combine imports for
109
     * @param string   $content The CSS content to combine imports for
110
     * @param string[] $parents Parent paths, for circular reference checks
111
     *
112
     * @return string
113
     *
114
     * @throws FileImportException
115
     */
116
    protected function combineImports($source, $content, $parents)
117
    {
118
        $importRegexes = array(
119
            // @import url(xxx)
120
            '/
121
            # import statement
122
            @import
123
124
            # whitespace
125
            \s+
126
127
                # open url()
128
                url\(
129
130
                    # (optional) open path enclosure
131
                    (?P<quotes>["\']?)
132
133
                        # fetch path
134
                        (?P<path>.+?)
135
136
                    # (optional) close path enclosure
137
                    (?P=quotes)
138
139
                # close url()
140
                \)
141
142
                # (optional) trailing whitespace
143
                \s*
144
145
                # (optional) media statement(s)
146
                (?P<media>[^;]*)
147
148
                # (optional) trailing whitespace
149
                \s*
150
151
            # (optional) closing semi-colon
152
            ;?
153
154
            /ix',
155
156
            // @import 'xxx'
157
            '/
158
159
            # import statement
160
            @import
161
162
            # whitespace
163
            \s+
164
165
                # open path enclosure
166
                (?P<quotes>["\'])
167
168
                    # fetch path
169
                    (?P<path>.+?)
170
171
                # close path enclosure
172
                (?P=quotes)
173
174
                # (optional) trailing whitespace
175
                \s*
176
177
                # (optional) media statement(s)
178
                (?P<media>[^;]*)
179
180
                # (optional) trailing whitespace
181
                \s*
182
183
            # (optional) closing semi-colon
184
            ;?
185
186
            /ix',
187
        );
188
189
        // find all relative imports in css
190
        $matches = array();
191
        foreach ($importRegexes as $importRegex) {
192
            if (preg_match_all($importRegex, $content, $regexMatches, PREG_SET_ORDER)) {
193
                $matches = array_merge($matches, $regexMatches);
194
            }
195
        }
196
197
        $search = array();
198
        $replace = array();
199
200
        // loop the matches
201
        foreach ($matches as $match) {
202
            // get the path for the file that will be imported
203
            $importPath = dirname($source).'/'.$match['path'];
204
205
            // only replace the import with the content if we can grab the
206
            // content of the file
207
            if (!$this->canImportByPath($match['path']) || !$this->canImportFile($importPath)) {
208
                continue;
209
            }
210
211
            // check if current file was not imported previously in the same
212
            // import chain.
213
            if (in_array($importPath, $parents)) {
214
                throw new FileImportException('Failed to import file "'.$importPath.'": circular reference detected.');
215
            }
216
217
            // grab referenced file & minify it (which may include importing
218
            // yet other @import statements recursively)
219
            $minifier = new self($importPath);
220
            $minifier->setMaxImportSize($this->maxImportSize);
221
            $minifier->setImportExtensions($this->importExtensions);
222
            $importContent = $minifier->execute($source, $parents);
223
224
            // check if this is only valid for certain media
225
            if (!empty($match['media'])) {
226
                $importContent = '@media '.$match['media'].'{'.$importContent.'}';
227
            }
228
229
            // add to replacement array
230
            $search[] = $match[0];
231
            $replace[] = $importContent;
232
        }
233
234
        // replace the import statements
235
        return str_replace($search, $replace, $content);
236
    }
237
238
    /**
239
     * Import files into the CSS, base64-ized.
240
     *
241
     * @url(image.jpg) images will be loaded and their content merged into the
242
     * original file, to save HTTP requests.
243
     *
244
     * @param string $source  The file to import files for
245
     * @param string $content The CSS content to import files for
246
     *
247
     * @return string
248
     */
249
    protected function importFiles($source, $content)
250
    {
251
        $regex = '/url\((["\']?)(.+?)\\1\)/i';
252
        if ($this->importExtensions && preg_match_all($regex, $content, $matches, PREG_SET_ORDER)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->importExtensions of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
253
            $search = array();
254
            $replace = array();
255
256
            // loop the matches
257
            foreach ($matches as $match) {
258
                $extension = substr(strrchr($match[2], '.'), 1);
259
                if ($extension && !array_key_exists($extension, $this->importExtensions)) {
260
                    continue;
261
                }
262
263
                // get the path for the file that will be imported
264
                $path = $match[2];
265
                $path = dirname($source).'/'.$path;
266
267
                // only replace the import with the content if we're able to get
268
                // the content of the file, and it's relatively small
269
                if ($this->canImportFile($path) && $this->canImportBySize($path)) {
270
                    // grab content && base64-ize
271
                    $importContent = $this->load($path);
272
                    $importContent = base64_encode($importContent);
273
274
                    // build replacement
275
                    $search[] = $match[0];
276
                    $replace[] = 'url('.$this->importExtensions[$extension].';base64,'.$importContent.')';
277
                }
278
            }
279
280
            // replace the import statements
281
            $content = str_replace($search, $replace, $content);
282
        }
283
284
        return $content;
285
    }
286
287
    /**
288
     * Minify the data.
289
     * Perform CSS optimizations.
290
     *
291
     * @param string[optional] $path    Path to write the data to
292
     * @param string[]         $parents Parent paths, for circular reference checks
293
     *
294
     * @return string The minified data
295
     */
296
    public function execute($path = null, $parents = array())
297
    {
298
        $content = '';
299
300
        // loop CSS data (raw data and files)
301
        foreach ($this->data as $source => $css) {
302
            /*
303
             * Let's first take out strings & comments, since we can't just
304
             * remove whitespace anywhere. If whitespace occurs inside a string,
305
             * we should leave it alone. E.g.:
306
             * p { content: "a   test" }
307
             */
308
            $this->extractStrings();
309
            $this->stripComments();
310
            $this->extractMath();
311
            $this->extractCustomProperties();
312
            $css = $this->replace($css);
313
314
            $css = $this->stripWhitespace($css);
315
            $css = $this->shortenColors($css);
316
            $css = $this->shortenZeroes($css);
317
            $css = $this->shortenFontWeights($css);
318
            $css = $this->stripEmptyTags($css);
319
320
            // restore the string we've extracted earlier
321
            $css = $this->restoreExtractedData($css);
322
323
            $source = is_int($source) ? '' : $source;
324
            $parents = $source ? array_merge($parents, array($source)) : $parents;
325
            $css = $this->combineImports($source, $css, $parents);
326
            $css = $this->importFiles($source, $css);
327
328
            /*
329
             * If we'll save to a new path, we'll have to fix the relative paths
330
             * to be relative no longer to the source file, but to the new path.
331
             * If we don't write to a file, fall back to same path so no
332
             * conversion happens (because we still want it to go through most
333
             * of the move code, which also addresses url() & @import syntax...)
334
             */
335
            $converter = $this->getPathConverter($source, $path ?: $source);
336
            $css = $this->move($converter, $css);
337
338
            // combine css
339
            $content .= $css;
340
        }
341
342
        $content = $this->moveImportsToTop($content);
343
344
        return $content;
345
    }
346
347
    /**
348
     * Moving a css file should update all relative urls.
349
     * Relative references (e.g. ../images/image.gif) in a certain css file,
350
     * will have to be updated when a file is being saved at another location
351
     * (e.g. ../../images/image.gif, if the new CSS file is 1 folder deeper).
352
     *
353
     * @param ConverterInterface $converter Relative path converter
354
     * @param string             $content   The CSS content to update relative urls for
355
     *
356
     * @return string
357
     */
358
    protected function move(ConverterInterface $converter, $content)
359
    {
360
        /*
361
         * Relative path references will usually be enclosed by url(). @import
362
         * is an exception, where url() is not necessary around the path (but is
363
         * allowed).
364
         * This *could* be 1 regular expression, where both regular expressions
365
         * in this array are on different sides of a |. But we're using named
366
         * patterns in both regexes, the same name on both regexes. This is only
367
         * possible with a (?J) modifier, but that only works after a fairly
368
         * recent PCRE version. That's why I'm doing 2 separate regular
369
         * expressions & combining the matches after executing of both.
370
         */
371
        $relativeRegexes = array(
372
            // url(xxx)
373
            '/
374
            # open url()
375
            url\(
376
377
                \s*
378
379
                # open path enclosure
380
                (?P<quotes>["\'])?
381
382
                    # fetch path
383
                    (?P<path>.+?)
384
385
                # close path enclosure
386
                (?(quotes)(?P=quotes))
387
388
                \s*
389
390
            # close url()
391
            \)
392
393
            /ix',
394
395
            // @import "xxx"
396
            '/
397
            # import statement
398
            @import
399
400
            # whitespace
401
            \s+
402
403
                # we don\'t have to check for @import url(), because the
404
                # condition above will already catch these
405
406
                # open path enclosure
407
                (?P<quotes>["\'])
408
409
                    # fetch path
410
                    (?P<path>.+?)
411
412
                # close path enclosure
413
                (?P=quotes)
414
415
            /ix',
416
        );
417
418
        // find all relative urls in css
419
        $matches = array();
420
        foreach ($relativeRegexes as $relativeRegex) {
421
            if (preg_match_all($relativeRegex, $content, $regexMatches, PREG_SET_ORDER)) {
422
                $matches = array_merge($matches, $regexMatches);
423
            }
424
        }
425
426
        $search = array();
427
        $replace = array();
428
429
        // loop all urls
430
        foreach ($matches as $match) {
431
            // determine if it's a url() or an @import match
432
            $type = (strpos($match[0], '@import') === 0 ? 'import' : 'url');
433
434
            $url = $match['path'];
435
            if ($this->canImportByPath($url)) {
436
                // attempting to interpret GET-params makes no sense, so let's discard them for awhile
437
                $params = strrchr($url, '?');
438
                $url = $params ? substr($url, 0, -strlen($params)) : $url;
439
440
                // fix relative url
441
                $url = $converter->convert($url);
442
443
                // now that the path has been converted, re-apply GET-params
444
                $url .= $params;
445
            }
446
447
            /*
448
             * Urls with control characters above 0x7e should be quoted.
449
             * According to Mozilla's parser, whitespace is only allowed at the
450
             * end of unquoted urls.
451
             * Urls with `)` (as could happen with data: uris) should also be
452
             * quoted to avoid being confused for the url() closing parentheses.
453
             * And urls with a # have also been reported to cause issues.
454
             * Urls with quotes inside should also remain escaped.
455
             *
456
             * @see https://developer.mozilla.org/nl/docs/Web/CSS/url#The_url()_functional_notation
457
             * @see https://hg.mozilla.org/mozilla-central/rev/14abca4e7378
458
             * @see https://github.com/matthiasmullie/minify/issues/193
459
             */
460
            $url = trim($url);
461
            if (preg_match('/[\s\)\'"#\x{7f}-\x{9f}]/u', $url)) {
462
                $url = $match['quotes'] . $url . $match['quotes'];
463
            }
464
465
            // build replacement
466
            $search[] = $match[0];
467
            if ($type === 'url') {
468
                $replace[] = 'url('.$url.')';
469
            } elseif ($type === 'import') {
470
                $replace[] = '@import "'.$url.'"';
471
            }
472
        }
473
474
        // replace urls
475
        return str_replace($search, $replace, $content);
476
    }
477
478
    /**
479
     * Shorthand hex color codes.
480
     * #FF0000 -> #F00.
481
     *
482
     * @param string $content The CSS content to shorten the hex color codes for
483
     *
484
     * @return string
485
     */
486
    protected function shortenColors($content)
487
    {
488
        $content = preg_replace('/(?<=[: ])#([0-9a-z])\\1([0-9a-z])\\2([0-9a-z])\\3(?:([0-9a-z])\\4)?(?=[; }])/i', '#$1$2$3$4', $content);
489
490
        // remove alpha channel if it's pointless...
491
        $content = preg_replace('/(?<=[: ])#([0-9a-z]{6})ff?(?=[; }])/i', '#$1', $content);
492
        $content = preg_replace('/(?<=[: ])#([0-9a-z]{3})f?(?=[; }])/i', '#$1', $content);
493
494
        $colors = array(
495
            // we can shorten some even more by replacing them with their color name
496
            '#F0FFFF' => 'azure',
497
            '#F5F5DC' => 'beige',
498
            '#A52A2A' => 'brown',
499
            '#FF7F50' => 'coral',
500
            '#FFD700' => 'gold',
501
            '#808080' => 'gray',
502
            '#008000' => 'green',
503
            '#4B0082' => 'indigo',
504
            '#FFFFF0' => 'ivory',
505
            '#F0E68C' => 'khaki',
506
            '#FAF0E6' => 'linen',
507
            '#800000' => 'maroon',
508
            '#000080' => 'navy',
509
            '#808000' => 'olive',
510
            '#CD853F' => 'peru',
511
            '#FFC0CB' => 'pink',
512
            '#DDA0DD' => 'plum',
513
            '#800080' => 'purple',
514
            '#F00' => 'red',
515
            '#FA8072' => 'salmon',
516
            '#A0522D' => 'sienna',
517
            '#C0C0C0' => 'silver',
518
            '#FFFAFA' => 'snow',
519
            '#D2B48C' => 'tan',
520
            '#FF6347' => 'tomato',
521
            '#EE82EE' => 'violet',
522
            '#F5DEB3' => 'wheat',
523
            // or the other way around
524
            'WHITE' => '#fff',
525
            'BLACK' => '#000',
526
        );
527
528
        return preg_replace_callback(
529
            '/(?<=[: ])('.implode('|', array_keys($colors)).')(?=[; }])/i',
530
            function ($match) use ($colors) {
531
                return $colors[strtoupper($match[0])];
532
            },
533
            $content
534
        );
535
    }
536
537
    /**
538
     * Shorten CSS font weights.
539
     *
540
     * @param string $content The CSS content to shorten the font weights for
541
     *
542
     * @return string
543
     */
544
    protected function shortenFontWeights($content)
545
    {
546
        $weights = array(
547
            'normal' => 400,
548
            'bold' => 700,
549
        );
550
551
        $callback = function ($match) use ($weights) {
552
            return $match[1].$weights[$match[2]];
553
        };
554
555
        return preg_replace_callback('/(font-weight\s*:\s*)('.implode('|', array_keys($weights)).')(?=[;}])/', $callback, $content);
556
    }
557
558
    /**
559
     * Shorthand 0 values to plain 0, instead of e.g. -0em.
560
     *
561
     * @param string $content The CSS content to shorten the zero values for
562
     *
563
     * @return string
564
     */
565
    protected function shortenZeroes($content)
566
    {
567
        // we don't want to strip units in `calc()` expressions:
568
        // `5px - 0px` is valid, but `5px - 0` is not
569
        // `10px * 0` is valid (equates to 0), and so is `10 * 0px`, but
570
        // `10 * 0` is invalid
571
        // we've extracted calcs earlier, so we don't need to worry about this
572
573
        // reusable bits of code throughout these regexes:
574
        // before & after are used to make sure we don't match lose unintended
575
        // 0-like values (e.g. in #000, or in http://url/1.0)
576
        // units can be stripped from 0 values, or used to recognize non 0
577
        // values (where wa may be able to strip a .0 suffix)
578
        $before = '(?<=[:(, ])';
579
        $after = '(?=[ ,);}])';
580
        $units = '(em|ex|%|px|cm|mm|in|pt|pc|ch|rem|vh|vw|vmin|vmax|vm)';
581
582
        // strip units after zeroes (0px -> 0)
583
        // NOTE: it should be safe to remove all units for a 0 value, but in
584
        // practice, Webkit (especially Safari) seems to stumble over at least
585
        // 0%, potentially other units as well. Only stripping 'px' for now.
586
        // @see https://github.com/matthiasmullie/minify/issues/60
587
        $content = preg_replace('/'.$before.'(-?0*(\.0+)?)(?<=0)px'.$after.'/', '\\1', $content);
588
589
        // strip 0-digits (.0 -> 0)
590
        $content = preg_replace('/'.$before.'\.0+'.$units.'?'.$after.'/', '0\\1', $content);
591
        // strip trailing 0: 50.10 -> 50.1, 50.10px -> 50.1px
592
        $content = preg_replace('/'.$before.'(-?[0-9]+\.[0-9]+)0+'.$units.'?'.$after.'/', '\\1\\2', $content);
593
        // strip trailing 0: 50.00 -> 50, 50.00px -> 50px
594
        $content = preg_replace('/'.$before.'(-?[0-9]+)\.0+'.$units.'?'.$after.'/', '\\1\\2', $content);
595
        // strip leading 0: 0.1 -> .1, 01.1 -> 1.1
596
        $content = preg_replace('/'.$before.'(-?)0+([0-9]*\.[0-9]+)'.$units.'?'.$after.'/', '\\1\\2\\3', $content);
597
598
        // strip negative zeroes (-0 -> 0) & truncate zeroes (00 -> 0)
599
        $content = preg_replace('/'.$before.'-?0+'.$units.'?'.$after.'/', '0\\1', $content);
600
601
        // IE doesn't seem to understand a unitless flex-basis value (correct -
602
        // it goes against the spec), so let's add it in again (make it `%`,
603
        // which is only 1 char: 0%, 0px, 0 anything, it's all just the same)
604
        // @see https://developer.mozilla.org/nl/docs/Web/CSS/flex
605
        $content = preg_replace('/flex:([0-9]+\s[0-9]+\s)0([;\}])/', 'flex:${1}0%${2}', $content);
606
        $content = preg_replace('/flex-basis:0([;\}])/', 'flex-basis:0%${1}', $content);
607
608
        return $content;
609
    }
610
611
    /**
612
     * Strip empty tags from source code.
613
     *
614
     * @param string $content
615
     *
616
     * @return string
617
     */
618
    protected function stripEmptyTags($content)
619
    {
620
        $content = preg_replace('/(?<=^)[^\{\};]+\{\s*\}/', '', $content);
621
        $content = preg_replace('/(?<=(\}|;))[^\{\};]+\{\s*\}/', '', $content);
622
623
        return $content;
624
    }
625
626
    /**
627
     * Strip comments from source code.
628
     */
629
    protected function stripComments()
630
    {
631
        // PHP only supports $this inside anonymous functions since 5.4
632
        $minifier = $this;
633
        $callback = function ($match) use ($minifier) {
634
            $count = count($minifier->extracted);
635
            $placeholder = '/*'.$count.'*/';
636
            $minifier->extracted[$placeholder] = $match[0];
637
638
            return $placeholder;
639
        };
640
        $this->registerPattern('/\n?\/\*(!|.*?@license|.*?@preserve).*?\*\/\n?/s', $callback);
641
642
        $this->registerPattern('/\/\*.*?\*\//s', '');
643
    }
644
645
    /**
646
     * Strip whitespace.
647
     *
648
     * @param string $content The CSS content to strip the whitespace for
649
     *
650
     * @return string
651
     */
652
    protected function stripWhitespace($content)
653
    {
654
        // remove leading & trailing whitespace
655
        $content = preg_replace('/^\s*/m', '', $content);
656
        $content = preg_replace('/\s*$/m', '', $content);
657
658
        // replace newlines with a single space
659
        $content = preg_replace('/\s+/', ' ', $content);
660
661
        // remove whitespace around meta characters
662
        // inspired by stackoverflow.com/questions/15195750/minify-compress-css-with-regex
663
        $content = preg_replace('/\s*([\*$~^|]?+=|[{};,>~]|!important\b)\s*/', '$1', $content);
664
        $content = preg_replace('/([\[(:>\+])\s+/', '$1', $content);
665
        $content = preg_replace('/\s+([\]\)>\+])/', '$1', $content);
666
        $content = preg_replace('/\s+(:)(?![^\}]*\{)/', '$1', $content);
667
668
        // whitespace around + and - can only be stripped inside some pseudo-
669
        // classes, like `:nth-child(3+2n)`
670
        // not in things like `calc(3px + 2px)`, shorthands like `3px -2px`, or
671
        // selectors like `div.weird- p`
672
        $pseudos = array('nth-child', 'nth-last-child', 'nth-last-of-type', 'nth-of-type');
673
        $content = preg_replace('/:('.implode('|', $pseudos).')\(\s*([+-]?)\s*(.+?)\s*([+-]?)\s*(.*?)\s*\)/', ':$1($2$3$4$5)', $content);
674
675
        // remove semicolon/whitespace followed by closing bracket
676
        $content = str_replace(';}', '}', $content);
677
678
        return trim($content);
679
    }
680
681
    /**
682
     * Replace all occurrences of functions that may contain math, where
683
     * whitespace around operators needs to be preserved (e.g. calc, clamp)
684
     */
685
    protected function extractMath()
686
    {
687
        $functions = array('calc', 'clamp', 'min', 'max');
688
        $pattern = '/\b('. implode('|', $functions) .')(\(.+?)(?=$|;|})/m';
689
690
        // PHP only supports $this inside anonymous functions since 5.4
691
        $minifier = $this;
692
        $callback = function ($match) use ($minifier, $pattern, &$callback) {
693
            $function = $match[1];
694
            $length = strlen($match[2]);
695
            $expr = '';
696
            $opened = 0;
697
698
            // the regular expression for extracting math has 1 significant problem:
699
            // it can't determine the correct closing parenthesis...
700
            // instead, it'll match a larger portion of code to where it's certain that
701
            // the calc() musts have ended, and we'll figure out which is the correct
702
            // closing parenthesis here, by counting how many have opened
703
            for ($i = 0; $i < $length; $i++) {
704
                $char = $match[2][$i];
705
                $expr .= $char;
706
                if ($char === '(') {
707
                    $opened++;
708
                } elseif ($char === ')' && --$opened === 0) {
709
                    break;
710
                }
711
            }
712
713
            // now that we've figured out where the calc() starts and ends, extract it
714
            $count = count($minifier->extracted);
715
            $placeholder = 'math('.$count.')';
716
            $minifier->extracted[$placeholder] = $function.'('.trim(substr($expr, 1, -1)).')';
717
718
            // and since we've captured more code than required, we may have some leftover
719
            // calc() in here too - go recursive on the remaining but of code to go figure
720
            // that out and extract what is needed
721
            $rest = str_replace($function.$expr, '', $match[0]);
722
            $rest = preg_replace_callback($pattern, $callback, $rest);
723
724
            return $placeholder.$rest;
725
        };
726
727
        $this->registerPattern($pattern, $callback);
728
    }
729
730
    /**
731
     * Replace custom properties, whose values may be used in scenarios where
732
     * we wouldn't want them to be minified (e.g. inside calc)
733
     */
734
    protected function extractCustomProperties()
735
    {
736
        // PHP only supports $this inside anonymous functions since 5.4
737
        $minifier = $this;
738
        $this->registerPattern(
739
            '/(?<=^|[;}])(--[^:;{}"\'\s]+)\s*:([^;{}]+)/m',
740
            function ($match) use ($minifier) {
741
                $placeholder = '--custom-'. count($minifier->extracted) . ':0';
742
                $minifier->extracted[$placeholder] = $match[1] .':'. trim($match[2]);
743
                return $placeholder;
744
745
            }
746
        );
747
    }
748
749
    /**
750
     * Check if file is small enough to be imported.
751
     *
752
     * @param string $path The path to the file
753
     *
754
     * @return bool
755
     */
756
    protected function canImportBySize($path)
757
    {
758
        return ($size = @filesize($path)) && $size <= $this->maxImportSize * 1024;
759
    }
760
761
    /**
762
     * Check if file a file can be imported, going by the path.
763
     *
764
     * @param string $path
765
     *
766
     * @return bool
767
     */
768
    protected function canImportByPath($path)
769
    {
770
        return preg_match('/^(data:|https?:|\\/)/', $path) === 0;
771
    }
772
773
    /**
774
     * Return a converter to update relative paths to be relative to the new
775
     * destination.
776
     *
777
     * @param string $source
778
     * @param string $target
779
     *
780
     * @return ConverterInterface
781
     */
782
    protected function getPathConverter($source, $target)
783
    {
784
        return new Converter($source, $target);
785
    }
786
}
787