ladybird/Libraries/LibWeb/SVG/SVGScriptElement.cpp
Andreas Kling b849af70b8 AK+LibWeb: Reject impossible Variant visit overloads
Make Variant::visit reject typed visitor overloads that cannot be
called for any variant alternative. This catches stale visitors after a
variant payload type changes instead of falling through to a generic
overload.

Update fetch body consumers that still expected ByteBuffer after the
body payload moved to Core::ImmutableBytes.
2026-05-18 01:21:34 +02:00

209 lines
8.5 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::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();
}
}