LibWeb: Treat CSS selectors containing undeclared namespaces as invalid

Selectors containing undeclared namespaces should be considered invalid,
not just not matching any elements.

Gains us 3 new WPT passes.
This commit is contained in:
Callum Law 2025-06-23 22:40:37 +12:00 committed by Sam Atkins
commit 6584ae0080
Notes: github-actions[bot] 2025-06-24 11:52:28 +00:00
17 changed files with 157 additions and 11 deletions

View file

@ -47,7 +47,7 @@ WebIDL::ExceptionOr<u32> CSSGroupingRule::insert_rule(StringView rule, u32 index
{
// The insertRule(rule, index) method must return the result of invoking insert a CSS rule rule into the child CSS
// rules at index, with the nested flag set.
TRY(m_rules->insert_a_css_rule(rule, index, CSSRuleList::Nested::Yes));
TRY(m_rules->insert_a_css_rule(rule, index, CSSRuleList::Nested::Yes, m_parent_style_sheet->declared_namespaces()));
// AD-HOC: The spec doesn't say where to set the parent rule, so we'll do it here.
m_rules->item(index)->set_parent_rule(this);

View file

@ -49,8 +49,9 @@ void CSSRuleList::visit_edges(Cell::Visitor& visitor)
visitor.visit(m_owner_rule);
}
// AD-HOC: The spec doesn't include a declared_namespaces parameter, but we need it to handle parsing of namespaced selectors.
// https://drafts.csswg.org/cssom/#insert-a-css-rule
WebIDL::ExceptionOr<unsigned> CSSRuleList::insert_a_css_rule(Variant<StringView, CSSRule*> rule, u32 index, Nested nested)
WebIDL::ExceptionOr<unsigned> CSSRuleList::insert_a_css_rule(Variant<StringView, CSSRule*> rule, u32 index, Nested nested, HashTable<FlyString> const& declared_namespaces)
{
// 1. Set length to the number of items in list.
auto length = m_rules.size();
@ -63,9 +64,13 @@ WebIDL::ExceptionOr<unsigned> CSSRuleList::insert_a_css_rule(Variant<StringView,
// NOTE: The insert-a-css-rule spec expects `rule` to be a string, but the CSSStyleSheet.insertRule()
// spec calls this algorithm with an already-parsed CSSRule. So, we use a Variant and skip step 3
// if that variant holds a CSSRule already.
CSSRule* new_rule = nullptr;
if (rule.has<StringView>()) {
new_rule = parse_css_rule(Parser::ParsingParams { realm() }, rule.get<StringView>());
Parser::ParsingParams parsing_params { realm() };
parsing_params.declared_namespaces = declared_namespaces;
new_rule = parse_css_rule(parsing_params, rule.get<StringView>());
} else {
new_rule = rule.get<CSSRule*>();
}
@ -74,6 +79,8 @@ WebIDL::ExceptionOr<unsigned> CSSRuleList::insert_a_css_rule(Variant<StringView,
if (!new_rule && nested == Nested::Yes) {
Parser::ParsingParams parsing_params { realm() };
parsing_params.rule_context = rule_context();
parsing_params.declared_namespaces = declared_namespaces;
// - Set declarations to the results of performing parse a CSS declaration block, on argument rule.
auto declarations = parse_css_property_declaration_block(parsing_params, rule.get<StringView>());

View file

@ -57,7 +57,7 @@ public:
No,
Yes,
};
WebIDL::ExceptionOr<unsigned> insert_a_css_rule(Variant<StringView, CSSRule*>, u32 index, Nested = Nested::No);
WebIDL::ExceptionOr<unsigned> insert_a_css_rule(Variant<StringView, CSSRule*>, u32 index, Nested, HashTable<FlyString> const& declared_namespaces);
void for_each_effective_rule(TraversalOrder, Function<void(CSSRule const&)> const& callback) const;
// Returns whether the match state of any media queries changed after evaluation.

View file

@ -126,12 +126,17 @@ void CSSStyleRule::set_selector_text(StringView selector_text)
clear_caches();
// 1. Run the parse a group of selectors algorithm on the given value.
Parser::ParsingParams parsing_params { realm() };
if (m_parent_style_sheet)
parsing_params.declared_namespaces = m_parent_style_sheet->declared_namespaces();
Optional<SelectorList> parsed_selectors;
if (parent_style_rule()) {
// AD-HOC: If we're a nested style rule, then we need to parse the selector as relative and then adapt it with implicit &s.
parsed_selectors = parse_selector_for_nested_style_rule(Parser::ParsingParams { realm() }, selector_text);
parsed_selectors = parse_selector_for_nested_style_rule(parsing_params, selector_text);
} else {
parsed_selectors = parse_selector(Parser::ParsingParams { realm() }, selector_text);
parsed_selectors = parse_selector(parsing_params, selector_text);
}
// 2. If the algorithm returns a non-null value replace the associated group of selectors with the returned value.

View file

@ -151,7 +151,7 @@ WebIDL::ExceptionOr<unsigned> CSSStyleSheet::insert_rule(StringView rule, unsign
return WebIDL::SyntaxError::create(realm(), "Can't insert @import rules into a constructed stylesheet."_string);
// 6. Return the result of invoking insert a CSS rule rule in the CSS rules at index.
auto result = m_rules->insert_a_css_rule(parsed_rule, index);
auto result = m_rules->insert_a_css_rule(parsed_rule, index, CSSRuleList::Nested::No, declared_namespaces());
if (!result.is_exception()) {
// NOTE: The spec doesn't say where to set the parent style sheet, so we'll do it here.
@ -375,6 +375,17 @@ Optional<FlyString> CSSStyleSheet::default_namespace() const
return {};
}
HashTable<FlyString> CSSStyleSheet::declared_namespaces() const
{
HashTable<FlyString> declared_namespaces;
for (auto namespace_ : m_namespace_rules.keys()) {
declared_namespaces.set(namespace_);
}
return declared_namespaces;
}
Optional<FlyString> CSSStyleSheet::namespace_uri(StringView namespace_prefix) const
{
return m_namespace_rules.get(namespace_prefix)
@ -444,9 +455,14 @@ bool CSSStyleSheet::has_associated_font_loader(FontLoader& font_loader) const
Parser::ParsingParams CSSStyleSheet::make_parsing_params() const
{
Parser::ParsingParams parsing_params;
if (auto document = owning_document())
return Parser::ParsingParams { *document };
return Parser::ParsingParams { realm() };
parsing_params = Parser::ParsingParams { *document };
else
parsing_params = Parser::ParsingParams { realm() };
parsing_params.declared_namespaces = declared_namespaces();
return parsing_params;
}
}

View file

@ -72,6 +72,7 @@ public:
Optional<FlyString> default_namespace() const;
GC::Ptr<CSSNamespaceRule> default_namespace_rule() const { return m_default_namespace_rule; }
HashTable<FlyString> declared_namespaces() const;
Optional<FlyString> namespace_uri(StringView namespace_prefix) const;

View file

@ -66,6 +66,7 @@ Parser::Parser(ParsingParams const& context, Vector<Token> tokens)
, m_tokens(move(tokens))
, m_token_stream(m_tokens)
, m_rule_context(move(context.rule_context))
, m_declared_namespaces(move(context.declared_namespaces))
{
}
@ -141,6 +142,8 @@ GC::RootVector<GC::Ref<CSSRule>> Parser::convert_rules(Vector<Rule> const& raw_r
if (!namespace_rules_valid)
continue;
m_declared_namespaces.set(as<CSSNamespaceRule>(*rule).prefix());
break;
default:
import_rules_valid = false;

View file

@ -81,6 +81,7 @@ struct ParsingParams {
ParsingMode mode { ParsingMode::Normal };
Vector<RuleContext> rule_context;
HashTable<FlyString> declared_namespaces;
};
// The very large CSS Parser implementation code is broken up among several .cpp files:
@ -543,6 +544,7 @@ private:
bool context_allows_quirky_length() const;
Vector<RuleContext> m_rule_context;
HashTable<FlyString> m_declared_namespaces;
Vector<PseudoClass> m_pseudo_class_context; // Stack of pseudo-class functions we're currently inside
};

View file

@ -258,8 +258,10 @@ Optional<Selector::SimpleSelector::QualifiedName> Parser::parse_selector_qualifi
? Selector::SimpleSelector::QualifiedName::NamespaceType::Any
: Selector::SimpleSelector::QualifiedName::NamespaceType::Named;
// FIXME: https://www.w3.org/TR/selectors-4/#invalid
// - a simple selector containing an undeclared namespace prefix is invalid
// https://www.w3.org/TR/selectors-4/#invalid
// a simple selector containing an undeclared namespace prefix is invalid
if (namespace_type == Selector::SimpleSelector::QualifiedName::NamespaceType::Named && !m_declared_namespaces.contains(namespace_))
return {};
transaction.commit();
return Selector::SimpleSelector::QualifiedName {

View file

@ -0,0 +1,10 @@
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<link rel="author" title="L. David Baron" href="https://dbaron.org/"/>
<link rel="author" title="Mozilla" href="http://mozilla.org/"/>
<title>CSS Namespaces Test Suite reference</title>
</head>
<body>
<p><test style="background: lime">This sentence should have a green background.</test></p>
</body>
</html>

View file

@ -0,0 +1,38 @@
<!doctype html>
<head>
<title>CSS Test (Conditional Rules): Invalid rules after @supports</title>
<!-- This test is paired with at-media-003.html ; please keep them in sync -->
<link rel="author" title="Elika J. Etemad" href="http://fantasai.inkedblade.net/contact">
<link rel="help" href="https://www.w3.org/TR/css-conditional-3/#contents-of">
<link rel="match" href="../../../../expected/wpt-import/css/css-conditional/at-supports-001-ref.html">
<meta name="assert"
content="Test passes if rules required to occur before all style rules are invalid after @supports.">
<style>
@namespace x "http://www.w3.org/";
@supports (background: blue) { /* invalidates later rules even if empty */ }
@import "support/fail.css";
@namespace y "http://www.w3.org/";
.test1, x|div { background: green; }
.test1, y|div { background: red; }
div {
background: red;
height: 50px;
width: 100px;
}
</style>
<style>
@supports (background: blue) {
/* @supports isn't invalidated, only misordered stuff after it is */
.test2 { background: green; }
}
@import "support/fail.css";
</style>
</head>
<body>
<p>Test passes if there is a <strong>filled green square</strong> and <strong>no red</strong>.</p>
<div class="test1"></div>
<div class="test2"></div>
</body>
</html>

View file

@ -0,0 +1 @@
* { background:red ! important }

View file

@ -0,0 +1,21 @@
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<link rel="author" title="Anne van Kesteren" href="http://annevankesteren.nl/"/>
<link rel="author" title="Opera Software ASA" href="http://opera.com/"/>
<link rel="help" href="http://www.w3.org/TR/css-namespaces-3/#scope"/>
<link rel="match" href="../../../../expected/wpt-import/css/css-namespaces/reference/ref-lime-1.xml"/>
<title>CSS Namespaces Test Suite: scope @import</title>
<style>
test { background:lime }
</style>
<style>
@import url("support/scope-002a.css");
@import url("support/scope-002b.css");
@namespace w "test";
x|test { background:red }
</style>
</head>
<body>
<p><test xmlns="test">This sentence should have a green background.</test></p>
</body>
</html>

View file

@ -0,0 +1,3 @@
@namespace x url("test");
@namespace y url("test");
w|test { background:red }

View file

@ -0,0 +1,2 @@
y|test { background: red }

View file

@ -0,0 +1,6 @@
Harness status: OK
Found 1 tests
1 Pass
Pass CSS Test: @namespace in CSSOM is not severely broken

View file

@ -0,0 +1,29 @@
<!doctype html>
<title>CSS Test: @namespace in CSSOM is not severely broken</title>
<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1464865">
<link rel="help" href="https://drafts.csswg.org/cssom/#insert-a-css-rule">
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<style id="s">
div { color: green }
</style>
<div>Should be green</div>
<script>
test(function() {
assert_throws_dom("InvalidStateError", function() {
s.sheet.insertRule('@namespace myhtml url("http://www.w3.org/1999/xhtml")', 0);
});
assert_equals(s.sheet.cssRules.length, 1, "Shouldn't have been inserted");
assert_throws_dom("SyntaxError", function() {
s.sheet.insertRule("myhtml|div { color: red !important }", 0);
});
assert_equals(s.sheet.cssRules.length, 1);
assert_equals(
getComputedStyle(document.querySelector("div")).color,
"rgb(0, 128, 0)",
"Namespace shouldn't be registered"
);
});
</script>