Forráskód Böngészése

LibWeb/CSS: Disallow :has() and pseudo-elements in :has() when parsing

Sam Atkins 8 hónapja
szülő
commit
7f803c5c3d

+ 2 - 0
Libraries/LibWeb/CSS/Parser/Parser.h

@@ -413,6 +413,8 @@ private:
     };
     static ContextType context_type_for_at_rule(FlyString const&);
     Vector<ContextType> m_rule_context;
+
+    Vector<PseudoClass> m_pseudo_class_context; // Stack of pseudo-class functions we're currently inside
 };
 
 }

+ 24 - 0
Libraries/LibWeb/CSS/Parser/SelectorParsing.cpp

@@ -411,6 +411,11 @@ Parser::ParseErrorOr<Selector::SimpleSelector> Parser::parse_pseudo_simple_selec
 
         // Note: We allow the "ignored" -webkit prefix here for -webkit-progress-bar/-webkit-progress-bar
         if (auto pseudo_element = Selector::PseudoElement::from_string(pseudo_name); pseudo_element.has_value()) {
+            // :has() is fussy about pseudo-elements inside it
+            if (m_pseudo_class_context.contains_slow(PseudoClass::Has) && !is_has_allowed_pseudo_element(pseudo_element->type())) {
+                return ParseError::SyntaxError;
+            }
+
             return Selector::SimpleSelector {
                 .type = Selector::SimpleSelector::Type::PseudoElement,
                 .value = pseudo_element.release_value()
@@ -423,6 +428,10 @@ Parser::ParseErrorOr<Selector::SimpleSelector> Parser::parse_pseudo_simple_selec
         // valid at parse time, but ::-webkit-jkl() is not.) If they’re not otherwise recognized and supported, they
         // must be treated as matching nothing, and are unknown -webkit- pseudo-elements.
         if (pseudo_name.starts_with_bytes("-webkit-"sv, CaseSensitivity::CaseInsensitive)) {
+            // :has() only allows a limited set of pseudo-elements inside it, which doesn't include unknown ones.
+            if (m_pseudo_class_context.contains_slow(PseudoClass::Has))
+                return ParseError::SyntaxError;
+
             return Selector::SimpleSelector {
                 .type = Selector::SimpleSelector::Type::PseudoElement,
                 // Unknown -webkit- pseudo-elements must be serialized in ASCII lowercase.
@@ -470,6 +479,11 @@ Parser::ParseErrorOr<Selector::SimpleSelector> Parser::parse_pseudo_simple_selec
             case Selector::PseudoElement::Type::Before:
             case Selector::PseudoElement::Type::FirstLetter:
             case Selector::PseudoElement::Type::FirstLine:
+                // :has() is fussy about pseudo-elements inside it
+                if (m_pseudo_class_context.contains_slow(PseudoClass::Has) && !is_has_allowed_pseudo_element(pseudo_element->type())) {
+                    return ParseError::SyntaxError;
+                }
+
                 return Selector::SimpleSelector {
                     .type = Selector::SimpleSelector::Type::PseudoElement,
                     .value = pseudo_element.value()
@@ -545,6 +559,16 @@ Parser::ParseErrorOr<Selector::SimpleSelector> Parser::parse_pseudo_simple_selec
             return ParseError::SyntaxError;
         }
 
+        // "The :has() pseudo-class cannot be nested; :has() is not valid within :has()."
+        // https://drafts.csswg.org/selectors/#relational
+        if (pseudo_class == PseudoClass::Has && m_pseudo_class_context.contains_slow(PseudoClass::Has)) {
+            dbgln_if(CSS_PARSER_DEBUG, ":has() is not allowed inside :has()");
+            return ParseError::SyntaxError;
+        }
+
+        m_pseudo_class_context.append(pseudo_class);
+        ScopeGuard guard = [&] { m_pseudo_class_context.take_last(); };
+
         switch (metadata.parameter_type) {
         case PseudoClassMetadata::ParameterType::ANPlusB:
             return parse_nth_child_selector(pseudo_class, pseudo_function.value, false);

+ 7 - 0
Libraries/LibWeb/CSS/Selector.cpp

@@ -687,4 +687,11 @@ SelectorList adapt_nested_relative_selector_list(SelectorList const& selectors)
     return new_list;
 }
 
+// https://drafts.csswg.org/selectors/#has-allowed-pseudo-element
+bool is_has_allowed_pseudo_element(Selector::PseudoElement::Type)
+{
+    // No spec currently defines any pseudo-elements that are allowed in :has()
+    return false;
+}
+
 }

+ 2 - 0
Libraries/LibWeb/CSS/Selector.h

@@ -275,6 +275,8 @@ String serialize_a_group_of_selectors(SelectorList const& selectors);
 
 SelectorList adapt_nested_relative_selector_list(SelectorList const&);
 
+bool is_has_allowed_pseudo_element(Selector::PseudoElement::Type);
+
 }
 
 namespace AK {

+ 5 - 0
Tests/LibWeb/Text/expected/css/invalid-selector-in-has.txt

@@ -1 +1,6 @@
 :has(:yakthonk) should be invalid: PASS
+:has(:not(:yakthonk)) should be invalid: PASS
+:has(:has(span)) should be invalid: PASS
+:has(:not(:has(span))) should be invalid: PASS
+:has(::before) should be invalid: PASS
+:has(:not(::before)) should be invalid: PASS

+ 5 - 0
Tests/LibWeb/Text/input/css/invalid-selector-in-has.html

@@ -5,6 +5,11 @@
     test(() => {
         let selectors = [
             ":has(:yakthonk)",
+            ":has(:not(:yakthonk))",
+            ":has(:has(span))",
+            ":has(:not(:has(span)))",
+            ":has(::before)",
+            ":has(:not(::before))",
         ];
 
         let style = document.getElementById("style");