2023-09-22 17:56:49 -03:00
|
|
|
|
/*
|
2024-01-14 00:42:41 -03:00
|
|
|
|
* Copyright (c) 2023-2024, Shannon Booth <shannon@serenityos.org>
|
2024-12-03 18:59:59 -03:00
|
|
|
|
* Copyright (c) 2024, Pavel Shliak <shlyakpavel@gmail.com>
|
2023-09-22 17:56:49 -03:00
|
|
|
|
*
|
|
|
|
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
2026-04-26 16:29:05 -03:00
|
|
|
|
#include <AK/ScopeGuard.h>
|
2026-05-17 15:50:41 -03:00
|
|
|
|
#include <LibCore/ImmutableBytes.h>
|
2026-04-18 05:54:06 -03:00
|
|
|
|
#include <LibWeb/Bindings/SVGScriptElement.h>
|
2026-02-08 13:13:36 -03:00
|
|
|
|
#include <LibWeb/DOM/Document.h>
|
2024-12-03 18:59:59 -03:00
|
|
|
|
#include <LibWeb/Fetch/Fetching/Fetching.h>
|
|
|
|
|
|
#include <LibWeb/Fetch/Infrastructure/FetchAlgorithms.h>
|
2026-02-08 13:13:36 -03:00
|
|
|
|
#include <LibWeb/Fetch/Infrastructure/HTTP/Requests.h>
|
2025-10-16 10:07:09 -03:00
|
|
|
|
#include <LibWeb/Fetch/Infrastructure/HTTP/Responses.h>
|
2023-09-25 09:12:21 -03:00
|
|
|
|
#include <LibWeb/HTML/Scripting/ClassicScript.h>
|
2025-11-26 15:32:39 -03:00
|
|
|
|
#include <LibWeb/MimeSniff/MimeType.h>
|
2023-09-25 09:12:21 -03:00
|
|
|
|
#include <LibWeb/Namespace.h>
|
|
|
|
|
|
#include <LibWeb/SVG/AttributeNames.h>
|
2023-09-22 17:56:49 -03:00
|
|
|
|
#include <LibWeb/SVG/SVGScriptElement.h>
|
|
|
|
|
|
|
|
|
|
|
|
namespace Web::SVG {
|
|
|
|
|
|
|
2024-11-14 12:01:23 -03:00
|
|
|
|
GC_DEFINE_ALLOCATOR(SVGScriptElement);
|
2023-11-19 15:47:52 -03:00
|
|
|
|
|
2023-09-22 17:56:49 -03:00
|
|
|
|
SVGScriptElement::SVGScriptElement(DOM::Document& document, DOM::QualifiedName qualified_name)
|
|
|
|
|
|
: SVGElement(document, move(qualified_name))
|
|
|
|
|
|
{
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void SVGScriptElement::initialize(JS::Realm& realm)
|
|
|
|
|
|
{
|
2024-03-16 09:13:08 -03:00
|
|
|
|
WEB_SET_PROTOTYPE_FOR_INTERFACE(SVGScriptElement);
|
2025-04-20 11:22:57 -03:00
|
|
|
|
Base::initialize(realm);
|
2023-09-22 17:56:49 -03:00
|
|
|
|
}
|
|
|
|
|
|
|
2023-09-25 09:12:21 -03:00
|
|
|
|
void SVGScriptElement::visit_edges(Cell::Visitor& visitor)
|
|
|
|
|
|
{
|
|
|
|
|
|
Base::visit_edges(visitor);
|
2024-07-16 09:49:28 -03:00
|
|
|
|
SVGURIReferenceMixin::visit_edges(visitor);
|
2023-09-25 09:12:21 -03:00
|
|
|
|
visitor.visit(m_script);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-10 16:58:06 -03:00
|
|
|
|
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());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-02-26 09:48:57 -03:00
|
|
|
|
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_);
|
2025-02-26 09:53:43 -03:00
|
|
|
|
if (name == SVG::AttributeNames::href || name == SVG::AttributeNames::type) {
|
2025-02-26 09:48:57 -03:00
|
|
|
|
process_the_script_element();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-02-26 09:37:38 -03:00
|
|
|
|
void SVGScriptElement::inserted()
|
|
|
|
|
|
{
|
|
|
|
|
|
Base::inserted();
|
|
|
|
|
|
if (m_parser_inserted)
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
process_the_script_element();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 09:36:27 -03:00
|
|
|
|
void SVGScriptElement::children_changed(ChildrenChangedMetadata const& metadata)
|
2025-02-26 09:37:38 -03:00
|
|
|
|
{
|
|
|
|
|
|
Base::children_changed(metadata);
|
|
|
|
|
|
if (m_parser_inserted)
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
process_the_script_element();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-09-25 09:12:21 -03:00
|
|
|
|
// 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;
|
|
|
|
|
|
|
2026-02-01 17:41:27 -03:00
|
|
|
|
// https://html.spec.whatwg.org/multipage/webappapis.html#enabling-and-disabling-scripting
|
|
|
|
|
|
if (is_scripting_disabled())
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
2025-02-26 09:53:43 -03:00
|
|
|
|
// 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-12-03 18:59:59 -03:00
|
|
|
|
// 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.
|
2023-09-25 09:12:21 -03:00
|
|
|
|
if (has_attribute(SVG::AttributeNames::href) || has_attribute_ns(Namespace::XLink.to_string(), SVG::AttributeNames::href)) {
|
2024-12-03 18:59:59 -03:00
|
|
|
|
auto href_value = href()->base_val();
|
|
|
|
|
|
|
2025-06-24 11:35:11 -03:00
|
|
|
|
auto maybe_script_url = document().encoding_parse_url(href_value);
|
2025-01-23 03:40:57 -03:00
|
|
|
|
if (!maybe_script_url.has_value()) {
|
2024-12-03 18:59:59 -03:00
|
|
|
|
dbgln("Invalid script URL: {}", href_value);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-04-26 16:29:05 -03:00
|
|
|
|
auto script_url = maybe_script_url.release_value();
|
2024-12-03 18:59:59 -03:00
|
|
|
|
|
|
|
|
|
|
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());
|
|
|
|
|
|
|
2026-04-26 16:29:05 -03:00
|
|
|
|
// 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);
|
2024-12-03 18:59:59 -03:00
|
|
|
|
|
|
|
|
|
|
Fetch::Infrastructure::FetchAlgorithms::Input fetch_algorithms_input {};
|
2026-04-26 16:29:05 -03:00
|
|
|
|
fetch_algorithms_input.process_response_consume_body
|
|
|
|
|
|
= [self = GC::Ref { *this }, script_url](auto response, auto body_bytes) {
|
2026-05-17 15:50:41 -03:00
|
|
|
|
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, {}); });
|
2026-04-26 16:29:05 -03:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
(void)Fetch::Fetching::fetch(realm(), request,
|
|
|
|
|
|
Fetch::Infrastructure::FetchAlgorithms::create(vm, move(fetch_algorithms_input)));
|
|
|
|
|
|
return;
|
2023-09-25 09:12:21 -03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-26 16:29:05 -03:00
|
|
|
|
auto script_content = child_text_content().to_utf8_but_should_be_ported_to_utf16();
|
|
|
|
|
|
if (script_content.is_empty())
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
2023-09-25 09:12:21 -03:00
|
|
|
|
// 3. The 'script' element's "already processed" flag is set to true.
|
|
|
|
|
|
m_already_processed = true;
|
2026-04-26 16:29:05 -03:00
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-17 15:50:41 -03:00
|
|
|
|
void SVGScriptElement::finish_external_script_fetch(URL::URL const& script_url, ReadonlyBytes body)
|
2026-04-26 16:29:05 -03:00
|
|
|
|
{
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
2023-09-25 09:12:21 -03:00
|
|
|
|
|
2026-04-26 16:29:05 -03:00
|
|
|
|
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()
|
|
|
|
|
|
{
|
2023-09-25 09:12:21 -03:00
|
|
|
|
// 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.
|
2024-01-14 00:42:41 -03:00
|
|
|
|
|
2026-04-26 16:29:05 -03:00
|
|
|
|
// 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;
|
|
|
|
|
|
|
2024-01-14 00:42:41 -03:00
|
|
|
|
// 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.
|
2026-04-26 16:29:05 -03:00
|
|
|
|
if (!m_document->ready_to_run_scripts())
|
|
|
|
|
|
return;
|
2024-12-03 18:59:59 -03:00
|
|
|
|
|
|
|
|
|
|
// 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'.
|
2023-09-25 09:12:21 -03:00
|
|
|
|
|
|
|
|
|
|
(void)m_script->run();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-09-22 17:56:49 -03:00
|
|
|
|
}
|