From 2c5cd6ffac1c4282ceb22f2da67dbba3c6640f00 Mon Sep 17 00:00:00 2001 From: Ivailo Hristov Date: Sun, 24 Feb 2019 17:32:12 +0200 Subject: [PATCH 01/14] Add basic selector validation --- lib/Sabberworm/CSS/Property/Selector.php | 10 ++++++++++ lib/Sabberworm/CSS/RuleSet/DeclarationBlock.php | 11 ++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/Sabberworm/CSS/Property/Selector.php b/lib/Sabberworm/CSS/Property/Selector.php index d84171f5..69fa3f43 100644 --- a/lib/Sabberworm/CSS/Property/Selector.php +++ b/lib/Sabberworm/CSS/Property/Selector.php @@ -2,6 +2,8 @@ namespace Sabberworm\CSS\Property; +use Sabberworm\CSS\Parsing\UnexpectedTokenException; + /** * Class representing a single CSS selector. Selectors have to be split by the comma prior to being passed into this class. */ @@ -38,7 +40,15 @@ class Selector { private $sSelector; private $iSpecificity; + public static function isValid($sSelector) { + return preg_match("/^[a-zA-Z0-9\x{00A0}-\x{FFFF}_\=\"\'\~\[\]\(\)\-\s\.:#\+\>]*$/u", $sSelector); + } + public function __construct($sSelector, $bCalculateSpecificity = false) { + if (!Selector::isValid($sSelector)) { + preg_match("/[^a-zA-Z0-9\x{00A0}-\x{FFFF}_\=\"\'\~\[\]\(\)\-\s\.:#\+\>]/u", $sSelector, $matches); + throw new UnexpectedTokenException("Selector did not match '/[^a-zA-Z0-9\x{00A0}-\x{FFFF}_\=\"\'\~\[\]\(\)\-\s\.:#\+\>]/u'. ({$matches[0]} found).", $sSelector, "custom"); + } $this->setSelector($sSelector); if ($bCalculateSpecificity) { $this->getSpecificity(); diff --git a/lib/Sabberworm/CSS/RuleSet/DeclarationBlock.php b/lib/Sabberworm/CSS/RuleSet/DeclarationBlock.php index 6614b1d1..54e837fa 100644 --- a/lib/Sabberworm/CSS/RuleSet/DeclarationBlock.php +++ b/lib/Sabberworm/CSS/RuleSet/DeclarationBlock.php @@ -4,6 +4,7 @@ use Sabberworm\CSS\Parsing\ParserState; use Sabberworm\CSS\Parsing\OutputException; +use Sabberworm\CSS\Parsing\UnexpectedTokenException; use Sabberworm\CSS\Property\Selector; use Sabberworm\CSS\Rule\Rule; use Sabberworm\CSS\Value\RuleValueList; @@ -28,7 +29,15 @@ public function __construct($iLineNo = 0) { public static function parse(ParserState $oParserState) { $aComments = array(); $oResult = new DeclarationBlock($oParserState->currentLine()); - $oResult->setSelector($oParserState->consumeUntil('{', false, true, $aComments)); + try { + $oResult->setSelector($oParserState->consumeUntil('{', false, true, $aComments)); + } catch (UnexpectedTokenException $e) { + if($oParserState->getSettings()->bLenientParsing) { + return NULL; + } else { + throw $e; + } + } $oResult->setComments($aComments); RuleSet::parseRuleSet($oParserState, $oResult); return $oResult; From 218a207279195693440bc1dbc1b108031d20e45b Mon Sep 17 00:00:00 2001 From: Ivailo Hristov Date: Sun, 24 Feb 2019 17:36:06 +0200 Subject: [PATCH 02/14] Add | and * to the list of valid selector characters --- lib/Sabberworm/CSS/Property/Selector.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Sabberworm/CSS/Property/Selector.php b/lib/Sabberworm/CSS/Property/Selector.php index 69fa3f43..0efae0e7 100644 --- a/lib/Sabberworm/CSS/Property/Selector.php +++ b/lib/Sabberworm/CSS/Property/Selector.php @@ -41,12 +41,12 @@ class Selector { private $iSpecificity; public static function isValid($sSelector) { - return preg_match("/^[a-zA-Z0-9\x{00A0}-\x{FFFF}_\=\"\'\~\[\]\(\)\-\s\.:#\+\>]*$/u", $sSelector); + return preg_match("/^[a-zA-Z0-9\x{00A0}-\x{FFFF}_\|\*\=\"\'\~\[\]\(\)\-\s\.:#\+\>]*$/u", $sSelector); } public function __construct($sSelector, $bCalculateSpecificity = false) { if (!Selector::isValid($sSelector)) { - preg_match("/[^a-zA-Z0-9\x{00A0}-\x{FFFF}_\=\"\'\~\[\]\(\)\-\s\.:#\+\>]/u", $sSelector, $matches); + preg_match("/[^a-zA-Z0-9\x{00A0}-\x{FFFF}_\|\*\=\"\'\~\[\]\(\)\-\s\.:#\+\>]/u", $sSelector, $matches); throw new UnexpectedTokenException("Selector did not match '/[^a-zA-Z0-9\x{00A0}-\x{FFFF}_\=\"\'\~\[\]\(\)\-\s\.:#\+\>]/u'. ({$matches[0]} found).", $sSelector, "custom"); } $this->setSelector($sSelector); From 8792d007fb3b4503b8909b2fc59b8d1689d54c69 Mon Sep 17 00:00:00 2001 From: raxbg Date: Mon, 25 Feb 2019 08:47:47 +0200 Subject: [PATCH 03/14] Add ^ and $ to the list of valid selector characters (all special characters should be covered now) --- lib/Sabberworm/CSS/Property/Selector.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Sabberworm/CSS/Property/Selector.php b/lib/Sabberworm/CSS/Property/Selector.php index 0efae0e7..adab7e80 100644 --- a/lib/Sabberworm/CSS/Property/Selector.php +++ b/lib/Sabberworm/CSS/Property/Selector.php @@ -41,13 +41,13 @@ class Selector { private $iSpecificity; public static function isValid($sSelector) { - return preg_match("/^[a-zA-Z0-9\x{00A0}-\x{FFFF}_\|\*\=\"\'\~\[\]\(\)\-\s\.:#\+\>]*$/u", $sSelector); + return preg_match("/^[a-zA-Z0-9\x{00A0}-\x{FFFF}_\^\$\|\*\=\"\'\~\[\]\(\)\-\s\.:#\+\>]*$/u", $sSelector); } public function __construct($sSelector, $bCalculateSpecificity = false) { if (!Selector::isValid($sSelector)) { - preg_match("/[^a-zA-Z0-9\x{00A0}-\x{FFFF}_\|\*\=\"\'\~\[\]\(\)\-\s\.:#\+\>]/u", $sSelector, $matches); - throw new UnexpectedTokenException("Selector did not match '/[^a-zA-Z0-9\x{00A0}-\x{FFFF}_\=\"\'\~\[\]\(\)\-\s\.:#\+\>]/u'. ({$matches[0]} found).", $sSelector, "custom"); + preg_match("/[^a-zA-Z0-9\x{00A0}-\x{FFFF}_\^\$\|\*\=\"\'\~\[\]\(\)\-\s\.:#\+\>]/u", $sSelector, $matches); + throw new UnexpectedTokenException("Selector did not match '/^[a-zA-Z0-9\x{00A0}-\x{FFFF}_\^\$\|\*\=\"\'\~\[\]\(\)\-\s\.:#\+\>]*$/u'. ({$matches[0]} found).", $sSelector, "custom"); } $this->setSelector($sSelector); if ($bCalculateSpecificity) { From 15f1fdb31e5ffcf8c2bc4168879147852aa313bd Mon Sep 17 00:00:00 2001 From: raxbg Date: Mon, 25 Feb 2019 09:45:30 +0200 Subject: [PATCH 04/14] Skip parsing of the next ruleset upon finding an invalid selector instead aborting the parsing process. This seems to match the browsers' behavior --- lib/Sabberworm/CSS/RuleSet/DeclarationBlock.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/Sabberworm/CSS/RuleSet/DeclarationBlock.php b/lib/Sabberworm/CSS/RuleSet/DeclarationBlock.php index 54e837fa..d69a79ac 100644 --- a/lib/Sabberworm/CSS/RuleSet/DeclarationBlock.php +++ b/lib/Sabberworm/CSS/RuleSet/DeclarationBlock.php @@ -33,7 +33,8 @@ public static function parse(ParserState $oParserState) { $oResult->setSelector($oParserState->consumeUntil('{', false, true, $aComments)); } catch (UnexpectedTokenException $e) { if($oParserState->getSettings()->bLenientParsing) { - return NULL; + $oParserState->consumeUntil('}', false, true); + return false; } else { throw $e; } From 4f8da975aafad60cadc71dac70359f9330ca2702 Mon Sep 17 00:00:00 2001 From: raxbg Date: Mon, 25 Feb 2019 11:57:08 +0200 Subject: [PATCH 05/14] Update closing bracket parsing in a way which allows us to better match the browsers' parsing behavior --- lib/Sabberworm/CSS/CSSList/CSSList.php | 8 ++++++-- tests/Sabberworm/CSS/ParserTest.php | 2 +- ...ed-close-brackets.css => -unopened-close-brackets.css} | 0 3 files changed, 7 insertions(+), 3 deletions(-) rename tests/files/{unopened-close-brackets.css => -unopened-close-brackets.css} (100%) diff --git a/lib/Sabberworm/CSS/CSSList/CSSList.php b/lib/Sabberworm/CSS/CSSList/CSSList.php index d883df82..597c8c97 100644 --- a/lib/Sabberworm/CSS/CSSList/CSSList.php +++ b/lib/Sabberworm/CSS/CSSList/CSSList.php @@ -83,10 +83,8 @@ private static function parseListItem(ParserState $oParserState, CSSList $oList) } return $oAtRule; } else if ($oParserState->comes('}')) { - $oParserState->consume('}'); if ($bIsRoot) { if ($oParserState->getSettings()->bLenientParsing) { - while ($oParserState->comes('}')) $oParserState->consume('}'); return DeclarationBlock::parse($oParserState); } else { throw new SourceException("Unopened {", $oParserState->currentLine()); @@ -123,6 +121,9 @@ private static function parseAtRule(ParserState $oParserState) { $oResult->setVendorKeyFrame($sIdentifier); $oResult->setAnimationName(trim($oParserState->consumeUntil('{', false, true))); CSSList::parseList($oParserState, $oResult); + if ($oParserState->comes('}')) { + $oParserState->consume('}'); + } return $oResult; } else if ($sIdentifier === 'namespace') { $sPrefix = null; @@ -162,6 +163,9 @@ private static function parseAtRule(ParserState $oParserState) { } else { $oAtRule = new AtRuleBlockList($sIdentifier, $sArgs, $iIdentifierLineNum); CSSList::parseList($oParserState, $oAtRule); + if ($oParserState->comes('}')) { + $oParserState->consume('}'); + } } return $oAtRule; } diff --git a/tests/Sabberworm/CSS/ParserTest.php b/tests/Sabberworm/CSS/ParserTest.php index d9f3ec64..8970dca4 100644 --- a/tests/Sabberworm/CSS/ParserTest.php +++ b/tests/Sabberworm/CSS/ParserTest.php @@ -501,7 +501,7 @@ function testCharsetFailure2() { * @expectedException \Sabberworm\CSS\Parsing\SourceException */ function testUnopenedClosingBracketFailure() { - $this->parsedStructureForFile('unopened-close-brackets', Settings::create()->withLenientParsing(false)); + $this->parsedStructureForFile('-unopened-close-brackets', Settings::create()->withLenientParsing(false)); } /** diff --git a/tests/files/unopened-close-brackets.css b/tests/files/-unopened-close-brackets.css similarity index 100% rename from tests/files/unopened-close-brackets.css rename to tests/files/-unopened-close-brackets.css From a06e5bbf053e795832673c66a8623b872fa76b90 Mon Sep 17 00:00:00 2001 From: raxbg Date: Mon, 25 Feb 2019 12:37:04 +0200 Subject: [PATCH 06/14] Add unit test for invalid selectors --- tests/Sabberworm/CSS/ParserTest.php | 8 ++++++++ tests/files/invalid-selectors.css | 24 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 tests/files/invalid-selectors.css diff --git a/tests/Sabberworm/CSS/ParserTest.php b/tests/Sabberworm/CSS/ParserTest.php index 8970dca4..f0d8b863 100644 --- a/tests/Sabberworm/CSS/ParserTest.php +++ b/tests/Sabberworm/CSS/ParserTest.php @@ -432,6 +432,14 @@ function testUnmatchedBracesInFile() { $this->assertSame($sExpected, $oDoc->render()); } + function testInvalidSelectorsInFile() { + $oDoc = $this->parsedStructureForFile('invalid-selectors', Settings::create()->withMultibyteSupport(true)); + $sExpected = '@keyframes mymove {from {top: 0px;}} +#test {color: white;background: green;} +#test {display: block;background: white;color: black;}'; + $this->assertSame($sExpected, $oDoc->render()); + } + /** * @expectedException Sabberworm\CSS\Parsing\UnexpectedTokenException */ diff --git a/tests/files/invalid-selectors.css b/tests/files/invalid-selectors.css new file mode 100644 index 00000000..1c633c57 --- /dev/null +++ b/tests/files/invalid-selectors.css @@ -0,0 +1,24 @@ +@keyframes mymove { + from { top: 0px; } +} + +#test { + color: white; + background: green; +} + +body + background: black; + } + +#test { + display: block; + background: red; + color: white; +} +#test { + display: block; + background: white; + color: black; +} + From 4ea4fd6b852645f9f1154848de42eebbe54aada7 Mon Sep 17 00:00:00 2001 From: raxbg Date: Wed, 8 May 2019 15:14:41 +0300 Subject: [PATCH 07/14] Add percentage matching since keyframe steps are treated as regular selectors --- lib/Sabberworm/CSS/Property/Selector.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/Sabberworm/CSS/Property/Selector.php b/lib/Sabberworm/CSS/Property/Selector.php index adab7e80..c566eb71 100644 --- a/lib/Sabberworm/CSS/Property/Selector.php +++ b/lib/Sabberworm/CSS/Property/Selector.php @@ -41,13 +41,12 @@ class Selector { private $iSpecificity; public static function isValid($sSelector) { - return preg_match("/^[a-zA-Z0-9\x{00A0}-\x{FFFF}_\^\$\|\*\=\"\'\~\[\]\(\)\-\s\.:#\+\>]*$/u", $sSelector); + return preg_match("/^([a-zA-Z0-9\x{00A0}-\x{FFFF}_\^\$\|\*\=\"\'\~\[\]\(\)\-\s\.:#\+\>]*|\s*?[\+-]?\d+\%\s*)$/u", $sSelector); } public function __construct($sSelector, $bCalculateSpecificity = false) { if (!Selector::isValid($sSelector)) { - preg_match("/[^a-zA-Z0-9\x{00A0}-\x{FFFF}_\^\$\|\*\=\"\'\~\[\]\(\)\-\s\.:#\+\>]/u", $sSelector, $matches); - throw new UnexpectedTokenException("Selector did not match '/^[a-zA-Z0-9\x{00A0}-\x{FFFF}_\^\$\|\*\=\"\'\~\[\]\(\)\-\s\.:#\+\>]*$/u'. ({$matches[0]} found).", $sSelector, "custom"); + throw new UnexpectedTokenException("Selector did not match '/^([a-zA-Z0-9\x{00A0}-\x{FFFF}_\^\$\|\*\=\"\'\~\[\]\(\)\-\s\.:#\+\>]*|\s*?[\+-]?\d+\%\s*)$/u'.", $sSelector, "custom"); } $this->setSelector($sSelector); if ($bCalculateSpecificity) { From 134f4e62fe8ab9f316425f3c0f480b3f4f52d804 Mon Sep 17 00:00:00 2001 From: raxbg Date: Wed, 8 May 2019 18:55:45 +0300 Subject: [PATCH 08/14] Improved selector parsing + better handling for '}' --- lib/Sabberworm/CSS/CSSList/CSSList.php | 16 ++++++++++------ lib/Sabberworm/CSS/RuleSet/DeclarationBlock.php | 9 +++++++-- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/lib/Sabberworm/CSS/CSSList/CSSList.php b/lib/Sabberworm/CSS/CSSList/CSSList.php index 597c8c97..9fcb0faa 100644 --- a/lib/Sabberworm/CSS/CSSList/CSSList.php +++ b/lib/Sabberworm/CSS/CSSList/CSSList.php @@ -83,14 +83,18 @@ private static function parseListItem(ParserState $oParserState, CSSList $oList) } return $oAtRule; } else if ($oParserState->comes('}')) { - if ($bIsRoot) { - if ($oParserState->getSettings()->bLenientParsing) { - return DeclarationBlock::parse($oParserState); + if (!$oParserState->getSettings()->bLenientParsing) { + throw new UnexpectedTokenException('CSS selector', '}', 'identifier', $oParserState->currentLine()); + } else { + if ($bIsRoot) { + if ($oParserState->getSettings()->bLenientParsing) { + return DeclarationBlock::parse($oParserState); + } else { + throw new SourceException("Unopened {", $oParserState->currentLine()); + } } else { - throw new SourceException("Unopened {", $oParserState->currentLine()); + return null; } - } else { - return null; } } else { return DeclarationBlock::parse($oParserState); diff --git a/lib/Sabberworm/CSS/RuleSet/DeclarationBlock.php b/lib/Sabberworm/CSS/RuleSet/DeclarationBlock.php index d69a79ac..d8bb813c 100644 --- a/lib/Sabberworm/CSS/RuleSet/DeclarationBlock.php +++ b/lib/Sabberworm/CSS/RuleSet/DeclarationBlock.php @@ -30,10 +30,15 @@ public static function parse(ParserState $oParserState) { $aComments = array(); $oResult = new DeclarationBlock($oParserState->currentLine()); try { - $oResult->setSelector($oParserState->consumeUntil('{', false, true, $aComments)); + $oResult->setSelector($oParserState->consume(1) . $oParserState->consumeUntil(array('{', '}'), false, false, $aComments)); + if ($oParserState->comes('{')) { + $oParserState->consume(1); + } } catch (UnexpectedTokenException $e) { if($oParserState->getSettings()->bLenientParsing) { - $oParserState->consumeUntil('}', false, true); + if(!$oParserState->comes('}')) { + $oParserState->consumeUntil('}', false, true); + } return false; } else { throw $e; From 5b3f780f4c0e5563daf473d6e9e5bea4b4962f61 Mon Sep 17 00:00:00 2001 From: raxbg Date: Tue, 28 May 2019 19:19:46 +0300 Subject: [PATCH 09/14] More tests for invalid selectors --- tests/Sabberworm/CSS/ParserTest.php | 9 ++++++++ tests/files/invalid-selectors-2.css | 33 +++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 tests/files/invalid-selectors-2.css diff --git a/tests/Sabberworm/CSS/ParserTest.php b/tests/Sabberworm/CSS/ParserTest.php index f0d8b863..67700e1c 100644 --- a/tests/Sabberworm/CSS/ParserTest.php +++ b/tests/Sabberworm/CSS/ParserTest.php @@ -438,6 +438,15 @@ function testInvalidSelectorsInFile() { #test {color: white;background: green;} #test {display: block;background: white;color: black;}'; $this->assertSame($sExpected, $oDoc->render()); + + $oDoc = $this->parsedStructureForFile('invalid-selectors-2', Settings::create()->withMultibyteSupport(true)); + $sExpected = '@media only screen and (max-width: 1215px) {.breadcrumb {padding-left: 10px;} + .super-menu > li:first-of-type {border-left-width: 0;} + .super-menu > li:last-of-type {border-right-width: 0;} + html[dir="rtl"] .super-menu > li:first-of-type {border-left-width: 1px;border-right-width: 0;} + html[dir="rtl"] .super-menu > li:last-of-type {border-left-width: 0;}} +body {background-color: red;}'; + $this->assertSame($sExpected, $oDoc->render()); } /** diff --git a/tests/files/invalid-selectors-2.css b/tests/files/invalid-selectors-2.css new file mode 100644 index 00000000..3398c62e --- /dev/null +++ b/tests/files/invalid-selectors-2.css @@ -0,0 +1,33 @@ +@media only screen and (max-width: 1215px) { +.breadcrumb{ +padding-left:10px; +} +.super-menu > li:first-of-type{ +border-left-width:0; +} +.super-menu > li:last-of-type{ +border-right-width:0; +} +html[dir="rtl"] .super-menu > li:first-of-type{ +border-left-width:1px; +border-right-width:0; +} +html[dir="rtl"] .super-menu > li:last-of-type{ +border-left-width:0; +} +html[dir="rtl"] .super-menu.menu-floated > li:first-of-type +border-right-width:0; +} +} + + +.super-menu.menu-floated{ +border-right-width:1px; +border-left-width:1px; +border-color:rgb(90, 66, 66); +border-style:dotted; +} + +body { + background-color: red; +} From 425c78e89b3236e93d11410afc3a58a370c48875 Mon Sep 17 00:00:00 2001 From: raxbg Date: Wed, 26 Jun 2019 19:11:11 +0300 Subject: [PATCH 10/14] Improvement: Handle escaped characters when validating selectors --- lib/Sabberworm/CSS/Property/Selector.php | 8 ++++++-- tests/Sabberworm/CSS/ParserTest.php | 16 ++++++++++++++++ tests/files/selector-escapes.css | 7 +++++++ 3 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 tests/files/selector-escapes.css diff --git a/lib/Sabberworm/CSS/Property/Selector.php b/lib/Sabberworm/CSS/Property/Selector.php index c566eb71..6c9c5df7 100644 --- a/lib/Sabberworm/CSS/Property/Selector.php +++ b/lib/Sabberworm/CSS/Property/Selector.php @@ -37,16 +37,20 @@ class Selector { )) /ix'; + const SELECTOR_VALIDATION_RX = '/ + ^((?:[a-zA-Z0-9\x{00A0}-\x{FFFF}_\^\$\|\*\=\"\'\~\[\]\(\)\-\s\.:#\+\>]*(?:\\\\.)?)*|\s*?[\+-]?\d+\%\s*)$ + /ux'; + private $sSelector; private $iSpecificity; public static function isValid($sSelector) { - return preg_match("/^([a-zA-Z0-9\x{00A0}-\x{FFFF}_\^\$\|\*\=\"\'\~\[\]\(\)\-\s\.:#\+\>]*|\s*?[\+-]?\d+\%\s*)$/u", $sSelector); + return preg_match(self::SELECTOR_VALIDATION_RX, $sSelector); } public function __construct($sSelector, $bCalculateSpecificity = false) { if (!Selector::isValid($sSelector)) { - throw new UnexpectedTokenException("Selector did not match '/^([a-zA-Z0-9\x{00A0}-\x{FFFF}_\^\$\|\*\=\"\'\~\[\]\(\)\-\s\.:#\+\>]*|\s*?[\+-]?\d+\%\s*)$/u'.", $sSelector, "custom"); + throw new UnexpectedTokenException("Selector did not match '{self::SELECTOR_VALIDATION_RX}'.", $sSelector, "custom"); } $this->setSelector($sSelector); if ($bCalculateSpecificity) { diff --git a/tests/Sabberworm/CSS/ParserTest.php b/tests/Sabberworm/CSS/ParserTest.php index 67700e1c..6f7df8fe 100644 --- a/tests/Sabberworm/CSS/ParserTest.php +++ b/tests/Sabberworm/CSS/ParserTest.php @@ -449,6 +449,22 @@ function testInvalidSelectorsInFile() { $this->assertSame($sExpected, $oDoc->render()); } + function testSelectorEscapesInFile() { + $oDoc = $this->parsedStructureForFile('selector-escapes', Settings::create()->withMultibyteSupport(true)); + $sExpected = '#\# {color: red;} +.col-sm-1\/5 {width: 20%;}'; + $this->assertSame($sExpected, $oDoc->render()); + + $oDoc = $this->parsedStructureForFile('invalid-selectors-2', Settings::create()->withMultibyteSupport(true)); + $sExpected = '@media only screen and (max-width: 1215px) {.breadcrumb {padding-left: 10px;} + .super-menu > li:first-of-type {border-left-width: 0;} + .super-menu > li:last-of-type {border-right-width: 0;} + html[dir="rtl"] .super-menu > li:first-of-type {border-left-width: 1px;border-right-width: 0;} + html[dir="rtl"] .super-menu > li:last-of-type {border-left-width: 0;}} +body {background-color: red;}'; + $this->assertSame($sExpected, $oDoc->render()); + } + /** * @expectedException Sabberworm\CSS\Parsing\UnexpectedTokenException */ diff --git a/tests/files/selector-escapes.css b/tests/files/selector-escapes.css new file mode 100644 index 00000000..7797e06f --- /dev/null +++ b/tests/files/selector-escapes.css @@ -0,0 +1,7 @@ +#\# { + color: red; +} + +.col-sm-1\/5 { + width: 20%; +} From 75f7b13b8f70468097fa2d6737055b68252ab702 Mon Sep 17 00:00:00 2001 From: raxbg Date: Fri, 12 Jul 2019 20:08:43 +0300 Subject: [PATCH 11/14] Match selectors with comments or quoted strings --- lib/Sabberworm/CSS/Property/Selector.php | 4 ++-- lib/Sabberworm/CSS/RuleSet/DeclarationBlock.php | 14 +++++++++++++- tests/Sabberworm/CSS/ParserTest.php | 8 ++++++++ tests/files/-tobedone.css | 9 --------- 4 files changed, 23 insertions(+), 12 deletions(-) delete mode 100644 tests/files/-tobedone.css diff --git a/lib/Sabberworm/CSS/Property/Selector.php b/lib/Sabberworm/CSS/Property/Selector.php index 6c9c5df7..6e731db6 100644 --- a/lib/Sabberworm/CSS/Property/Selector.php +++ b/lib/Sabberworm/CSS/Property/Selector.php @@ -38,7 +38,7 @@ class Selector { /ix'; const SELECTOR_VALIDATION_RX = '/ - ^((?:[a-zA-Z0-9\x{00A0}-\x{FFFF}_\^\$\|\*\=\"\'\~\[\]\(\)\-\s\.:#\+\>]*(?:\\\\.)?)*|\s*?[\+-]?\d+\%\s*)$ + ^((?:[a-zA-Z0-9\x{00A0}-\x{FFFF}_\^\$\|\*\=\"\'\~\[\]\(\)\-\s\.:#\+\>]*(?:\\\\.)?(?:\'.*?\')?(?:\".*?\")?)*|\s*?[\+-]?\d+\%\s*)$ /ux'; private $sSelector; @@ -50,7 +50,7 @@ public static function isValid($sSelector) { public function __construct($sSelector, $bCalculateSpecificity = false) { if (!Selector::isValid($sSelector)) { - throw new UnexpectedTokenException("Selector did not match '{self::SELECTOR_VALIDATION_RX}'.", $sSelector, "custom"); + throw new UnexpectedTokenException("Selector did not match '" . self::SELECTOR_VALIDATION_RX . "'.", $sSelector, "custom"); } $this->setSelector($sSelector); if ($bCalculateSpecificity) { diff --git a/lib/Sabberworm/CSS/RuleSet/DeclarationBlock.php b/lib/Sabberworm/CSS/RuleSet/DeclarationBlock.php index d8bb813c..03d6506d 100644 --- a/lib/Sabberworm/CSS/RuleSet/DeclarationBlock.php +++ b/lib/Sabberworm/CSS/RuleSet/DeclarationBlock.php @@ -30,7 +30,19 @@ public static function parse(ParserState $oParserState) { $aComments = array(); $oResult = new DeclarationBlock($oParserState->currentLine()); try { - $oResult->setSelector($oParserState->consume(1) . $oParserState->consumeUntil(array('{', '}'), false, false, $aComments)); + $aSelectorParts = array(); + $sStringWrapperChar = false; + do { + $aSelectorParts[] = $oParserState->consume(1) . $oParserState->consumeUntil(array('{', '}', '\'', '"'), false, false, $aComments); + if ( in_array($oParserState->peek(), array('\'', '"')) && substr(end($aSelectorParts), -1) != "\\" ) { + if ( $sStringWrapperChar === false ) { + $sStringWrapperChar = $oParserState->peek(); + } else if ($sStringWrapperChar == $oParserState->peek()) { + $sStringWrapperChar = false; + } + } + } while (!in_array($oParserState->peek(), array('{', '}')) || $sStringWrapperChar !== false); + $oResult->setSelector(implode('', $aSelectorParts)); if ($oParserState->comes('{')) { $oParserState->consume(1); } diff --git a/tests/Sabberworm/CSS/ParserTest.php b/tests/Sabberworm/CSS/ParserTest.php index 6f7df8fe..d7e6ce69 100644 --- a/tests/Sabberworm/CSS/ParserTest.php +++ b/tests/Sabberworm/CSS/ParserTest.php @@ -465,6 +465,14 @@ function testSelectorEscapesInFile() { $this->assertSame($sExpected, $oDoc->render()); } + function testSelectorIgnoresInFile() { + $oDoc = $this->parsedStructureForFile('selector-ignores', Settings::create()->withMultibyteSupport(true)); + $sExpected = '.some[selectors-may=\'contain-a-{\'] {} +.this-selector .valid {width: 100px;} +@media only screen and (min-width: 200px) {.test {prop: val;}}'; + $this->assertSame($sExpected, $oDoc->render()); + } + /** * @expectedException Sabberworm\CSS\Parsing\UnexpectedTokenException */ diff --git a/tests/files/-tobedone.css b/tests/files/-tobedone.css deleted file mode 100644 index d9fc1117..00000000 --- a/tests/files/-tobedone.css +++ /dev/null @@ -1,9 +0,0 @@ -.some[selectors-may='contain-a-{'] { - -} - -@media only screen and (min-width: 200px) { - .test { - prop: val; - } -} \ No newline at end of file From 397ca39b0e8e17a488f00a2a549eca8b88eb280b Mon Sep 17 00:00:00 2001 From: Ivailo Hristov Date: Fri, 12 Jul 2019 22:13:35 +0300 Subject: [PATCH 12/14] Add forgotten test file --- tests/files/selector-ignores.css | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 tests/files/selector-ignores.css diff --git a/tests/files/selector-ignores.css b/tests/files/selector-ignores.css new file mode 100644 index 00000000..5834e009 --- /dev/null +++ b/tests/files/selector-ignores.css @@ -0,0 +1,13 @@ +.some[selectors-may='contain-a-{'] { + +} + +.this-selector /* should remain-} */ .valid { + width:100px; +} + +@media only screen and (min-width: 200px) { + .test { + prop: val; + } +} From 2943d9f1206c9e9436a2005a8c51d588a994e35b Mon Sep 17 00:00:00 2001 From: Ivailo Hristov Date: Sat, 13 Jul 2019 21:18:24 +0300 Subject: [PATCH 13/14] Move selector validation outside of the Selector's constructor --- lib/Sabberworm/CSS/CSSList/CSSList.php | 3 +++ lib/Sabberworm/CSS/Property/Selector.php | 3 --- lib/Sabberworm/CSS/RuleSet/DeclarationBlock.php | 3 +++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/Sabberworm/CSS/CSSList/CSSList.php b/lib/Sabberworm/CSS/CSSList/CSSList.php index 9fcb0faa..5def89a0 100644 --- a/lib/Sabberworm/CSS/CSSList/CSSList.php +++ b/lib/Sabberworm/CSS/CSSList/CSSList.php @@ -272,6 +272,9 @@ public function removeDeclarationBlockBySelector($mSelector, $bRemoveAll = false } foreach ($mSelector as $iKey => &$mSel) { if (!($mSel instanceof Selector)) { + if (!Selector::isValid($mSel)) { + throw new UnexpectedTokenException("Selector did not match '" . Selector::SELECTOR_VALIDATION_RX . "'.", $mSel, "custom"); + } $mSel = new Selector($mSel); } } diff --git a/lib/Sabberworm/CSS/Property/Selector.php b/lib/Sabberworm/CSS/Property/Selector.php index 6e731db6..654d1007 100644 --- a/lib/Sabberworm/CSS/Property/Selector.php +++ b/lib/Sabberworm/CSS/Property/Selector.php @@ -49,9 +49,6 @@ public static function isValid($sSelector) { } public function __construct($sSelector, $bCalculateSpecificity = false) { - if (!Selector::isValid($sSelector)) { - throw new UnexpectedTokenException("Selector did not match '" . self::SELECTOR_VALIDATION_RX . "'.", $sSelector, "custom"); - } $this->setSelector($sSelector); if ($bCalculateSpecificity) { $this->getSpecificity(); diff --git a/lib/Sabberworm/CSS/RuleSet/DeclarationBlock.php b/lib/Sabberworm/CSS/RuleSet/DeclarationBlock.php index 03d6506d..08c3b0fd 100644 --- a/lib/Sabberworm/CSS/RuleSet/DeclarationBlock.php +++ b/lib/Sabberworm/CSS/RuleSet/DeclarationBlock.php @@ -70,6 +70,9 @@ public function setSelectors($mSelector) { } foreach ($this->aSelectors as $iKey => $mSelector) { if (!($mSelector instanceof Selector)) { + if (!Selector::isValid($mSelector)) { + throw new UnexpectedTokenException("Selector did not match '" . Selector::SELECTOR_VALIDATION_RX . "'.", $mSelector, "custom"); + } $this->aSelectors[$iKey] = new Selector($mSelector); } } From a27e301ec864cc1d7569e3d8ab06221d3322be05 Mon Sep 17 00:00:00 2001 From: Ivailo Hristov Date: Sat, 13 Jul 2019 21:54:09 +0300 Subject: [PATCH 14/14] Simplify the CSS validating regex --- lib/Sabberworm/CSS/Property/Selector.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/Sabberworm/CSS/Property/Selector.php b/lib/Sabberworm/CSS/Property/Selector.php index 654d1007..745a4b29 100644 --- a/lib/Sabberworm/CSS/Property/Selector.php +++ b/lib/Sabberworm/CSS/Property/Selector.php @@ -38,7 +38,13 @@ class Selector { /ix'; const SELECTOR_VALIDATION_RX = '/ - ^((?:[a-zA-Z0-9\x{00A0}-\x{FFFF}_\^\$\|\*\=\"\'\~\[\]\(\)\-\s\.:#\+\>]*(?:\\\\.)?(?:\'.*?\')?(?:\".*?\")?)*|\s*?[\+-]?\d+\%\s*)$ + ^( + (?: + [a-zA-Z0-9\x{00A0}-\x{FFFF}_^$|*="\'~\[\]()\-\s\.:#+>]* # any sequence of valid unescaped characters + (?:\\\\.)? # a single escaped character + (?:([\'"]).*?(?