diff --git a/Userland/Libraries/LibWeb/HTML/HTMLLinkElement.cpp b/Userland/Libraries/LibWeb/HTML/HTMLLinkElement.cpp index 294f5ee67bf..217ebdbde38 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLLinkElement.cpp +++ b/Userland/Libraries/LibWeb/HTML/HTMLLinkElement.cpp @@ -2,6 +2,7 @@ * Copyright (c) 2018-2021, Andreas Kling * Copyright (c) 2021, the SerenityOS developers. * Copyright (c) 2021, Sam Atkins + * Copyright (c) 2023, Srikavin Ramkumar * * SPDX-License-Identifier: BSD-2-Clause */ @@ -11,7 +12,14 @@ #include #include #include +#include +#include +#include +#include +#include +#include #include +#include #include #include #include @@ -36,25 +44,18 @@ JS::ThrowCompletionOr HTMLLinkElement::initialize(JS::Realm& realm) void HTMLLinkElement::inserted() { - if (has_attribute(AttributeNames::disabled) && (m_relationship & Relationship::Stylesheet)) - return; - HTMLElement::inserted(); + // FIXME: Handle alternate stylesheets properly if (m_relationship & Relationship::Stylesheet && !(m_relationship & Relationship::Alternate)) { - auto url = document().parse_url(href()); - dbgln_if(CSS_LOADER_DEBUG, "HTMLLinkElement: Loading import URL: {}", url); - auto request = LoadRequest::create_for_url_on_page(url, document().page()); - // NOTE: Mark this element as delaying the document load event *before* calling set_resource() - // as it may trigger a synchronous resource_did_load() callback. - m_document_load_event_delayer.emplace(document()); - set_resource(ResourceLoader::the().load_resource(Resource::Type::Generic, request)); - - // NOTE: If we ended up not loading a resource for whatever reason, don't delay the load event. - if (!resource()) - m_document_load_event_delayer.clear(); + // https://html.spec.whatwg.org/multipage/links.html#link-type-stylesheet:fetch-and-process-the-linked-resource + // The appropriate times to fetch and process this type of link are: + // - When the external resource link is created on a link element that is already browsing-context connected. + // - When the external resource link's link element becomes browsing-context connected. + fetch_and_process_linked_resource(); } + // FIXME: Follow spec for fetching and processing these attributes as well if (m_relationship & Relationship::Preload) { // FIXME: Respect the "as" attribute. LoadRequest request; @@ -103,68 +104,314 @@ void HTMLLinkElement::parse_attribute(DeprecatedFlyString const& name, Deprecate } } - if (name == HTML::AttributeNames::disabled && (m_relationship & Relationship::Stylesheet) && m_loaded_style_sheet) - document().style_sheets().remove_sheet(*m_loaded_style_sheet); + if (m_relationship & Relationship::Stylesheet) { + if (name == HTML::AttributeNames::disabled && m_loaded_style_sheet) + document().style_sheets().remove_sheet(*m_loaded_style_sheet); + + // https://html.spec.whatwg.org/multipage/links.html#link-type-stylesheet:fetch-and-process-the-linked-resource + // The appropriate times to fetch and process this type of link are: + if ( + // - When the href attribute of the link element of an external resource link that is already browsing-context connected is changed. + name == AttributeNames::href || + // - When the disabled attribute of the link element of an external resource link that is already browsing-context connected is set, changed, or removed. + name == AttributeNames::disabled || + // - When the crossorigin attribute of the link element of an external resource link that is already browsing-context connected is set, changed, or removed. + name == AttributeNames::crossorigin + // FIXME: - When the type attribute of the link element of an external resource link that is already browsing-context connected is set or changed to a value that does not or no longer matches the Content-Type metadata of the previous obtained external resource, if any. + // FIXME: - When the type attribute of the link element of an external resource link that is already browsing-context connected, but was previously not obtained due to the type attribute specifying an unsupported type, is removed or changed. + ) { + fetch_and_process_linked_resource(); + } + } } void HTMLLinkElement::resource_did_fail() { dbgln_if(CSS_LOADER_DEBUG, "HTMLLinkElement: Resource did fail. URL: {}", resource()->url()); - - m_document_load_event_delayer.clear(); } void HTMLLinkElement::resource_did_load() { VERIFY(resource()); - VERIFY(m_relationship & (Relationship::Stylesheet | Relationship::Icon)); - - if (m_relationship & Relationship::Stylesheet) - resource_did_load_stylesheet(); - if (m_relationship & Relationship::Icon) + VERIFY(m_relationship & (Relationship::Icon)); + if (m_relationship & Relationship::Icon) { resource_did_load_favicon(); + m_document_load_event_delayer.clear(); + } } void HTMLLinkElement::did_remove_attribute(DeprecatedFlyString const& attr) { - if (attr == HTML::AttributeNames::disabled && (m_relationship & Relationship::Stylesheet)) { - if (!resource()) - inserted(); - else - resource_did_load_stylesheet(); + if (m_relationship & Relationship::Stylesheet) { + // https://html.spec.whatwg.org/multipage/links.html#link-type-stylesheet:fetch-and-process-the-linked-resource + // The appropriate times to fetch and process this type of link are: + if ( + // - When the href attribute of the link element of an external resource link that is already browsing-context connected is changed. + attr == AttributeNames::href || + // - When the disabled attribute of the link element of an external resource link that is already browsing-context connected is set, changed, or removed. + attr == AttributeNames::disabled || + // - When the crossorigin attribute of the link element of an external resource link that is already browsing-context connected is set, changed, or removed. + attr == AttributeNames::crossorigin + // FIXME: - When the type attribute of the link element of an external resource link that is already browsing-context connected, but was previously not obtained due to the type attribute specifying an unsupported type, is removed or changed. + ) { + fetch_and_process_linked_resource(); + } } } -void HTMLLinkElement::resource_did_load_stylesheet() +// https://html.spec.whatwg.org/multipage/semantics.html#create-link-options-from-element +HTMLLinkElement::LinkProcessingOptions HTMLLinkElement::create_link_options() { - VERIFY(m_relationship & Relationship::Stylesheet); - m_document_load_event_delayer.clear(); + // 1. Let document be el's node document. + auto& document = this->document(); - if (!resource()->has_encoded_data()) { - dbgln_if(CSS_LOADER_DEBUG, "HTMLLinkElement: Resource did load, no encoded data. URL: {}", resource()->url()); + // 2. Let options be a new link processing options with + LinkProcessingOptions options; + // FIXME: destination the result of translating the state of el's as attribute + // crossorigin the state of el's crossorigin content attribute + options.crossorigin = cors_setting_attribute_from_keyword( + has_attribute(AttributeNames::crossorigin) ? String::from_deprecated_string(get_attribute(AttributeNames::crossorigin)).release_value_but_fixme_should_propagate_errors() + : Optional {}); + // FIXME: referrer policy the state of el's referrerpolicy content attribute + // FIXME: source set el's source set + // base URL document's URL + options.base_url = document.url(); + // origin document's origin + options.origin = document.origin(); + // environment document's relevant settings object + options.environment = &document.relevant_settings_object(); + // policy container document's policy container + options.policy_container = document.policy_container(); + // document document + options.document = &document; + // FIXME: cryptographic nonce metadata The current value of el's [[CryptographicNonce]] internal slot + + // 3. If el has an href attribute, then set options's href to the value of el's href attribute. + if (has_attribute(AttributeNames::href)) + options.href = String::from_deprecated_string(get_attribute(AttributeNames::href)).release_value_but_fixme_should_propagate_errors(); + + // 4. If el has an integrity attribute, then set options's integrity to the value of el's integrity content attribute. + if (has_attribute(AttributeNames::integrity)) + options.integrity = String::from_deprecated_string(get_attribute(AttributeNames::integrity)).release_value_but_fixme_should_propagate_errors(); + + // 5. If el has a type attribute, then set options's type to the value of el's type attribute. + if (has_attribute(AttributeNames::type)) + options.type = String::from_deprecated_string(get_attribute(AttributeNames::type)).release_value_but_fixme_should_propagate_errors(); + + // FIXME: 6. Assert: options's href is not the empty string, or options's source set is not null. + // A link element with neither an href or an imagesrcset does not represent a link. + + // 7. Return options. + return options; +} + +// https://html.spec.whatwg.org/multipage/semantics.html#create-a-link-request +JS::GCPtr HTMLLinkElement::create_link_request(HTMLLinkElement::LinkProcessingOptions const& options) +{ + // 1. Assert: options's href is not the empty string. + + // FIXME: 2. If options's destination is not a destination, then return null. + + // 3. Parse a URL given options's href, relative to options's base URL. If that fails, then return null. Otherwise, let url be the resulting URL record. + auto url = options.base_url.complete_url(options.href); + if (!url.is_valid()) + return nullptr; + + // 4. Let request be the result of creating a potential-CORS request given url, options's destination, and options's crossorigin. + auto request = create_potential_CORS_request(vm(), url, options.destination, options.crossorigin); + + // 5. Set request's policy container to options's policy container. + request->set_policy_container(options.policy_container); + + // 6. Set request's integrity metadata to options's integrity. + request->set_integrity_metadata(options.integrity); + + // 7. Set request's cryptographic nonce metadata to options's cryptographic nonce metadata. + request->set_cryptographic_nonce_metadata(options.cryptographic_nonce_metadata); + + // 8. Set request's referrer policy to options's referrer policy. + request->set_referrer_policy(options.referrer_policy); + + // 9. Set request's client to options's environment. + request->set_client(options.environment); + + // 10. Return request. + return request; +} + +// https://html.spec.whatwg.org/multipage/semantics.html#fetch-and-process-the-linked-resource +void HTMLLinkElement::fetch_and_process_linked_resource() +{ + default_fetch_and_process_linked_resource(); +} + +// https://html.spec.whatwg.org/multipage/semantics.html#default-fetch-and-process-the-linked-resource +void HTMLLinkElement::default_fetch_and_process_linked_resource() +{ + // https://html.spec.whatwg.org/multipage/semantics.html#the-link-element:attr-link-href-4 + // If both the href and imagesrcset attributes are absent, then the element does not define a link. + // FIXME: Support imagesrcset attribute + if (!has_attribute(AttributeNames::href) || href().is_empty()) + return; + + // 1. Let options be the result of creating link options from el. + auto options = create_link_options(); + + // 2. Let request be the result of creating a link request given options. + auto request = create_link_request(options); + + // 3. If request is null, then return. + if (request == nullptr) { + return; + } + + // FIXME: 4. Set request's synchronous flag. + + // 5. Run the linked resource fetch setup steps, given el and request. If the result is false, then return. + if (!linked_resource_fetch_setup_steps(*request)) + return; + + // 6. Set request's initiator type to "css" if el's rel attribute contains the keyword stylesheet; "link" otherwise. + if (m_relationship & Relationship::Stylesheet) { + request->set_initiator_type(Fetch::Infrastructure::Request::InitiatorType::CSS); } else { - dbgln_if(CSS_LOADER_DEBUG, "HTMLLinkElement: Resource did load, has encoded data. URL: {}", resource()->url()); - - if (resource()->mime_type() != "text/css"sv) { - dbgln_if(CSS_LOADER_DEBUG, "HTMLLinkElement: Resource did load, but MIME type was {} instead of text/css. URL: {}", resource()->mime_type(), resource()->url()); - return; - } + request->set_initiator_type(Fetch::Infrastructure::Request::InitiatorType::Link); } - CSS::CSSStyleSheet* sheet = m_loaded_style_sheet; - if (!sheet) { - sheet = parse_css_stylesheet(CSS::Parser::ParsingContext(document(), resource()->url()), resource()->encoded_data()); + // 7. Fetch request with processResponseConsumeBody set to the following steps given response response and null, failure, or a byte sequence bodyBytes: + Fetch::Fetching::fetch( + realm(), *request, + Fetch::Infrastructure::FetchAlgorithms::create(vm(), + { .process_request_body_chunk_length = {}, + .process_request_end_of_body = {}, + .process_early_hints_response = {}, + .process_response = {}, + .process_response_end_of_body = {}, + .process_response_consume_body = [this, hr = options](auto response, auto body_bytes) { + // 1. Let success be true. + bool success = true; - if (!sheet) { + // 2. If either of the following conditions are met: + // - bodyBytes is null or failure; or + // - response's status is not an ok status, + // NOTE: content-specific errors, e.g., CSS parse errors or PNG decoding errors, do not affect success. + if (body_bytes.template has() || body_bytes.template has() || !Fetch::Infrastructure::is_ok_status(response->status())) { + // then set success to false. + success = false; + } + // FIXME: 3. Otherwise, wait for the link resource's critical subresources to finish loading. + + // 4. Process the linked resource given el, success, response, and bodyBytes. + process_linked_resource(success, response, body_bytes); + } })) + .release_value_but_fixme_should_propagate_errors(); +} + +// https://html.spec.whatwg.org/multipage/links.html#link-type-stylesheet:process-the-linked-resource +void HTMLLinkElement::process_stylesheet_resource(bool success, Fetch::Infrastructure::Response const& response, Variant body_bytes) +{ + // 1. If the resource's Content-Type metadata is not text/css, then set success to false. + auto extracted_mime_type = response.header_list()->extract_mime_type().release_value_but_fixme_should_propagate_errors(); + if (!extracted_mime_type.has_value() || extracted_mime_type->essence() != "text/css") { + success = false; + } + + // FIXME: 2. If el no longer creates an external resource link that contributes to the styling processing model, + // or if, since the resource in question was fetched, it has become appropriate to fetch it again, then return. + + // 3. If el has an associated CSS style sheet, remove the CSS style sheet. + if (m_loaded_style_sheet) { + document().style_sheets().remove_sheet(*m_loaded_style_sheet); + m_loaded_style_sheet = nullptr; + } + + // 4. If success is true, then: + if (success) { + // 1. Create a CSS style sheet with the following properties: + // type + // text/css + // location + // The resulting URL string determined during the fetch and process the linked resource algorithm. + // owner node + // element + // media + // The media attribute of element. + // title + // The title attribute of element, if element is in a document tree, or the empty string otherwise. + // alternate flag + // Set if the link is an alternative style sheet and element's explicitly enabled is false; unset otherwise. + // origin-clean flag + // Set if the resource is CORS-same-origin; unset otherwise. + // parent CSS style sheet + // owner CSS rule + // null + // disabled flag + // Left at its default value. + // CSS rules + // Left uninitialized. + // + // The CSS environment encoding is the result of running the following steps: [CSSSYNTAX] + // 1. If the element has a charset attribute, get an encoding from that attribute's value. If that succeeds, return the resulting encoding. [ENCODING] + // 2. Otherwise, return the document's character encoding. [DOM] + m_loaded_style_sheet = parse_css_stylesheet(CSS::Parser::ParsingContext(document(), *response.url()), body_bytes.template get()); + + if (m_loaded_style_sheet) { + m_loaded_style_sheet->set_owner_node(this); + document().style_sheets().add_sheet(*m_loaded_style_sheet); + } else { dbgln_if(CSS_LOADER_DEBUG, "HTMLLinkElement: Failed to parse stylesheet: {}", resource()->url()); - return; } - m_loaded_style_sheet = sheet; + // 2. Fire an event named load at el. + dispatch_event(*DOM::Event::create(realm(), HTML::EventNames::load).release_value_but_fixme_should_propagate_errors()); + } + // 5. Otherwise, fire an event named error at el. + else { + dispatch_event(*DOM::Event::create(realm(), HTML::EventNames::error).release_value_but_fixme_should_propagate_errors()); } - sheet->set_owner_node(this); - document().style_sheets().add_sheet(*sheet); + // FIXME: 6. If el contributes a script-blocking style sheet, then: + // FIXME: 1. Assert: el's node document's script-blocking style sheet counter is greater than 0. + // FIXME: 2. Decrement el's node document's script-blocking style sheet counter by 1. + + // 7. Unblock rendering on el. + m_document_load_event_delayer.clear(); +} + +// https://html.spec.whatwg.org/multipage/semantics.html#process-the-linked-resource +void HTMLLinkElement::process_linked_resource(bool success, Fetch::Infrastructure::Response const& response, Variant body_bytes) +{ + if (m_relationship & Relationship::Stylesheet) + process_stylesheet_resource(success, response, body_bytes); +} + +// https://html.spec.whatwg.org/multipage/semantics.html#linked-resource-fetch-setup-steps +bool HTMLLinkElement::linked_resource_fetch_setup_steps(Fetch::Infrastructure::Request& request) +{ + if (m_relationship & Relationship::Stylesheet) + return stylesheet_linked_resource_fetch_setup_steps(request); + + return true; +} + +// https://html.spec.whatwg.org/multipage/links.html#link-type-stylesheet:linked-resource-fetch-setup-steps +bool HTMLLinkElement::stylesheet_linked_resource_fetch_setup_steps(Fetch::Infrastructure::Request& request) +{ + // 1. If el's disabled attribute is set, then return false. + if (has_attribute(AttributeNames::disabled)) + return false; + // FIXME: 2. If el contributes a script-blocking style sheet, increment el's node document's script-blocking style sheet counter by 1. + + // 3. If el's media attribute's value matches the environment and el is potentially render-blocking, then block rendering on el. + // FIXME: Check media attribute value. + m_document_load_event_delayer.emplace(document()); + + // 4. If el is currently render-blocking, then set request's render-blocking to true. + // FIXME: Check if el is currently render-blocking. + request.set_render_blocking(true); + + // 5. Return true. + return true; } void HTMLLinkElement::resource_did_load_favicon() diff --git a/Userland/Libraries/LibWeb/HTML/HTMLLinkElement.h b/Userland/Libraries/LibWeb/HTML/HTMLLinkElement.h index ee78861ec6d..819a16bfd11 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLLinkElement.h +++ b/Userland/Libraries/LibWeb/HTML/HTMLLinkElement.h @@ -2,6 +2,7 @@ * Copyright (c) 2018-2020, Andreas Kling * Copyright (c) 2021, the SerenityOS developers. * Copyright (c) 2021, Sam Atkins + * Copyright (c) 2023, Srikavin Ramkumar * * SPDX-License-Identifier: BSD-2-Clause */ @@ -9,6 +10,9 @@ #pragma once #include +#include +#include +#include #include namespace Web::HTML { @@ -44,7 +48,71 @@ private: virtual void did_remove_attribute(DeprecatedFlyString const&) override; virtual void visit_edges(Cell::Visitor&) override; - void resource_did_load_stylesheet(); + struct LinkProcessingOptions { + // href (default the empty string) + String href {}; + // destination (default the empty string) + Optional destination {}; + // initiator (default "link") + Optional initiator { Fetch::Infrastructure::Request::InitiatorType::Link }; + // integrity (default the empty string) + String integrity {}; + // type (default the empty string) + String type {}; + // cryptographic nonce metadata (default the empty string) + // A string + String cryptographic_nonce_metadata {}; + // crossorigin (default No CORS) + // A CORS settings attribute state + CORSSettingAttribute crossorigin { CORSSettingAttribute::NoCORS }; + // referrer policy (default the empty string) + // A referrer policy + Optional referrer_policy {}; + // FIXME: source set (default null) + // Null or a source set + // base URL + // A URL + AK::URL base_url; + // origin + // An origin + HTML::Origin origin; + // environment + // An environment + HTML::EnvironmentSettingsObject* environment; + // policy container + // A policy container + HTML::PolicyContainer policy_container; + // document (default null) + // Null or a Document + Web::DOM::Document* document { nullptr }; + // FIXME: on document ready (default null) + // Null or an algorithm accepting a Document + }; + + // https://html.spec.whatwg.org/multipage/semantics.html#create-link-options-from-element + LinkProcessingOptions create_link_options(); + + // https://html.spec.whatwg.org/multipage/semantics.html#create-a-link-request + JS::GCPtr create_link_request(LinkProcessingOptions const&); + + // https://html.spec.whatwg.org/multipage/semantics.html#linked-resource-fetch-setup-steps + bool linked_resource_fetch_setup_steps(Fetch::Infrastructure::Request&); + + // https://html.spec.whatwg.org/multipage/links.html#link-type-stylesheet:linked-resource-fetch-setup-steps + bool stylesheet_linked_resource_fetch_setup_steps(Fetch::Infrastructure::Request&); + + // https://html.spec.whatwg.org/multipage/semantics.html#fetch-and-process-the-linked-resource + void fetch_and_process_linked_resource(); + + // https://html.spec.whatwg.org/multipage/semantics.html#process-the-linked-resource + void process_linked_resource(bool success, Fetch::Infrastructure::Response const&, Variant); + + // https://html.spec.whatwg.org/multipage/links.html#link-type-stylesheet:process-the-linked-resource + void process_stylesheet_resource(bool success, Fetch::Infrastructure::Response const&, Variant); + + // https://html.spec.whatwg.org/multipage/semantics.html#default-fetch-and-process-the-linked-resource + void default_fetch_and_process_linked_resource(); + void resource_did_load_favicon(); struct Relationship {