Issues (3)

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/CssToInlineStyles.php (3 issues)

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
declare(strict_types=1);
4
5
namespace voku\CssToInlineStyles;
6
7
use Symfony\Component\CssSelector\CssSelectorConverter;
8
use Symfony\Component\CssSelector\Exception\ExceptionInterface;
9
use voku\helper\HtmlDomParser;
10
11
/**
12
 * CSS to Inline Styles class
13
 */
14
class CssToInlineStyles
15
{
16
    /**
17
     * regular expression: css media queries
18
     *
19
     * @var string
20
     */
21
    private static $cssMediaQueriesRegEx = '#(?:____SIMPLE_HTML_DOM__VOKU__AT____|@)media\\s+(?:only\\s)?(?:[\\s{\\(]|screen|all)\\s?[^{]+{.*}\\s*}\\s*#misuU';
22
23
    /**
24
     * regular expression: css charset
25
     *
26
     * @var string
27
     */
28
    private static $cssCharsetRegEx = '/@charset [\'"][^\'"]+[\'"];/i';
29
30
    /**
31
     * regular expression: conditional inline style tags
32
     *
33
     * @var string
34
     */
35
    private static $excludeConditionalInlineStylesBlockRegEx = '/<!--\[if.*<style.*-->/isU';
36
37
    /**
38
     * regular expression: inline style tags
39
     *
40
     * @var string
41
     */
42
    private static $styleTagRegEx = '|<style(?:\s.*)?>(.*)</style>|isuU';
43
44
    /**
45
     * regular expression: html-comments without conditional comments
46
     *
47
     * @var string
48
     */
49
    private static $htmlCommentWithoutConditionalCommentRegEx = '|<!--(?!\[if).*-->|isU';
50
51
    /**
52
     * regular expression: style-tag with 'cleanup'-css-class
53
     *
54
     * @var string
55
     */
56
    private static $styleTagWithCleanupClassRegEx = '|<style[^>]+class="cleanup"[^>]*>.*</style>|isU';
57
58
    /**
59
     * regular expression: css-comments
60
     *
61
     * @var string
62
     */
63
    private static $styleCommentRegEx = '/\\/\\*.*\\*\\//sU';
64
65
    /**
66
     * @var CssSelectorConverter
67
     */
68
    private $cssConverter;
69
70
    /**
71
     * The CSS to use.
72
     *
73
     * @var string
74
     */
75
    private $css;
76
77
    /**
78
     * The CSS-Media-Queries to use.
79
     *
80
     * @var string
81
     */
82
    private $css_media_queries;
83
84
    /**
85
     * Should the generated HTML be cleaned.
86
     *
87
     * @var bool
88
     */
89
    private $cleanup = false;
90
91
    /**
92
     * The encoding to use.
93
     *
94
     * @var string
95
     */
96
    private $encoding = 'UTF-8';
97
98
    /**
99
     * The HTML to process.
100
     *
101
     * @var string
102
     */
103
    private $html;
104
105
    /**
106
     * Use inline-styles block as CSS.
107
     *
108
     * @var bool
109
     */
110
    private $useInlineStylesBlock = false;
111
112
    /**
113
     * Use link block reference as CSS.
114
     *
115
     * @var bool
116
     */
117
    private $loadCSSFromHTML = false;
118
119
    /**
120
     * Strip original style tags.
121
     *
122
     * @var bool
123
     */
124
    private $stripOriginalStyleTags = false;
125
126
    /**
127
     * Exclude conditional inline-style blocks.
128
     *
129
     * @var bool
130
     */
131
    private $excludeConditionalInlineStylesBlock = true;
132
133
    /**
134
     * Exclude media queries from "$this->css" and keep media queries for inline-styles blocks.
135
     *
136
     * @var bool
137
     */
138
    private $excludeMediaQueries = true;
139
140
    /**
141
     * Exclude media queries from "$this->css" and keep media queries for inline-styles blocks.
142
     *
143
     * @var bool
144
     */
145
    private $excludeCssCharset = true;
146
147
    /**
148
     * Creates an instance, you could set the HTML and CSS here, or load it later.
149
     *
150
     * @param string|null $html the HTML to process
151
     * @param string|null $css  the CSS to use
152
     */
153 58
    public function __construct(string $html = null, string $css = null)
154
    {
155 58
        if ($html !== null) {
156 2
            $this->setHTML($html);
157
        }
158
159 58
        if ($css !== null) {
160 2
            $this->setCSS($css);
161
        }
162
163 58
        if (\class_exists(CssSelectorConverter::class)) {
164 58
            $this->cssConverter = new CssSelectorConverter();
165
        }
166 58
    }
167
168
    /**
169
     * Converts the loaded HTML into an HTML-string with inline styles based on the loaded CSS.
170
     *
171
     * @param bool        $outputXHTML        [optional] <p>Should we output valid XHTML?</p>
172
     * @param int|null    $libXMLExtraOptions [optional] <p>$libXMLExtraOptions Since PHP 5.4.0 and Libxml 2.6.0,
173
     *                                        you may also use the options parameter to specify additional
174
     *                                        Libxml parameters.
175
     *                                        </p>
176
     * @param string|null $path               [optional] <p>Set the path to your external css-files.</p>
177
     *
178
     * @throws Exception
179
     *
180
     * @return string
181
     */
182 56
    public function convert(bool $outputXHTML = false, int $libXMLExtraOptions = null, $path = null): string
183
    {
184
        // validate
185 56
        if (!$this->html) {
186 1
            throw new Exception('No HTML provided.');
187
        }
188
189
        // use local variables
190 55
        $css = $this->css;
191
192
        // create new HtmlDomParser
193 55
        $dom = HtmlDomParser::str_get_html($this->html, $libXMLExtraOptions);
194
195
        // check if there is some link css reference
196 55
        if ($this->loadCSSFromHTML) {
197 1
            foreach ($dom->findMulti('link') as $node) {
198 1
                $file = ($path ?: __DIR__) . '/' . $node->getAttribute('href');
199
200 1
                if (\file_exists($file)) {
201 1
                    $css .= \file_get_contents($file);
202
203
                    // converting to inline css because we don't need/want to load css files, so remove the link
204 1
                    $node->outertext = '';
205
                }
206
            }
207
        }
208
209
        // should we use inline style-block
210 55
        if ($this->useInlineStylesBlock) {
211 31
            if ($this->excludeConditionalInlineStylesBlock === true) {
212 27
                $this->html = (string) \preg_replace(self::$excludeConditionalInlineStylesBlockRegEx, '', $this->html);
213
            }
214
215 31
            $css .= $this->getCssFromInlineHtmlStyleBlock($this->html);
216
        }
217
218
        // process css
219 55
        $cssRules = $this->processCSS($css);
220
221
        // create new XPath
222 55
        $xPath = $this->createXPath($dom->getDocument(), $cssRules);
223
224
        // strip original style tags if we need to
225 55
        if ($this->stripOriginalStyleTags === true) {
226 14
            $this->stripOriginalStyleTags($xPath);
227
        }
228
229
        // cleanup the HTML if we need to
230 55
        if ($this->cleanup === true) {
231 3
            $this->cleanupHTML($xPath);
232
        }
233
234
        // should we output XHTML?
235 55
        if ($outputXHTML === true) {
236 5
            return $dom->xml();
237
        }
238
239
        // just regular HTML 4.01 as it should be used in newsletters
240 51
        $html = $dom->html();
241
242
        // add css media queries from "$this->setCSS()"
243
        if (
244 51
            $this->stripOriginalStyleTags === false
245
            &&
246 51
            $this->css_media_queries
247
        ) {
248 3
            $html = \str_ireplace('</head>', "\n" . '<style type="text/css">' . "\n" . $this->css_media_queries . "\n" . '</style>' . "\n" . '</head>', $html);
249
        }
250
251 51
        return $html;
252
    }
253
254
    /**
255
     * get css from inline-html style-block
256
     *
257
     * @param string $html
258
     *
259
     * @return string
260
     */
261 33
    public function getCssFromInlineHtmlStyleBlock($html): string
262
    {
263
        // init var
264 33
        $css = '';
265 33
        $matches = [];
266
267 33
        $htmlNoComments = (string) \preg_replace(self::$htmlCommentWithoutConditionalCommentRegEx, '', $html);
268
269
        // match the style blocks
270 33
        \preg_match_all(self::$styleTagRegEx, $htmlNoComments, $matches);
271
272
        // any style-blocks found?
273 33
        if (!empty($matches[1])) {
274
            // add
275 32
            foreach ($matches[1] as $match) {
276 32
                $css .= \trim($match) . "\n";
277
            }
278
        }
279
280 33
        return $css;
281
    }
282
283
    /**
284
     * Set CSS to use.
285
     *
286
     * @param string $css <p>The CSS to use.</p>
287
     *
288
     * @return $this
289
     */
290 51
    public function setCSS(string $css): self
291
    {
292 51
        $this->css = $css;
293
294 51
        $this->css_media_queries = $this->getMediaQueries($css);
295
296 51
        return $this;
297
    }
298
299
    /**
300
     * Should the IDs and classes be removed?
301
     *
302
     * @param bool $on Should we enable cleanup?
303
     *
304
     * @return $this
305
     */
306 3
    public function setCleanup(bool $on = true): self
307
    {
308 3
        $this->cleanup = $on;
309
310 3
        return $this;
311
    }
312
313
    /**
314
     * Set the encoding to use with the DOMDocument.
315
     *
316
     * @param string $encoding the encoding to use
317
     *
318
     * @return $this
319
     *
320
     * @deprecated Doesn't have any effect
321
     */
322 2
    public function setEncoding(string $encoding): self
323
    {
324 2
        $this->encoding = $encoding;
325
326 2
        return $this;
327
    }
328
329
    /**
330
     * Set exclude conditional inline-style blocks.
331
     *
332
     * e.g.: <!--[if gte mso 9]><style>.foo { bar } </style><![endif]-->
333
     *
334
     * @param bool $on
335
     *
336
     * @return $this
337
     */
338 6
    public function setExcludeConditionalInlineStylesBlock(bool $on = true): self
339
    {
340 6
        $this->excludeConditionalInlineStylesBlock = $on;
341
342 6
        return $this;
343
    }
344
345
    /**
346
     * Set exclude charset.
347
     *
348
     * @param bool $on
349
     *
350
     * @return $this
351
     */
352 1
    public function setExcludeCssCharset(bool $on = true): self
353
    {
354 1
        $this->excludeCssCharset = $on;
355
356 1
        return $this;
357
    }
358
359
    /**
360
     * Set exclude media queries.
361
     *
362
     * Info: If this is enabled the media queries will be removed before inlining the rules.
363
     *
364
     * WARNING: If you use inline styles block "<style>" this option will keep the media queries.
365
     *
366
     * @param bool $on
367
     *
368
     * @return $this
369
     */
370 16
    public function setExcludeMediaQueries(bool $on = true): self
371
    {
372 16
        $this->excludeMediaQueries = $on;
373
374 16
        return $this;
375
    }
376
377
    /**
378
     * Set HTML to process.
379
     *
380
     * @param string $html <p>The HTML to process.</p>
381
     *
382
     * @return $this
383
     */
384 56
    public function setHTML(string $html): self
385
    {
386
        // strip style definitions, if we use css-class "cleanup" on a style-element
387 56
        $this->html = (string) \preg_replace(self::$styleTagWithCleanupClassRegEx, ' ', $html);
388
389 56
        return $this;
390
    }
391
392
    /**
393
     * Set use of inline link block.
394
     *
395
     * Info: If this is enabled the class will use the links reference in the HTML.
396
     *
397
     * @param bool $on [optional] Should we process link styles?
398
     *
399
     * @return $this
400
     */
401 2
    public function setLoadCSSFromHTML(bool $on = true): self
402
    {
403 2
        $this->loadCSSFromHTML = $on;
404
405 2
        return $this;
406
    }
407
408
    /**
409
     * Set strip original style tags.
410
     *
411
     * Info: If this is enabled the class will remove all style tags in the HTML.
412
     *
413
     * @param bool $on Should we process inline styles?
414
     *
415
     * @return $this
416
     */
417 18
    public function setStripOriginalStyleTags(bool $on = true): self
418
    {
419 18
        $this->stripOriginalStyleTags = $on;
420
421 18
        return $this;
422
    }
423
424
    /**
425
     * Set use of inline styles block.
426
     *
427
     * Info: If this is enabled the class will use the style-block in the HTML.
428
     *
429
     * @param bool $on Should we process inline styles?
430
     *
431
     * @return $this
432
     */
433 31
    public function setUseInlineStylesBlock(bool $on = true): self
434
    {
435 31
        $this->useInlineStylesBlock = $on;
436
437 31
        return $this;
438
    }
439
440
    /**
441
     * Remove id and class attributes.
442
     *
443
     * @param \DOMXPath $xPath the DOMXPath for the entire document
444
     *
445
     * @return void
446
     */
447 3
    private function cleanupHTML(\DOMXPath $xPath)
448
    {
449
        /** @var \DOMAttr[]|\DOMNodeList<\DOMAttr>|false $nodes */
0 ignored issues
show
The doc-type \DOMAttr[]|\DOMNodeList<\DOMAttr>|false could not be parsed: Expected "|" or "end of type", but got "<" at position 23. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
450 3
        $nodes = $xPath->query('//@class | //@id');
451 3
        if ($nodes !== false) {
452 3
            foreach ($nodes as $node) {
453 3
                $node->ownerElement->removeAttributeNode($node);
454
            }
455
        }
456 3
    }
457
458
    /**
459
     * @param \DOMElement $element
460
     * @param array       $ruleProperties
461
     *
462
     * @return string
463
     */
464 40
    private function createPropertyChunks(\DOMElement $element, array $ruleProperties): string
465
    {
466
        // init var
467 40
        $properties = [];
468
469
        // get current styles
470
        /** @var \DOMAttr|null $stylesAttribute */
471 40
        $stylesAttribute = $element->attributes->getNamedItem('style');
472
473
        // any styles defined before?
474 40
        if ($stylesAttribute !== null) {
475
            // get value for the styles attribute
476
            /** @noinspection PhpUndefinedFieldInspection */
477 40
            $definedStyles = (string) $stylesAttribute->value;
478
479
            // split into properties
480 40
            $definedProperties = $this->splitIntoProperties($definedStyles);
481
482 40
            $properties = $this->splitStyleIntoChunks($definedProperties);
483
        }
484
485
        // add new properties into the list
486 40
        foreach ($ruleProperties as $key => $value) {
487
            // If one of the rules is already set and is !important, don't apply it,
488
            // except if the new rule is also important.
489
            if (
490 40
                !isset($properties[$key])
491
                ||
492 12
                \stripos($properties[$key], '!important') === false
493
                ||
494 40
                \stripos(\implode('', (array) $value), '!important') !== false
495
            ) {
496 40
                unset($properties[$key]);
497 40
                $properties[$key] = $value;
498
            }
499
        }
500
501
        // build string
502 40
        $propertyChunks = [];
503
504
        // build chunks
505 40
        foreach ($properties as $key => $values) {
506 40
            foreach ((array) $values as $value) {
507 40
                $propertyChunks[] = $key . ': ' . $value . ';';
508
            }
509
        }
510
511 40
        return \implode(' ', $propertyChunks);
512
    }
513
514
    /**
515
     * create XPath
516
     *
517
     * @param \DOMDocument $document
518
     * @param array        $cssRules
519
     *
520
     * @return \DOMXPath
521
     */
522 55
    private function createXPath(\DOMDocument $document, array $cssRules): \DOMXPath
523
    {
524
        /** @var \DOMElement[]|\SplObjectStorage $propertyStorage */
525 55
        $propertyStorage = new \SplObjectStorage();
526 55
        $xPath = new \DOMXPath($document);
527
528
        // any rules?
529 55
        if (\count($cssRules) !== 0) {
530
            // loop rules
531 42
            foreach ($cssRules as $rule) {
532 42
                $ruleSelector = $rule['selector'];
533 42
                $ruleProperties = $rule['properties'];
534
535 42
                if (!$ruleSelector || !$ruleProperties) {
536 4
                    continue;
537
                }
538
539
                try {
540 41
                    $query = $this->cssConverter->toXPath($ruleSelector);
541 5
                } catch (ExceptionInterface $e) {
542 5
                    $query = null;
543
                }
544
545
                // validate query
546 41
                if ($query === null) {
547 5
                    continue;
548
                }
549
550
                // search elements
551
                /** @var \DOMElement[]|\DOMNodeList<\DOMElement>|false $elements */
0 ignored issues
show
The doc-type \DOMElement[]|\DOMNodeList<\DOMElement>|false could not be parsed: Expected "|" or "end of type", but got "<" at position 26. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
552 40
                $elements = $xPath->query($query);
553
554
                // validate elements
555 40
                if ($elements === false) {
556
                    continue;
557
                }
558
559
                // loop found elements
560 40
                foreach ($elements as $element) {
561
                    if (
562 40
                        $ruleSelector === '*'
563
                        &&
564
                        (
565 2
                            $element->tagName === 'html'
566 2
                            || $element->tagName === 'title'
567 2
                            || $element->tagName === 'meta'
568 2
                            || $element->tagName === 'head'
569 2
                            || $element->tagName === 'style'
570 2
                            || $element->tagName === 'script'
571 40
                            || $element->tagName === 'link'
572
                        )
573
                    ) {
574 2
                        continue;
575
                    }
576
577
                    // no styles stored?
578 40
                    if (!isset($propertyStorage[$element])) {
579
580
                        // init var
581
                        /** @var \DOMAttr|null $originalStyle */
582 40
                        $originalStyle = $element->attributes->getNamedItem('style');
583
584 40
                        if ($originalStyle !== null) {
585 12
                            $originalStyle = (string) $originalStyle->value;
586
                        } else {
587 38
                            $originalStyle = '';
588
                        }
589
590
                        // store original styles
591 40
                        $propertyStorage->attach($element, $originalStyle);
592
593
                        // clear the styles
594 40
                        $element->setAttribute('style', '');
595
                    }
596
597
                    // set attribute
598 40
                    $propertiesString = $this->createPropertyChunks($element, $ruleProperties);
599 40
                    if ($propertiesString) {
600 40
                        $element->setAttribute('style', $propertiesString);
601
                    }
602
                }
603
            }
604
605 42
            foreach ($propertyStorage as $element) {
606 40
                $originalStyle = $propertyStorage->getInfo();
607 40
                if ($originalStyle) {
608 12
                    $originalStyles = $this->splitIntoProperties($originalStyle);
609 12
                    $originalProperties = $this->splitStyleIntoChunks($originalStyles);
610
611
                    // set attribute
612 12
                    $propertiesString = $this->createPropertyChunks($element, $originalProperties);
613 12
                    if ($propertiesString) {
614 40
                        $element->setAttribute('style', $propertiesString);
615
                    }
616
                }
617
            }
618
        }
619
620 55
        return $xPath;
621
    }
622
623
    /**
624
     * @param string $css
625
     *
626
     * @return string
627
     */
628 55
    private function doCleanup($css): string
629
    {
630
        // remove newlines & replace double quotes by single quotes
631 55
        $css = \str_replace(
632 55
            ["\r", "\n", '"'],
633 55
            ['', '', '\''],
634 55
            $css
635
        );
636
637
        // remove comments
638 55
        $css = (string) \preg_replace(self::$styleCommentRegEx, '', $css);
639
640
        // remove spaces
641 55
        $css = (string) \preg_replace('/\s\s+/u', ' ', $css);
642
643
        // remove css charset
644 55
        if ($this->excludeCssCharset === true) {
645 55
            $css = $this->stripeCharsetInCss($css);
646
        }
647
648
        // remove css media queries
649 55
        if ($this->excludeMediaQueries === true) {
650 53
            $css = $this->stripeMediaQueries($css);
651
        }
652
653 55
        return (string) $css;
654
    }
655
656
    /**
657
     * get css media queries from the string
658
     *
659
     * @param string $css
660
     *
661
     * @return string
662
     */
663 51
    private function getMediaQueries($css): string
664
    {
665
        // remove comments previously to matching media queries
666 51
        $css = (string) \preg_replace(self::$styleCommentRegEx, '', $css);
667
668 51
        \preg_match_all(self::$cssMediaQueriesRegEx, $css, $matches);
669
670 51
        return \implode("\n", $matches[0]);
671
    }
672
673
    /**
674
     * Process the loaded CSS
675
     *
676
     * @param string $css
677
     *
678
     * @return array
679
     */
680 55
    private function processCSS($css): array
681
    {
682
        //reset current set of rules
683 55
        $cssRules = [];
684
685
        // init vars
686 55
        $css = (string) $css;
687
688 55
        $css = $this->doCleanup($css);
689
690
        // rules are splitted by }
691 55
        $rules = \explode('}', $css);
692
693
        // init var
694 55
        $i = 1;
695
696
        // loop rules
697 55
        foreach ($rules as $rule) {
698
            // split into chunks
699 55
            $chunks = \explode('{', $rule);
700
701
            // invalid rule?
702 55
            if (!isset($chunks[1])) {
703 55
                continue;
704
            }
705
706
            // set the selectors
707 42
            $selectors = \trim($chunks[0]);
708
709
            // get css-properties
710 42
            $cssProperties = \trim($chunks[1]);
711
712
            // split multiple selectors
713 42
            $selectors = \explode(',', $selectors);
714
715
            // loop selectors
716 42
            foreach ($selectors as $selector) {
717
                // cleanup
718 42
                $selector = \trim($selector);
719
720
                // build an array for each selector
721 42
                $ruleSet = [];
722
723
                // store selector
724 42
                $ruleSet['selector'] = $selector;
725
726
                // process the properties
727 42
                $ruleSet['properties'] = $this->processCSSProperties($cssProperties);
728
729
                // calculate specificity
730 42
                $ruleSet['specificity'] = Specificity::fromSelector($selector);
731
732
                // remember the order in which the rules appear
733 42
                $ruleSet['order'] = $i;
734
735
                // add into rules
736 42
                $cssRules[] = $ruleSet;
737
738
                // increment
739 42
                ++$i;
740
            }
741
        }
742
743
        // sort based on specificity
744 55
        if (\count($cssRules) !== 0) {
745 42
            \usort($cssRules, [__CLASS__, 'sortOnSpecificity']);
746
        }
747
748 55
        return $cssRules;
749
    }
750
751
    /**
752
     * Process the CSS-properties
753
     *
754
     * @param string $propertyString the CSS-properties
755
     *
756
     * @return array
757
     */
758 42
    private function processCSSProperties($propertyString): array
759
    {
760
        // split into chunks
761 42
        $properties = $this->splitIntoProperties($propertyString);
762
763
        // init var
764 42
        $pairs = [];
765
766
        // loop properties
767 42
        foreach ($properties as $property) {
768
            // split into chunks
769 42
            $chunks = \explode(':', $property, 2);
770
771
            // validate
772 42
            if (!isset($chunks[1])) {
773 36
                continue;
774
            }
775
776
            // cleanup
777 41
            $chunks[0] = \trim($chunks[0]);
778 41
            $chunks[1] = \trim($chunks[1]);
779
780
            // add to pairs array
781
            if (
782 41
                !isset($pairs[$chunks[0]])
783
                ||
784 41
                !\in_array($chunks[1], $pairs[$chunks[0]], true)
785
            ) {
786 41
                $pairs[$chunks[0]][] = $chunks[1];
787
            }
788
        }
789
790
        // sort the pairs
791 42
        \ksort($pairs);
792
793
        // return
794 42
        return $pairs;
795
    }
796
797
    /**
798
     * Sort an array on the specificity element in an ascending way.
799
     *
800
     * INFO: Lower specificity will be sorted to the beginning of the array.
801
     *
802
     * @param array $e1 the first element
803
     * @param array $e2 the second element
804
     *
805
     * @return int
806
     *
807
     * @psalm-param array<specificity: Specificity, order: int> $e1
808
     * @psalm-param array<specificity: Specificity, order: int> $e2
809
     */
810 24
    private static function sortOnSpecificity(array $e1, array $e2): int
811
    {
812
        // Compare the specificity
813 24
        $value = $e1['specificity']->compareTo($e2['specificity']);
814
815
        // if the specificity is the same, use the order in which the element appeared
816 24
        if ($value === 0) {
817 19
            $value = $e1['order'] - $e2['order'];
818
        }
819
820 24
        return $value;
821
    }
822
823
    /**
824
     * Split a style string into an array of properties.
825
     * The returned array can contain empty strings.
826
     *
827
     * @param string $styles ex: 'color:blue;font-size:12px;'
828
     *
829
     * @return array an array of strings containing css property ex: array('color:blue','font-size:12px')
830
     */
831 42
    private function splitIntoProperties($styles): array
832
    {
833 42
        $properties = \explode(';', $styles);
834 42
        $propertiesCount = \count($properties);
835
836
        /** @noinspection ForeachInvariantsInspection */
837 42
        for ($i = 0; $i < $propertiesCount; ++$i) {
838
            // If next property begins with base64,
839
            // Then the ';' was part of this property (and we should not have split on it).
840
            if (
841 42
                isset($properties[$i + 1])
842
                &&
843 42
                \strpos($properties[$i + 1], 'base64,') !== false
844
            ) {
845 1
                $properties[$i] .= ';' . $properties[$i + 1];
846 1
                $properties[$i + 1] = '';
847 1
                ++$i;
848
            }
849
        }
850
851 42
        return $properties;
852
    }
853
854
    /**
855
     * @param array $definedProperties
856
     *
857
     * @return array
858
     */
859 40
    private function splitStyleIntoChunks(array $definedProperties): array
860
    {
861
        // init var
862 40
        $properties = [];
863
864
        // loop properties
865 40
        foreach ($definedProperties as $property) {
866
            // validate property
867
            if (
868 40
                !$property
869
                ||
870 40
                \strpos($property, ':') === false
871
            ) {
872 40
                continue;
873
            }
874
875
            // split into chunks
876 20
            $chunks = \explode(':', \trim($property), 2);
877
878
            // validate
879 20
            if (!isset($chunks[1])) {
880
                continue;
881
            }
882
883
            // loop chunks
884 20
            $properties[$chunks[0]] = \trim($chunks[1]);
885
        }
886
887 40
        return $properties;
888
    }
889
890
    /**
891
     * Strip style tags into the generated HTML.
892
     *
893
     * @param \DOMXPath $xPath the DOMXPath for the entire document
894
     *
895
     * @return void
896
     */
897 14
    private function stripOriginalStyleTags(\DOMXPath $xPath)
898
    {
899
        // get all style tags
900
        /** @var \DOMElement[]|\DOMNodeList<\DOMElement>|false $nodes */
0 ignored issues
show
The doc-type \DOMElement[]|\DOMNodeList<\DOMElement>|false could not be parsed: Expected "|" or "end of type", but got "<" at position 26. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
901 14
        $nodes = $xPath->query('descendant-or-self::style');
902 14
        if ($nodes !== false) {
903 14
            foreach ($nodes as $node) {
904 13
                if ($this->excludeMediaQueries === true) {
905
906
                    // remove comments previously to matching media queries
907 12
                    $nodeValueTmp = (string) \preg_replace(self::$styleCommentRegEx, '', $node->nodeValue);
908
909
                    // search for Media Queries
910 12
                    \preg_match_all(self::$cssMediaQueriesRegEx, $nodeValueTmp, $mqs);
911
912
                    // replace the nodeValue with just the Media Queries
913 12
                    $node->nodeValue = \implode("\n", $mqs[0]);
914
                } else {
915
                    // remove the entire style tag
916 1
                    if ($node->parentNode !== null) {
917 13
                        $node->parentNode->removeChild($node);
918
                    }
919
                }
920
            }
921
        }
922 14
    }
923
924
    /**
925
     * remove charset from the string
926
     *
927
     * @param string $css
928
     *
929
     * @return string
930
     */
931 55
    private function stripeCharsetInCss($css): string
932
    {
933 55
        return (string) \preg_replace(self::$cssCharsetRegEx, '', $css);
934
    }
935
936
    /**
937
     * remove css media queries from the string
938
     *
939
     * @param string $css
940
     *
941
     * @return string
942
     */
943 53
    private function stripeMediaQueries($css): string
944
    {
945
        // remove comments previously to matching media queries
946 53
        $css = (string) \preg_replace(self::$styleCommentRegEx, '', $css);
947
948 53
        return (string) \preg_replace(self::$cssMediaQueriesRegEx, '', $css);
949
    }
950
}
951