/src/serenity/Userland/Libraries/LibWeb/HTML/HTMLLinkElement.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2018-2023, Andreas Kling <kling@serenityos.org> |
3 | | * Copyright (c) 2021, the SerenityOS developers. |
4 | | * Copyright (c) 2021, Sam Atkins <atkinssj@serenityos.org> |
5 | | * Copyright (c) 2023, Srikavin Ramkumar <me@srikavin.me> |
6 | | * |
7 | | * SPDX-License-Identifier: BSD-2-Clause |
8 | | */ |
9 | | |
10 | | #include <AK/ByteBuffer.h> |
11 | | #include <AK/Debug.h> |
12 | | #include <LibTextCodec/Decoder.h> |
13 | | #include <LibURL/URL.h> |
14 | | #include <LibWeb/Bindings/HTMLLinkElementPrototype.h> |
15 | | #include <LibWeb/CSS/Parser/Parser.h> |
16 | | #include <LibWeb/DOM/DOMTokenList.h> |
17 | | #include <LibWeb/DOM/Document.h> |
18 | | #include <LibWeb/DOM/Event.h> |
19 | | #include <LibWeb/DOM/ShadowRoot.h> |
20 | | #include <LibWeb/Fetch/Fetching/Fetching.h> |
21 | | #include <LibWeb/Fetch/Infrastructure/FetchAlgorithms.h> |
22 | | #include <LibWeb/Fetch/Infrastructure/FetchController.h> |
23 | | #include <LibWeb/Fetch/Infrastructure/HTTP/Requests.h> |
24 | | #include <LibWeb/Fetch/Infrastructure/HTTP/Responses.h> |
25 | | #include <LibWeb/HTML/EventNames.h> |
26 | | #include <LibWeb/HTML/HTMLLinkElement.h> |
27 | | #include <LibWeb/HTML/PotentialCORSRequest.h> |
28 | | #include <LibWeb/HTML/TraversableNavigable.h> |
29 | | #include <LibWeb/Infra/CharacterTypes.h> |
30 | | #include <LibWeb/Infra/Strings.h> |
31 | | #include <LibWeb/Loader/ResourceLoader.h> |
32 | | #include <LibWeb/Page/Page.h> |
33 | | #include <LibWeb/Platform/ImageCodecPlugin.h> |
34 | | |
35 | | namespace Web::HTML { |
36 | | |
37 | | JS_DEFINE_ALLOCATOR(HTMLLinkElement); |
38 | | |
39 | | HTMLLinkElement::HTMLLinkElement(DOM::Document& document, DOM::QualifiedName qualified_name) |
40 | 0 | : HTMLElement(document, move(qualified_name)) |
41 | 0 | { |
42 | 0 | } |
43 | | |
44 | 0 | HTMLLinkElement::~HTMLLinkElement() = default; |
45 | | |
46 | | void HTMLLinkElement::initialize(JS::Realm& realm) |
47 | 0 | { |
48 | 0 | Base::initialize(realm); |
49 | 0 | WEB_SET_PROTOTYPE_FOR_INTERFACE(HTMLLinkElement); |
50 | 0 | } |
51 | | |
52 | | void HTMLLinkElement::removed_from(Node* old_parent) |
53 | 0 | { |
54 | 0 | Base::removed_from(old_parent); |
55 | 0 | if (m_loaded_style_sheet) { |
56 | 0 | document_or_shadow_root_style_sheets().remove_a_css_style_sheet(*m_loaded_style_sheet); |
57 | 0 | m_loaded_style_sheet = nullptr; |
58 | 0 | } |
59 | 0 | } |
60 | | |
61 | | void HTMLLinkElement::inserted() |
62 | 0 | { |
63 | 0 | HTMLElement::inserted(); |
64 | |
|
65 | 0 | if (!document().browsing_context()) { |
66 | 0 | return; |
67 | 0 | } |
68 | | |
69 | 0 | if (m_relationship & Relationship::Stylesheet) { |
70 | | // https://html.spec.whatwg.org/multipage/links.html#link-type-stylesheet:fetch-and-process-the-linked-resource |
71 | | // The appropriate times to fetch and process this type of link are: |
72 | | // - When the external resource link is created on a link element that is already browsing-context connected. |
73 | | // - When the external resource link's link element becomes browsing-context connected. |
74 | 0 | fetch_and_process_linked_resource(); |
75 | 0 | } |
76 | | |
77 | | // FIXME: Follow spec for fetching and processing these attributes as well |
78 | 0 | if (m_relationship & Relationship::Preload) { |
79 | | // FIXME: Respect the "as" attribute. |
80 | 0 | LoadRequest request; |
81 | 0 | request.set_url(document().parse_url(get_attribute_value(HTML::AttributeNames::href))); |
82 | 0 | set_resource(ResourceLoader::the().load_resource(Resource::Type::Generic, request)); |
83 | 0 | } else if (m_relationship & Relationship::DNSPrefetch) { |
84 | 0 | ResourceLoader::the().prefetch_dns(document().parse_url(get_attribute_value(HTML::AttributeNames::href))); |
85 | 0 | } else if (m_relationship & Relationship::Preconnect) { |
86 | 0 | ResourceLoader::the().preconnect(document().parse_url(get_attribute_value(HTML::AttributeNames::href))); |
87 | 0 | } else if (m_relationship & Relationship::Icon) { |
88 | 0 | auto favicon_url = document().parse_url(href()); |
89 | 0 | auto favicon_request = LoadRequest::create_for_url_on_page(favicon_url, &document().page()); |
90 | 0 | set_resource(ResourceLoader::the().load_resource(Resource::Type::Generic, favicon_request)); |
91 | 0 | } |
92 | 0 | } |
93 | | |
94 | | // https://html.spec.whatwg.org/multipage/semantics.html#dom-link-as |
95 | | String HTMLLinkElement::as() const |
96 | 0 | { |
97 | 0 | String attribute_value = get_attribute_value(HTML::AttributeNames::as); |
98 | |
|
99 | 0 | if (attribute_value.equals_ignoring_ascii_case("fetch"sv) |
100 | 0 | || attribute_value.equals_ignoring_ascii_case("image"sv) |
101 | 0 | || attribute_value.equals_ignoring_ascii_case("script"sv) |
102 | 0 | || attribute_value.equals_ignoring_ascii_case("style"sv) |
103 | 0 | || attribute_value.equals_ignoring_ascii_case("video"sv) |
104 | 0 | || attribute_value.equals_ignoring_ascii_case("audio"sv) |
105 | 0 | || attribute_value.equals_ignoring_ascii_case("track"sv) |
106 | 0 | || attribute_value.equals_ignoring_ascii_case("font"sv)) |
107 | 0 | return attribute_value.to_lowercase().release_value(); |
108 | | |
109 | 0 | return String {}; |
110 | 0 | } |
111 | | |
112 | | WebIDL::ExceptionOr<void> HTMLLinkElement::set_as(String const& value) |
113 | 0 | { |
114 | 0 | return set_attribute(HTML::AttributeNames::as, move(value)); |
115 | 0 | } |
116 | | |
117 | | // https://html.spec.whatwg.org/multipage/semantics.html#dom-link-rellist |
118 | | JS::NonnullGCPtr<DOM::DOMTokenList> HTMLLinkElement::rel_list() |
119 | 0 | { |
120 | | // The relList IDL attribute must reflect the rel content attribute. |
121 | 0 | if (!m_rel_list) |
122 | 0 | m_rel_list = DOM::DOMTokenList::create(*this, HTML::AttributeNames::rel); |
123 | 0 | return *m_rel_list; |
124 | 0 | } |
125 | | |
126 | | bool HTMLLinkElement::has_loaded_icon() const |
127 | 0 | { |
128 | 0 | return m_relationship & Relationship::Icon && resource() && resource()->is_loaded() && resource()->has_encoded_data(); |
129 | 0 | } |
130 | | |
131 | | void HTMLLinkElement::attribute_changed(FlyString const& name, Optional<String> const& old_value, Optional<String> const& value) |
132 | 0 | { |
133 | 0 | HTMLElement::attribute_changed(name, old_value, value); |
134 | | |
135 | | // 4.6.7 Link types - https://html.spec.whatwg.org/multipage/links.html#linkTypes |
136 | 0 | if (name == HTML::AttributeNames::rel) { |
137 | 0 | m_relationship = 0; |
138 | | // Keywords are always ASCII case-insensitive, and must be compared as such. |
139 | 0 | auto lowercased_value = value.value_or(String {}).to_ascii_lowercase(); |
140 | | // To determine which link types apply to a link, a, area, or form element, |
141 | | // the element's rel attribute must be split on ASCII whitespace. |
142 | | // The resulting tokens are the keywords for the link types that apply to that element. |
143 | 0 | auto parts = lowercased_value.bytes_as_string_view().split_view_if(Infra::is_ascii_whitespace); |
144 | 0 | for (auto& part : parts) { |
145 | 0 | if (part == "stylesheet"sv) |
146 | 0 | m_relationship |= Relationship::Stylesheet; |
147 | 0 | else if (part == "alternate"sv) |
148 | 0 | m_relationship |= Relationship::Alternate; |
149 | 0 | else if (part == "preload"sv) |
150 | 0 | m_relationship |= Relationship::Preload; |
151 | 0 | else if (part == "dns-prefetch"sv) |
152 | 0 | m_relationship |= Relationship::DNSPrefetch; |
153 | 0 | else if (part == "preconnect"sv) |
154 | 0 | m_relationship |= Relationship::Preconnect; |
155 | 0 | else if (part == "icon"sv) |
156 | 0 | m_relationship |= Relationship::Icon; |
157 | 0 | } |
158 | |
|
159 | 0 | if (m_rel_list) |
160 | 0 | m_rel_list->associated_attribute_changed(value.value_or(String {})); |
161 | 0 | } |
162 | | |
163 | | // https://html.spec.whatwg.org/multipage/semantics.html#the-link-element:explicitly-enabled |
164 | | // Whenever the disabled attribute is removed, set the link element's explicitly enabled attribute to true. |
165 | 0 | if (!value.has_value() && name == HTML::AttributeNames::disabled) |
166 | 0 | m_explicitly_enabled = true; |
167 | |
|
168 | 0 | if (m_relationship & Relationship::Stylesheet) { |
169 | 0 | if (name == HTML::AttributeNames::disabled && m_loaded_style_sheet) |
170 | 0 | document_or_shadow_root_style_sheets().remove_a_css_style_sheet(*m_loaded_style_sheet); |
171 | | |
172 | | // https://html.spec.whatwg.org/multipage/links.html#link-type-stylesheet:fetch-and-process-the-linked-resource |
173 | | // The appropriate times to fetch and process this type of link are: |
174 | 0 | if ( |
175 | 0 | is_browsing_context_connected() |
176 | 0 | && ( |
177 | | // AD-HOC: When the rel attribute changes |
178 | 0 | name == AttributeNames::rel || |
179 | | // - When the href attribute of the link element of an external resource link that is already browsing-context connected is changed. |
180 | 0 | name == AttributeNames::href || |
181 | | // - When the disabled attribute of the link element of an external resource link that is already browsing-context connected is set, changed, or removed. |
182 | 0 | name == AttributeNames::disabled || |
183 | | // - When the crossorigin attribute of the link element of an external resource link that is already browsing-context connected is set, changed, or removed. |
184 | 0 | name == AttributeNames::crossorigin |
185 | | // 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. |
186 | | // 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. |
187 | 0 | )) { |
188 | 0 | fetch_and_process_linked_resource(); |
189 | 0 | } |
190 | |
|
191 | 0 | if (name == HTML::AttributeNames::media && m_loaded_style_sheet) { |
192 | 0 | m_loaded_style_sheet->set_media(value.value_or(String {})); |
193 | 0 | } |
194 | 0 | } |
195 | 0 | } |
196 | | |
197 | | void HTMLLinkElement::resource_did_fail() |
198 | 0 | { |
199 | 0 | dbgln_if(CSS_LOADER_DEBUG, "HTMLLinkElement: Resource did fail. URL: {}", resource()->url()); |
200 | 0 | if (m_relationship & Relationship::Preload) { |
201 | 0 | dispatch_event(*DOM::Event::create(realm(), HTML::EventNames::error)); |
202 | 0 | } |
203 | 0 | } |
204 | | |
205 | | void HTMLLinkElement::resource_did_load() |
206 | 0 | { |
207 | 0 | VERIFY(resource()); |
208 | 0 | if (m_relationship & Relationship::Icon) { |
209 | 0 | resource_did_load_favicon(); |
210 | 0 | m_document_load_event_delayer.clear(); |
211 | 0 | } |
212 | 0 | if (m_relationship & Relationship::Preload) { |
213 | 0 | dispatch_event(*DOM::Event::create(realm(), HTML::EventNames::load)); |
214 | 0 | } |
215 | 0 | } |
216 | | |
217 | | // https://html.spec.whatwg.org/multipage/semantics.html#create-link-options-from-element |
218 | | HTMLLinkElement::LinkProcessingOptions HTMLLinkElement::create_link_options() |
219 | 0 | { |
220 | | // 1. Let document be el's node document. |
221 | 0 | auto& document = this->document(); |
222 | | |
223 | | // 2. Let options be a new link processing options with |
224 | 0 | LinkProcessingOptions options; |
225 | | // FIXME: destination the result of translating the state of el's as attribute |
226 | | // crossorigin the state of el's crossorigin content attribute |
227 | 0 | options.crossorigin = cors_setting_attribute_from_keyword(get_attribute(AttributeNames::crossorigin)); |
228 | | // referrer policy the state of el's referrerpolicy content attribute |
229 | 0 | options.referrer_policy = ReferrerPolicy::from_string(get_attribute(AttributeNames::referrerpolicy).value_or(""_string)).value_or(ReferrerPolicy::ReferrerPolicy::EmptyString); |
230 | | // FIXME: source set el's source set |
231 | | // base URL document's document base URL |
232 | 0 | options.base_url = document.base_url(); |
233 | | // origin document's origin |
234 | 0 | options.origin = document.origin(); |
235 | | // environment document's relevant settings object |
236 | 0 | options.environment = &document.relevant_settings_object(); |
237 | | // policy container document's policy container |
238 | 0 | options.policy_container = document.policy_container(); |
239 | | // document document |
240 | 0 | options.document = &document; |
241 | | // FIXME: cryptographic nonce metadata The current value of el's [[CryptographicNonce]] internal slot |
242 | | // fetch priority the state of el's fetchpriority content attribute |
243 | 0 | options.fetch_priority = Fetch::Infrastructure::request_priority_from_string(get_attribute_value(HTML::AttributeNames::fetchpriority)).value_or(Fetch::Infrastructure::Request::Priority::Auto); |
244 | | |
245 | | // 3. If el has an href attribute, then set options's href to the value of el's href attribute. |
246 | 0 | if (auto maybe_href = get_attribute(AttributeNames::href); maybe_href.has_value()) |
247 | 0 | options.href = maybe_href.value(); |
248 | | |
249 | | // 4. If el has an integrity attribute, then set options's integrity to the value of el's integrity content attribute. |
250 | 0 | if (auto maybe_integrity = get_attribute(AttributeNames::integrity); maybe_integrity.has_value()) |
251 | 0 | options.integrity = maybe_integrity.value(); |
252 | | |
253 | | // 5. If el has a type attribute, then set options's type to the value of el's type attribute. |
254 | 0 | if (auto maybe_type = get_attribute(AttributeNames::type); maybe_type.has_value()) |
255 | 0 | options.type = maybe_type.value(); |
256 | | |
257 | | // FIXME: 6. Assert: options's href is not the empty string, or options's source set is not null. |
258 | | // A link element with neither an href or an imagesrcset does not represent a link. |
259 | | |
260 | | // 7. Return options. |
261 | 0 | return options; |
262 | 0 | } |
263 | | |
264 | | // https://html.spec.whatwg.org/multipage/semantics.html#create-a-link-request |
265 | | JS::GCPtr<Fetch::Infrastructure::Request> HTMLLinkElement::create_link_request(HTMLLinkElement::LinkProcessingOptions const& options) |
266 | 0 | { |
267 | | // 1. Assert: options's href is not the empty string. |
268 | 0 | VERIFY(!options.href.is_empty()); |
269 | | |
270 | | // FIXME: 2. If options's destination is null, then return null. |
271 | | |
272 | | // 3. Let url be the result of encoding-parsing a URL given options's href, relative to options's base URL. |
273 | 0 | auto url = options.base_url.complete_url(options.href); |
274 | | |
275 | | // 4. If url is failure, then return null. |
276 | 0 | if (!url.is_valid()) |
277 | 0 | return nullptr; |
278 | | |
279 | | // 5. Let request be the result of creating a potential-CORS request given url, options's destination, and options's crossorigin. |
280 | 0 | auto request = create_potential_CORS_request(vm(), url, options.destination, options.crossorigin); |
281 | | |
282 | | // 6. Set request's policy container to options's policy container. |
283 | 0 | request->set_policy_container(options.policy_container); |
284 | | |
285 | | // 7. Set request's integrity metadata to options's integrity. |
286 | 0 | request->set_integrity_metadata(options.integrity); |
287 | | |
288 | | // 8. Set request's cryptographic nonce metadata to options's cryptographic nonce metadata. |
289 | 0 | request->set_cryptographic_nonce_metadata(options.cryptographic_nonce_metadata); |
290 | | |
291 | | // 9. Set request's referrer policy to options's referrer policy. |
292 | 0 | request->set_referrer_policy(options.referrer_policy); |
293 | | |
294 | | // 10. Set request's client to options's environment. |
295 | 0 | request->set_client(options.environment); |
296 | | |
297 | | // 11. Set request's priority to options's fetch priority. |
298 | 0 | request->set_priority(options.fetch_priority); |
299 | | |
300 | | // 12. Return request. |
301 | 0 | return request; |
302 | 0 | } |
303 | | |
304 | | // https://html.spec.whatwg.org/multipage/semantics.html#fetch-and-process-the-linked-resource |
305 | | void HTMLLinkElement::fetch_and_process_linked_resource() |
306 | 0 | { |
307 | 0 | default_fetch_and_process_linked_resource(); |
308 | 0 | } |
309 | | |
310 | | // https://html.spec.whatwg.org/multipage/semantics.html#default-fetch-and-process-the-linked-resource |
311 | | void HTMLLinkElement::default_fetch_and_process_linked_resource() |
312 | 0 | { |
313 | | // https://html.spec.whatwg.org/multipage/semantics.html#the-link-element:attr-link-href-4 |
314 | | // If both the href and imagesrcset attributes are absent, then the element does not define a link. |
315 | | // FIXME: Support imagesrcset attribute |
316 | 0 | if (!has_attribute(AttributeNames::href) || href().is_empty()) |
317 | 0 | return; |
318 | | |
319 | | // 1. Let options be the result of creating link options from el. |
320 | 0 | auto options = create_link_options(); |
321 | | |
322 | | // 2. Let request be the result of creating a link request given options. |
323 | 0 | auto request = create_link_request(options); |
324 | | |
325 | | // 3. If request is null, then return. |
326 | 0 | if (request == nullptr) { |
327 | 0 | return; |
328 | 0 | } |
329 | | |
330 | | // FIXME: 4. Set request's synchronous flag. |
331 | | |
332 | | // 5. Run the linked resource fetch setup steps, given el and request. If the result is false, then return. |
333 | 0 | if (!linked_resource_fetch_setup_steps(*request)) |
334 | 0 | return; |
335 | | |
336 | | // 6. Set request's initiator type to "css" if el's rel attribute contains the keyword stylesheet; "link" otherwise. |
337 | 0 | if (m_relationship & Relationship::Stylesheet) { |
338 | 0 | request->set_initiator_type(Fetch::Infrastructure::Request::InitiatorType::CSS); |
339 | 0 | } else { |
340 | 0 | request->set_initiator_type(Fetch::Infrastructure::Request::InitiatorType::Link); |
341 | 0 | } |
342 | | |
343 | | // 7. Fetch request with processResponseConsumeBody set to the following steps given response response and null, failure, or a byte sequence bodyBytes: |
344 | 0 | Fetch::Infrastructure::FetchAlgorithms::Input fetch_algorithms_input {}; |
345 | 0 | fetch_algorithms_input.process_response_consume_body = [this, hr = options](auto response, auto body_bytes) { |
346 | | // FIXME: If the response is CORS cross-origin, we must use its internal response to query any of its data. See: |
347 | | // https://github.com/whatwg/html/issues/9355 |
348 | 0 | response = response->unsafe_response(); |
349 | | |
350 | | // 1. Let success be true. |
351 | 0 | bool success = true; |
352 | | |
353 | | // 2. If either of the following conditions are met: |
354 | | // - bodyBytes is null or failure; or |
355 | | // - response's status is not an ok status, |
356 | 0 | if (body_bytes.template has<Empty>() || body_bytes.template has<Fetch::Infrastructure::FetchAlgorithms::ConsumeBodyFailureTag>() || !Fetch::Infrastructure::is_ok_status(response->status())) { |
357 | | // then set success to false. |
358 | 0 | success = false; |
359 | 0 | } |
360 | | |
361 | | // FIXME: 3. Otherwise, wait for the link resource's critical subresources to finish loading. |
362 | | |
363 | | // 4. Process the linked resource given el, success, response, and bodyBytes. |
364 | 0 | process_linked_resource(success, response, body_bytes); |
365 | 0 | }; |
366 | |
|
367 | 0 | if (m_fetch_controller) |
368 | 0 | m_fetch_controller->abort(realm(), {}); |
369 | 0 | m_fetch_controller = MUST(Fetch::Fetching::fetch(realm(), *request, Fetch::Infrastructure::FetchAlgorithms::create(vm(), move(fetch_algorithms_input)))); |
370 | 0 | } |
371 | | |
372 | | // https://html.spec.whatwg.org/multipage/links.html#link-type-stylesheet:process-the-linked-resource |
373 | | void HTMLLinkElement::process_stylesheet_resource(bool success, Fetch::Infrastructure::Response const& response, Variant<Empty, Fetch::Infrastructure::FetchAlgorithms::ConsumeBodyFailureTag, ByteBuffer> body_bytes) |
374 | 0 | { |
375 | | // 1. If the resource's Content-Type metadata is not text/css, then set success to false. |
376 | 0 | auto extracted_mime_type = response.header_list()->extract_mime_type(); |
377 | 0 | if (!extracted_mime_type.has_value() || extracted_mime_type->essence() != "text/css") { |
378 | 0 | success = false; |
379 | 0 | } |
380 | | |
381 | | // FIXME: 2. If el no longer creates an external resource link that contributes to the styling processing model, |
382 | | // or if, since the resource in question was fetched, it has become appropriate to fetch it again, then return. |
383 | | |
384 | | // 3. If el has an associated CSS style sheet, remove the CSS style sheet. |
385 | 0 | if (m_loaded_style_sheet) { |
386 | 0 | document_or_shadow_root_style_sheets().remove_a_css_style_sheet(*m_loaded_style_sheet); |
387 | 0 | m_loaded_style_sheet = nullptr; |
388 | 0 | } |
389 | | |
390 | | // 4. If success is true, then: |
391 | 0 | if (success) { |
392 | | // 1. Create a CSS style sheet with the following properties: |
393 | | // type |
394 | | // text/css |
395 | | // location |
396 | | // response's URL list[0] |
397 | | // owner node |
398 | | // element |
399 | | // media |
400 | | // The media attribute of element. |
401 | | // title |
402 | | // The title attribute of element, if element is in a document tree, or the empty string otherwise. |
403 | | // alternate flag |
404 | | // Set if the link is an alternative style sheet and element's explicitly enabled is false; unset otherwise. |
405 | | // origin-clean flag |
406 | | // Set if the resource is CORS-same-origin; unset otherwise. |
407 | | // parent CSS style sheet |
408 | | // owner CSS rule |
409 | | // null |
410 | | // disabled flag |
411 | | // Left at its default value. |
412 | | // CSS rules |
413 | | // Left uninitialized. |
414 | | // |
415 | | // The CSS environment encoding is the result of running the following steps: [CSSSYNTAX] |
416 | | // 1. If the element has a charset attribute, get an encoding from that attribute's value. If that succeeds, return the resulting encoding. [ENCODING] |
417 | | // 2. Otherwise, return the document's character encoding. [DOM] |
418 | |
|
419 | 0 | Optional<String> encoding; |
420 | 0 | if (auto charset = attribute(HTML::AttributeNames::charset); charset.has_value()) |
421 | 0 | encoding = charset.release_value(); |
422 | |
|
423 | 0 | if (!encoding.has_value()) |
424 | 0 | encoding = document().encoding_or_default(); |
425 | |
|
426 | 0 | auto decoder = TextCodec::decoder_for(*encoding); |
427 | |
|
428 | 0 | if (!decoder.has_value()) { |
429 | | // If we don't support the encoding yet, let's error out instead of trying to decode it as something it's most likely not. |
430 | 0 | dbgln("FIXME: Style sheet encoding '{}' is not supported yet", encoding); |
431 | 0 | dispatch_event(*DOM::Event::create(realm(), HTML::EventNames::error)); |
432 | 0 | } else { |
433 | 0 | auto const& encoded_string = body_bytes.get<ByteBuffer>(); |
434 | 0 | auto maybe_decoded_string = TextCodec::convert_input_to_utf8_using_given_decoder_unless_there_is_a_byte_order_mark(*decoder, encoded_string); |
435 | 0 | if (maybe_decoded_string.is_error()) { |
436 | 0 | dbgln("Style sheet {} claimed to be '{}' but decoding failed", response.url().value_or(URL::URL()), encoding); |
437 | 0 | dispatch_event(*DOM::Event::create(realm(), HTML::EventNames::error)); |
438 | 0 | } else { |
439 | 0 | auto const decoded_string = maybe_decoded_string.release_value(); |
440 | 0 | m_loaded_style_sheet = parse_css_stylesheet(CSS::Parser::ParsingContext(document(), *response.url()), decoded_string); |
441 | |
|
442 | 0 | if (m_loaded_style_sheet) { |
443 | 0 | Optional<String> location; |
444 | 0 | if (!response.url_list().is_empty()) |
445 | 0 | location = MUST(response.url_list().first().to_string()); |
446 | |
|
447 | 0 | document().style_sheets().create_a_css_style_sheet( |
448 | 0 | "text/css"_string, |
449 | 0 | this, |
450 | 0 | attribute(HTML::AttributeNames::media).value_or({}), |
451 | 0 | in_a_document_tree() ? attribute(HTML::AttributeNames::title).value_or({}) : String {}, |
452 | 0 | m_relationship & Relationship::Alternate && !m_explicitly_enabled, |
453 | 0 | true, |
454 | 0 | move(location), |
455 | 0 | nullptr, |
456 | 0 | nullptr, |
457 | 0 | *m_loaded_style_sheet); |
458 | 0 | } else { |
459 | 0 | dbgln_if(CSS_LOADER_DEBUG, "HTMLLinkElement: Failed to parse stylesheet: {}", resource()->url()); |
460 | 0 | } |
461 | | |
462 | | // 2. Fire an event named load at el. |
463 | 0 | dispatch_event(*DOM::Event::create(realm(), HTML::EventNames::load)); |
464 | 0 | } |
465 | 0 | } |
466 | 0 | } |
467 | | // 5. Otherwise, fire an event named error at el. |
468 | 0 | else { |
469 | 0 | dispatch_event(*DOM::Event::create(realm(), HTML::EventNames::error)); |
470 | 0 | } |
471 | | |
472 | | // FIXME: 6. If el contributes a script-blocking style sheet, then: |
473 | | // FIXME: 1. Assert: el's node document's script-blocking style sheet counter is greater than 0. |
474 | | // FIXME: 2. Decrement el's node document's script-blocking style sheet counter by 1. |
475 | | |
476 | | // 7. Unblock rendering on el. |
477 | 0 | m_document_load_event_delayer.clear(); |
478 | 0 | } |
479 | | |
480 | | // https://html.spec.whatwg.org/multipage/semantics.html#process-the-linked-resource |
481 | | void HTMLLinkElement::process_linked_resource(bool success, Fetch::Infrastructure::Response const& response, Variant<Empty, Fetch::Infrastructure::FetchAlgorithms::ConsumeBodyFailureTag, ByteBuffer> body_bytes) |
482 | 0 | { |
483 | 0 | if (m_relationship & Relationship::Stylesheet) |
484 | 0 | process_stylesheet_resource(success, response, body_bytes); |
485 | 0 | } |
486 | | |
487 | | // https://html.spec.whatwg.org/multipage/semantics.html#linked-resource-fetch-setup-steps |
488 | | bool HTMLLinkElement::linked_resource_fetch_setup_steps(Fetch::Infrastructure::Request& request) |
489 | 0 | { |
490 | 0 | if (m_relationship & Relationship::Stylesheet) |
491 | 0 | return stylesheet_linked_resource_fetch_setup_steps(request); |
492 | | |
493 | 0 | return true; |
494 | 0 | } |
495 | | |
496 | | // https://html.spec.whatwg.org/multipage/links.html#link-type-stylesheet:linked-resource-fetch-setup-steps |
497 | | bool HTMLLinkElement::stylesheet_linked_resource_fetch_setup_steps(Fetch::Infrastructure::Request& request) |
498 | 0 | { |
499 | | // 1. If el's disabled attribute is set, then return false. |
500 | 0 | if (has_attribute(AttributeNames::disabled)) |
501 | 0 | return false; |
502 | | // FIXME: 2. If el contributes a script-blocking style sheet, increment el's node document's script-blocking style sheet counter by 1. |
503 | | |
504 | | // 3. If el's media attribute's value matches the environment and el is potentially render-blocking, then block rendering on el. |
505 | | // FIXME: Check media attribute value. |
506 | 0 | m_document_load_event_delayer.emplace(document()); |
507 | | |
508 | | // 4. If el is currently render-blocking, then set request's render-blocking to true. |
509 | | // FIXME: Check if el is currently render-blocking. |
510 | 0 | request.set_render_blocking(true); |
511 | | |
512 | | // 5. Return true. |
513 | 0 | return true; |
514 | 0 | } |
515 | | |
516 | | void HTMLLinkElement::resource_did_load_favicon() |
517 | 0 | { |
518 | 0 | VERIFY(m_relationship & (Relationship::Icon)); |
519 | 0 | if (!resource()->has_encoded_data()) { |
520 | 0 | dbgln_if(SPAM_DEBUG, "Favicon downloaded, no encoded data"); |
521 | 0 | return; |
522 | 0 | } |
523 | | |
524 | 0 | dbgln_if(SPAM_DEBUG, "Favicon downloaded, {} bytes from {}", resource()->encoded_data().size(), resource()->url()); |
525 | |
|
526 | 0 | document().check_favicon_after_loading_link_resource(); |
527 | 0 | } |
528 | | |
529 | | static NonnullRefPtr<Core::Promise<Web::Platform::DecodedImage>> decode_favicon(ReadonlyBytes favicon_data, URL::URL const& favicon_url, JS::NonnullGCPtr<DOM::Document> document) |
530 | 0 | { |
531 | 0 | auto on_failed_decode = [favicon_url]([[maybe_unused]] Error& error) { |
532 | 0 | dbgln_if(IMAGE_DECODER_DEBUG, "Failed to decode favicon {}: {}", favicon_url, error); |
533 | 0 | }; |
534 | |
|
535 | 0 | auto on_successful_decode = [document = JS::Handle(document)](Web::Platform::DecodedImage& decoded_image) -> ErrorOr<void> { |
536 | 0 | auto favicon_bitmap = decoded_image.frames[0].bitmap; |
537 | 0 | dbgln_if(IMAGE_DECODER_DEBUG, "Decoded favicon, {}", favicon_bitmap->size()); |
538 | |
|
539 | 0 | auto navigable = document->navigable(); |
540 | 0 | if (navigable && navigable->is_traversable()) |
541 | 0 | navigable->traversable_navigable()->page().client().page_did_change_favicon(*favicon_bitmap); |
542 | |
|
543 | 0 | return {}; |
544 | 0 | }; |
545 | |
|
546 | 0 | auto promise = Platform::ImageCodecPlugin::the().decode_image(favicon_data, move(on_successful_decode), move(on_failed_decode)); |
547 | |
|
548 | 0 | return promise; |
549 | 0 | } |
550 | | |
551 | | bool HTMLLinkElement::load_favicon_and_use_if_window_is_active() |
552 | 0 | { |
553 | 0 | if (!has_loaded_icon()) |
554 | 0 | return false; |
555 | | |
556 | | // FIXME: Refactor the caller(s) to handle the async nature of image loading |
557 | 0 | auto promise = decode_favicon(resource()->encoded_data(), resource()->url(), document()); |
558 | 0 | auto result = promise->await(); |
559 | 0 | return !result.is_error(); |
560 | 0 | } |
561 | | |
562 | | // https://html.spec.whatwg.org/multipage/links.html#rel-icon:the-link-element-3 |
563 | | WebIDL::ExceptionOr<void> HTMLLinkElement::load_fallback_favicon_if_needed(JS::NonnullGCPtr<DOM::Document> document) |
564 | 0 | { |
565 | 0 | auto& realm = document->realm(); |
566 | 0 | auto& vm = realm.vm(); |
567 | | |
568 | | // In the absence of a link with the icon keyword, for Document objects whose URL's scheme is an HTTP(S) scheme, |
569 | | // user agents may instead run these steps in parallel: |
570 | 0 | if (document->has_active_favicon()) |
571 | 0 | return {}; |
572 | 0 | if (!document->url().scheme().is_one_of("http"sv, "https"sv)) |
573 | 0 | return {}; |
574 | | |
575 | | // 1. Let request be a new request whose URL is the URL record obtained by resolving the URL "/favicon.ico" against |
576 | | // the Document object's URL, client is the Document object's relevant settings object, destination is "image", |
577 | | // synchronous flag is set, credentials mode is "include", and whose use-URL-credentials flag is set. |
578 | | // NOTE: Fetch requests no longer have a synchronous flag, see https://github.com/whatwg/fetch/pull/1165 |
579 | 0 | auto request = Fetch::Infrastructure::Request::create(vm); |
580 | 0 | request->set_url(document->parse_url("/favicon.ico"sv)); |
581 | 0 | request->set_client(&document->relevant_settings_object()); |
582 | 0 | request->set_destination(Fetch::Infrastructure::Request::Destination::Image); |
583 | 0 | request->set_credentials_mode(Fetch::Infrastructure::Request::CredentialsMode::Include); |
584 | 0 | request->set_use_url_credentials(true); |
585 | | |
586 | | // 2. Let response be the result of fetching request. |
587 | 0 | Fetch::Infrastructure::FetchAlgorithms::Input fetch_algorithms_input {}; |
588 | 0 | fetch_algorithms_input.process_response = [document, request](JS::NonnullGCPtr<Fetch::Infrastructure::Response> response) { |
589 | 0 | auto& realm = document->realm(); |
590 | 0 | auto global = JS::NonnullGCPtr { realm.global_object() }; |
591 | |
|
592 | 0 | auto process_body = JS::create_heap_function(realm.heap(), [document, request](ByteBuffer body) { |
593 | 0 | (void)decode_favicon(body, request->url(), document); |
594 | 0 | }); |
595 | 0 | auto process_body_error = JS::create_heap_function(realm.heap(), [](JS::Value) { |
596 | 0 | }); |
597 | | |
598 | | // Check for failed favicon response |
599 | 0 | if (!Fetch::Infrastructure::is_ok_status(response->status()) || !response->body()) { |
600 | 0 | return; |
601 | 0 | } |
602 | | |
603 | | // 3. Use response's unsafe response as an icon as if it had been declared using the icon keyword. |
604 | 0 | if (auto body = response->unsafe_response()->body()) |
605 | 0 | body->fully_read(realm, process_body, process_body_error, global); |
606 | 0 | }; |
607 | |
|
608 | 0 | TRY(Fetch::Fetching::fetch(realm, request, Fetch::Infrastructure::FetchAlgorithms::create(vm, move(fetch_algorithms_input)))); |
609 | 0 | return {}; |
610 | 0 | } |
611 | | |
612 | | void HTMLLinkElement::visit_edges(Cell::Visitor& visitor) |
613 | 0 | { |
614 | 0 | Base::visit_edges(visitor); |
615 | 0 | visitor.visit(m_fetch_controller); |
616 | 0 | visitor.visit(m_loaded_style_sheet); |
617 | 0 | visitor.visit(m_rel_list); |
618 | 0 | } |
619 | | |
620 | | } |