ladybird/Libraries/LibWeb/SVG/SVGScriptElement.cpp
Zaggy1024 57a78f2b59 LibWeb: Transfer load delayers when elements are adopted
Otherwise, the load event will block the original document until GC
runs.

Without this, media-load-task-after-adoption.html would wait for the
idle timeout to trigger a garbage collection, which could sometimes
cause the test to time out entirely.
2026-06-11 00:34:12 +02:00

217 lines
8.7 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Copyright (c) 2023-2024, Shannon Booth <shannon@serenityos.org>
* Copyright (c) 2024, Pavel Shliak <shlyakpavel@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/ScopeGuard.h>
#include <LibCore/ImmutableBytes.h>
#include <LibWeb/Bindings/SVGScriptElement.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/Fetch/Fetching/Fetching.h>
#include <LibWeb/Fetch/Infrastructure/FetchAlgorithms.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Requests.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Responses.h>
#include <LibWeb/HTML/Scripting/ClassicScript.h>
#include <LibWeb/MimeSniff/MimeType.h>
#include <LibWeb/Namespace.h>
#include <LibWeb/SVG/AttributeNames.h>
#include <LibWeb/SVG/SVGScriptElement.h>
namespace Web::SVG {
GC_DEFINE_ALLOCATOR(SVGScriptElement);
SVGScriptElement::SVGScriptElement(DOM::Document& document, DOM::QualifiedName qualified_name)
: SVGElement(document, move(qualified_name))
{
}
void SVGScriptElement::initialize(JS::Realm& realm)
{
WEB_SET_PROTOTYPE_FOR_INTERFACE(SVGScriptElement);
Base::initialize(realm);
}
void SVGScriptElement::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
SVGURIReferenceMixin::visit_edges(visitor);
visitor.visit(m_script);
}
void SVGScriptElement::adopted_from(DOM::Document& old_document)
{
Base::adopted_from(old_document);
if (m_document_load_event_delayer.has_value())
m_document_load_event_delayer.emplace(document());
}
void SVGScriptElement::attribute_changed(FlyString const& name, Optional<String> const& old_value, Optional<String> const& value, Optional<FlyString> const& namespace_)
{
Base::attribute_changed(name, old_value, value, namespace_);
if (name == SVG::AttributeNames::href || name == SVG::AttributeNames::type) {
process_the_script_element();
}
}
void SVGScriptElement::inserted()
{
Base::inserted();
if (m_parser_inserted)
return;
process_the_script_element();
}
void SVGScriptElement::children_changed(ChildrenChangedMetadata const& metadata)
{
Base::children_changed(metadata);
if (m_parser_inserted)
return;
process_the_script_element();
}
// https://www.w3.org/TR/SVGMobile12/script.html#ScriptContentProcessing
void SVGScriptElement::process_the_script_element()
{
// 1. If the 'script' element's "already processed" flag is true or if the element is not in the
// document tree, then no action is performed and these steps are ended.
if (m_already_processed || !in_a_document_tree())
return;
// https://html.spec.whatwg.org/multipage/webappapis.html#enabling-and-disabling-scripting
if (is_scripting_disabled())
return;
// https://svgwg.org/svg2-draft/interact.html#ScriptElement
// Before attempting to execute the script element the resolved media type value for type must be inspected.
// If the SVG user agent does not support the scripting language then the script element must not be executed.
// FIXME: Support type="module" scripts
auto maybe_script_type = attribute(SVG::AttributeNames::type);
if (maybe_script_type.has_value() && !maybe_script_type->is_empty()) {
auto script_type = MUST(maybe_script_type->to_ascii_lowercase().trim_ascii_whitespace());
if (!MimeSniff::is_javascript_mime_type_essence_match(script_type)) {
dbgln("SVGScriptElement: Unsupported script type: {}", *maybe_script_type);
return;
}
}
// 2. If the 'script' element references external script content, then the external script content
// using the current value of the 'xlink:href' attribute is fetched. Further processing of the
// 'script' element is dependent on the external script content, and will block here until the
// resource has been fetched or is determined to be an invalid IRI reference.
if (has_attribute(SVG::AttributeNames::href) || has_attribute_ns(Namespace::XLink.to_string(), SVG::AttributeNames::href)) {
auto href_value = href()->base_val();
auto maybe_script_url = document().encoding_parse_url(href_value);
if (!maybe_script_url.has_value()) {
dbgln("Invalid script URL: {}", href_value);
return;
}
auto script_url = maybe_script_url.release_value();
auto& vm = realm().vm();
auto request = Fetch::Infrastructure::Request::create(vm);
request->set_url(script_url);
request->set_destination(Fetch::Infrastructure::Request::Destination::Script);
// FIXME: Use CORS state specified by the crossorigin attribute.
request->set_mode(Fetch::Infrastructure::Request::Mode::NoCORS);
request->set_credentials_mode(Fetch::Infrastructure::Request::CredentialsMode::SameOrigin);
request->set_client(&document().relevant_settings_object());
// 3. The 'script' element's "already processed" flag is set to true.
// We set this before dispatching the fetch so that re-entrant calls (e.g. from attribute_changed
// while the fetch is in flight) early-return at step 1, matching Chromium's `already_started_`
// semantics. The in-flight fetch is allowed to finish.
m_already_processed = true;
m_document_load_event_delayer.emplace(*m_document);
if (m_parser_inserted)
m_document->set_pending_parsing_blocking_svg_script(this);
Fetch::Infrastructure::FetchAlgorithms::Input fetch_algorithms_input {};
fetch_algorithms_input.process_response_consume_body
= [self = GC::Ref { *this }, script_url](auto response, auto body_bytes) {
if (response->is_network_error()) {
self->finish_external_script_fetch(script_url, {});
return;
}
body_bytes.visit(
[&](Core::ImmutableBytes& bytes) { self->finish_external_script_fetch(script_url, bytes.bytes()); },
[&](auto) { self->finish_external_script_fetch(script_url, {}); });
};
(void)Fetch::Fetching::fetch(realm(), request,
Fetch::Infrastructure::FetchAlgorithms::create(vm, move(fetch_algorithms_input)));
return;
}
auto script_content = child_text_content().to_utf8_but_should_be_ported_to_utf16();
if (script_content.is_empty())
return;
// 3. The 'script' element's "already processed" flag is set to true.
m_already_processed = true;
m_script = HTML::ClassicScript::create(m_document->url().basename(), script_content,
HTML::relevant_settings_object(*this), m_document->base_url(), m_source_line_number);
execute_script();
}
void SVGScriptElement::finish_external_script_fetch(URL::URL const& script_url, ReadonlyBytes body)
{
if (!body.is_empty() && in_a_document_tree() && !is_scripting_disabled()) {
auto script_content = String::from_utf8(body);
if (script_content.is_error())
dbgln("Failed to decode SVG external script as UTF-8");
else
m_script = HTML::ClassicScript::create(script_url.basename(), script_content.release_value(),
HTML::relevant_settings_object(*this), m_document->base_url(), m_source_line_number);
}
if (m_parser_inserted) {
m_ready_to_be_parser_executed = true;
m_document->schedule_html_parser_end_check();
return;
}
ScopeGuard clear_delayer { [&] { m_document_load_event_delayer.clear(); } };
execute_script();
}
void SVGScriptElement::execute_pending_parser_blocking_script(Badge<HTML::HTMLParser>)
{
VERIFY(m_ready_to_be_parser_executed);
m_ready_to_be_parser_executed = false;
ScopeGuard clear_delayer { [&] { m_document_load_event_delayer.clear(); } };
execute_script();
}
void SVGScriptElement::execute_script()
{
// 4. If the script content is inline, or if it is external and was fetched successfully, then the
// script is executed. Note that at this point, these steps may be re-entrant if the execution
// of the script results in further 'script' elements being inserted into the document.
// m_script is null when the external fetch failed; the parser-blocking slot still needs to be drained
// so the parser can resume, but there's no script to run.
if (!m_script)
return;
// https://html.spec.whatwg.org/multipage/document-lifecycle.html#read-html
// Before any script execution occurs, the user agent must wait for scripts may run for the newly-created document to be true for document.
if (!m_document->ready_to_run_scripts())
return;
// FIXME: Note that a load event is dispatched on a 'script' element once it has been processed,
// unless it referenced external script content with an invalid IRI reference and 'externalResourcesRequired' was set to 'true'.
(void)m_script->run();
}
}