LibWeb: Don't construct stylesheet when modifying CSSStyleSheet rules

Previously, `CSSStyleSheet.replace()` and `CSSStyleSheet.replaceSync()`
parsed the given CSS text into a temporary stylesheet object, from
which a list of rules was extracted. Doing this had the unintended
side-effect that a fetch request would be started if the given CSS text
referenced any external resources. This fetch request would cause a
crash, since the temporary stylesheet object didn't set the constructed
flag, or constructor document. We now parse the given CSS text as a
list of rules without constructing a temporary stylesheet.
This commit is contained in:
Tim Ledbetter 2025-04-15 18:01:54 +01:00
parent edaac0f2ee
commit 69c3a1070f
5 changed files with 95 additions and 6 deletions

View file

@ -1,7 +1,7 @@
/*
* Copyright (c) 2019-2022, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2022-2024, Sam Atkins <sam@ladybird.org>
* Copyright (c) 2024, Tim Ledbetter <timledbetter@gmail.com>
* Copyright (c) 2024-2025, Tim Ledbetter <tim.ledbetter@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -207,8 +207,7 @@ GC::Ref<WebIDL::Promise> CSSStyleSheet::replace(String text)
HTML::TemporaryExecutionContext execution_context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes };
// 1. Let rules be the result of running parse a stylesheets contents from text.
auto parsed_stylesheet = parse_css_stylesheet(make_parsing_params(), text);
auto& rules = parsed_stylesheet->rules();
auto rules = CSS::Parser::Parser::create(make_parsing_params(), text).parse_as_stylesheet_contents();
// 2. If rules contains one or more @import rules, remove those rules from rules.
GC::RootVector<GC::Ref<CSSRule>> rules_without_import(realm.heap());
@ -240,8 +239,7 @@ WebIDL::ExceptionOr<void> CSSStyleSheet::replace_sync(StringView text)
return WebIDL::NotAllowedError::create(realm(), "Can't call replaceSync() on non-modifiable stylesheets"_string);
// 2. Let rules be the result of running parse a stylesheets contents from text.
auto parsed_stylesheet = parse_css_stylesheet(make_parsing_params(), text);
auto& rules = parsed_stylesheet->rules();
auto rules = CSS::Parser::Parser::create(make_parsing_params(), text).parse_as_stylesheet_contents();
// 3. If rules contains one or more @import rules, remove those rules from rules.
GC::RootVector<GC::Ref<CSSRule>> rules_without_import(realm().heap());
@ -250,7 +248,7 @@ WebIDL::ExceptionOr<void> CSSStyleSheet::replace_sync(StringView text)
rules_without_import.append(rule);
}
// 4.Set sheets CSS rules to rules.
// 4. Set sheets CSS rules to rules.
m_rules->set_rules({}, rules_without_import);
return {};

View file

@ -118,6 +118,22 @@ Vector<Rule> Parser::parse_a_stylesheets_contents(TokenStream<T>& input)
return consume_a_stylesheets_contents(input);
}
GC::RootVector<GC::Ref<CSSRule>> Parser::parse_as_stylesheet_contents()
{
auto raw_rules = parse_a_stylesheets_contents(m_token_stream);
GC::RootVector<GC::Ref<CSSRule>> rules(realm().heap());
for (auto const& raw_rule : raw_rules) {
auto rule = convert_to_rule(raw_rule, Nested::No);
if (!rule) {
log_parse_error();
continue;
}
rules.append(*rule);
}
return rules;
}
// https://drafts.csswg.org/css-syntax/#parse-a-css-stylesheet
GC::Ref<CSS::CSSStyleSheet> Parser::parse_as_css_stylesheet(Optional<::URL::URL> location, Vector<NonnullRefPtr<MediaQuery>> media_query_list)
{

View file

@ -100,6 +100,7 @@ public:
Vector<Descriptor> parse_as_list_of_descriptors(AtRuleID);
CSSRule* parse_as_css_rule();
Optional<StyleProperty> parse_as_supports_condition();
GC::RootVector<GC::Ref<CSSRule>> parse_as_stylesheet_contents();
enum class SelectorParsingMode {
Standard,

View file

@ -0,0 +1,8 @@
Harness status: OK
Found 3 tests
3 Pass
Pass Constructing sheet with custom base URL ueses that URL for CSS rules
Pass Constructing sheet with relative URL adds to the constructor document's base URL
Pass Constructing sheet with invalid base URL throws a NotAllowedError

View file

@ -0,0 +1,66 @@
<!DOCTYPE html>
<title>CSSStyleSheet baseURL</title>
<link rel="author" title="Erik Nordin" href="mailto:enordin@mozilla.com">
<link rel="help" href="https://drafts.csswg.org/cssom-1/#dom-cssstylesheetinit-baseurl">
<div id="target"></div>
<script src='../../resources/testharness.js'></script>
<script src='../../resources/testharnessreport.js'></script>
<script>
function currentLocation() {
const sections = location.href.split("/")
sections.pop();
return sections.join("/");
}
test(() => {
const span = document.createElement("span");
target.appendChild(span);
span.attachShadow({ mode: "open" })
const shadowDiv = document.createElement("div");
span.shadowRoot.appendChild(shadowDiv);
const fileName = "example.png"
const baseURL = `${location.origin}/custom/path/`;
const fullURL = `${baseURL}${fileName}`;
const sheet = new CSSStyleSheet({ baseURL });
span.shadowRoot.adoptedStyleSheets = [sheet];
sheet.replaceSync(`* { background-image: url("${fileName}"); }`);
const styleFromRelative = getComputedStyle(shadowDiv).backgroundImage;
sheet.replaceSync(`* { background-image: url("${fullURL}"); }`);
const styleFromFull = getComputedStyle(shadowDiv).backgroundImage;
assert_equals(styleFromRelative, styleFromFull);
}, "Constructing sheet with custom base URL ueses that URL for CSS rules");
test(() => {
const span = document.createElement("span");
target.appendChild(span);
span.attachShadow({ mode: "open" })
const shadowDiv = document.createElement("div");
span.shadowRoot.appendChild(shadowDiv);
const fileName = "example.png"
const baseURL = "custom/path/";
const fullURL = `${currentLocation()}/${baseURL}${fileName}`;
const sheet = new CSSStyleSheet({ baseURL });
span.shadowRoot.adoptedStyleSheets = [sheet];
sheet.replaceSync(`* { background-image: url("${fileName}"); }`);
const styleFromRelative = getComputedStyle(shadowDiv).backgroundImage;
sheet.replaceSync(`* { background-image: url("${fullURL}"); }`);
const styleFromFull = getComputedStyle(shadowDiv).backgroundImage;
assert_equals(styleFromRelative, styleFromFull);
}, "Constructing sheet with relative URL adds to the constructor document's base URL");
test(() => {
assert_throws_dom("NotAllowedError", () => { new CSSStyleSheet({ baseURL: "https://test:test/"}) });
}, "Constructing sheet with invalid base URL throws a NotAllowedError");
</script>