diff options
Diffstat (limited to 'components/script/dom/htmllinkelement.rs')
-rw-r--r-- | components/script/dom/htmllinkelement.rs | 336 |
1 files changed, 328 insertions, 8 deletions
diff --git a/components/script/dom/htmllinkelement.rs b/components/script/dom/htmllinkelement.rs index f4e7683cf2a..41edb2c22d3 100644 --- a/components/script/dom/htmllinkelement.rs +++ b/components/script/dom/htmllinkelement.rs @@ -5,6 +5,7 @@ use std::borrow::{Borrow, ToOwned}; use std::cell::Cell; use std::default::Default; +use std::str::FromStr; use base::id::WebViewId; use content_security_policy as csp; @@ -12,6 +13,8 @@ use dom_struct::dom_struct; use embedder_traits::EmbedderMsg; use html5ever::{LocalName, Prefix, local_name, ns}; use js::rust::HandleObject; +use mime::Mime; +use net_traits::mime_classifier::{MediaType, MimeClassifier}; use net_traits::policy_container::PolicyContainer; use net_traits::request::{ CorsSettings, Destination, Initiator, InsecureRequestsPolicy, Referrer, RequestBuilder, @@ -22,7 +25,7 @@ use net_traits::{ ResourceTimingType, }; use servo_arc::Arc; -use servo_url::ServoUrl; +use servo_url::{ImmutableOrigin, ServoUrl}; use style::attr::AttrValue; use style::stylesheets::Stylesheet; use stylo_atoms::Atom; @@ -78,6 +81,7 @@ struct LinkProcessingOptions { policy_container: PolicyContainer, source_set: Option<()>, base_url: ServoUrl, + origin: ImmutableOrigin, insecure_requests_policy: InsecureRequestsPolicy, has_trustworthy_ancestor_origin: bool, // Some fields that we don't need yet are missing @@ -113,6 +117,10 @@ pub(crate) struct HTMLLinkElement { request_generation_id: Cell<RequestGenerationId>, /// <https://html.spec.whatwg.org/multipage/#explicitly-enabled> is_explicitly_enabled: Cell<bool>, + /// Whether the previous type matched with the destination + previous_type_matched: Cell<bool>, + /// Whether the previous media environment matched with the media query + previous_media_environment_matched: Cell<bool>, } impl HTMLLinkElement { @@ -133,6 +141,8 @@ impl HTMLLinkElement { any_failed_load: Cell::new(false), request_generation_id: Cell::new(RequestGenerationId(0)), is_explicitly_enabled: Cell::new(false), + previous_type_matched: Cell::new(true), + previous_media_environment_matched: Cell::new(true), } } @@ -236,7 +246,7 @@ impl VirtualMethods for HTMLLinkElement { return; } - if !self.upcast::<Node>().is_connected() || is_removal { + if !self.upcast::<Node>().is_connected() { return; } match *local_name { @@ -245,6 +255,12 @@ impl VirtualMethods for HTMLLinkElement { .set(LinkRelations::for_element(self.upcast())); }, local_name!("href") => { + if is_removal { + return; + } + // https://html.spec.whatwg.org/multipage/#link-type-stylesheet + // When the href attribute of the link element of an external resource link + // that is already browsing-context connected is changed. if self.relations.get().contains(LinkRelations::STYLESHEET) { self.handle_stylesheet_url(&attr.value()); } @@ -254,9 +270,19 @@ impl VirtualMethods for HTMLLinkElement { self.handle_favicon_url(&attr.value(), &sizes); } + // https://html.spec.whatwg.org/multipage/#link-type-prefetch + // When the href attribute of the link element of an external resource link + // that is already browsing-context connected is changed. if self.relations.get().contains(LinkRelations::PREFETCH) { self.fetch_and_process_prefetch_link(&attr.value()); } + + // https://html.spec.whatwg.org/multipage/#link-type-preload + // When the href attribute of the link element of an external resource link + // that is already browsing-context connected is changed. + if self.relations.get().contains(LinkRelations::PRELOAD) { + self.handle_preload_url(); + } }, local_name!("sizes") if self.relations.get().contains(LinkRelations::ICON) => { if let Some(ref href) = get_attr(self.upcast(), &local_name!("href")) { @@ -264,9 +290,73 @@ impl VirtualMethods for HTMLLinkElement { } }, local_name!("crossorigin") => { + // https://html.spec.whatwg.org/multipage/#link-type-prefetch + // When the crossorigin attribute of the link element of an external resource link + // that is already browsing-context connected is set, changed, or removed. if self.relations.get().contains(LinkRelations::PREFETCH) { self.fetch_and_process_prefetch_link(&attr.value()); } + + // https://html.spec.whatwg.org/multipage/#link-type-stylesheet + // When the crossorigin attribute of the link element of an external resource link + // that is already browsing-context connected is set, changed, or removed. + if self.relations.get().contains(LinkRelations::STYLESHEET) { + self.handle_stylesheet_url(&attr.value()); + } + }, + local_name!("as") => { + // https://html.spec.whatwg.org/multipage/#link-type-preload + // When the as attribute of the link element of an external resource link + // that is already browsing-context connected is changed. + if self.relations.get().contains(LinkRelations::PRELOAD) { + if let AttributeMutation::Set(Some(_)) = mutation { + self.handle_preload_url(); + } + } + }, + local_name!("type") => { + // https://html.spec.whatwg.org/multipage/#link-type-stylesheet + // 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. + // + // TODO: Match Content-Type metadata to check if it needs to be updated + if self.relations.get().contains(LinkRelations::STYLESHEET) { + self.handle_stylesheet_url(&attr.value()); + } + + // https://html.spec.whatwg.org/multipage/#link-type-preload + // 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 for the request destination, + // is set, removed, or changed. + if self.relations.get().contains(LinkRelations::PRELOAD) && + !self.previous_type_matched.get() + { + self.handle_preload_url(); + } + }, + local_name!("media") => { + // https://html.spec.whatwg.org/multipage/#link-type-preload + // When the media attribute of the link element of an external resource link that + // is already browsing-context connected, but was previously not obtained due to + // the media attribute not matching the environment, is changed or removed. + if self.relations.get().contains(LinkRelations::PRELOAD) && + !self.previous_media_environment_matched.get() + { + match mutation { + AttributeMutation::Removed | AttributeMutation::Set(Some(_)) => { + self.handle_preload_url() + }, + _ => {}, + }; + } + + let matches_media_environment = + self.upcast::<Element>().matches_environment(&attr.value()); + self.previous_media_environment_matched + .set(matches_media_environment); }, _ => {}, } @@ -307,6 +397,10 @@ impl VirtualMethods for HTMLLinkElement { if relations.contains(LinkRelations::PREFETCH) { self.fetch_and_process_prefetch_link(&href); } + + if relations.contains(LinkRelations::PRELOAD) { + self.handle_preload_url(); + } } } } @@ -325,6 +419,14 @@ impl VirtualMethods for HTMLLinkElement { } impl HTMLLinkElement { + fn compute_destination_for_attribute(&self) -> Destination { + let element = self.upcast::<Element>(); + element + .get_attribute(&ns!(), &local_name!("as")) + .map(|attr| translate_a_preload_destination(&attr.value())) + .unwrap_or(Destination::None) + } + /// <https://html.spec.whatwg.org/multipage/#create-link-options-from-element> fn processing_options(&self) -> LinkProcessingOptions { let element = self.upcast::<Element>(); @@ -333,10 +435,7 @@ impl HTMLLinkElement { let document = self.upcast::<Node>().owner_doc(); // Step 2. Let options be a new link processing options - let destination = element - .get_attribute(&ns!(), &local_name!("as")) - .map(|attr| translate_a_preload_destination(&attr.value())) - .unwrap_or(Destination::None); + let destination = self.compute_destination_for_attribute(); let mut options = LinkProcessingOptions { href: String::new(), @@ -348,6 +447,7 @@ impl HTMLLinkElement { referrer_policy: referrer_policy_for_element(element), policy_container: document.policy_container().to_owned(), source_set: None, // FIXME + origin: document.borrow().origin().immutable().to_owned(), base_url: document.borrow().base_url(), insecure_requests_policy: document.insecure_requests_policy(), has_trustworthy_ancestor_origin: document.has_trustworthy_ancestor_or_current_origin(), @@ -446,6 +546,10 @@ impl HTMLLinkElement { None => "", }; + if !element.matches_environment(mq_str) { + return; + } + let media = MediaList::parse_media_list(mq_str, document.window()); let im_attribute = element.get_attribute(&ns!(), &local_name!("integrity")); @@ -458,8 +562,6 @@ impl HTMLLinkElement { self.request_generation_id .set(self.request_generation_id.get().increment()); - // TODO: #8085 - Don't load external stylesheets if the node's mq - // doesn't match. let loader = StylesheetLoader::for_element(self.upcast()); loader.load( StylesheetContextSource::LinkElement { media: Some(media) }, @@ -494,6 +596,133 @@ impl HTMLLinkElement { Err(e) => debug!("Parsing url {} failed: {}", href, e), } } + + /// <https://html.spec.whatwg.org/multipage/#link-type-preload:fetch-and-process-the-linked-resource-2> + fn handle_preload_url(&self) { + // Step 1. Update the source set for el. + // TODO + // Step 2. Let options be the result of creating link options from el. + let options = self.processing_options(); + // Step 3. Preload options, with the following steps given a response response: + // Step 3.1 If response is a network error, fire an event named error at el. + // Otherwise, fire an event named load at el. + self.preload(options); + } + + /// <https://html.spec.whatwg.org/multipage/#preload> + fn preload(&self, options: LinkProcessingOptions) { + // Step 1. If options's type doesn't match options's destination, then return. + let type_matches_destination: bool = + HTMLLinkElement::type_matches_destination(&options.link_type, options.destination); + self.previous_type_matched.set(type_matches_destination); + if !type_matches_destination { + return; + } + // Step 2. If options's destination is "image" and options's source set is not null, + // then set options's href to the result of selecting an image source from options's source set. + // TODO + // Step 3. Let request be the result of creating a link request given options. + let url = options.base_url.clone(); + let Some(request) = options.create_link_request(self.owner_window().webview_id()) else { + // Step 4. If request is null, then return. + return; + }; + let document = self.upcast::<Node>().owner_doc(); + // Step 5. Let unsafeEndTime be 0. + // TODO + // Step 6. Let entry be a new preload entry whose integrity metadata is options's integrity. + // TODO + // Step 7. Let key be the result of creating a preload key given request. + // TODO + // Step 8. If options's document is "pending", then set request's initiator type to "early hint". + // TODO + // Step 9. Let controller be null. + // Step 10. Let reportTiming given a Document document be to report timing for controller + // given document's relevant global object. + // Step 11. Set controller to the result of fetching request, with processResponseConsumeBody + // set to the following steps given a response response and null, failure, or a byte sequence bodyBytes: + let fetch_context = PreloadContext { + url, + link: Trusted::new(self), + resource_timing: ResourceFetchTiming::new(ResourceTimingType::Resource), + }; + document.fetch_background(request.clone(), fetch_context); + } + + /// <https://html.spec.whatwg.org/multipage/#match-preload-type> + fn type_matches_destination(mime_type: &str, destination: Option<Destination>) -> bool { + // Step 1. If type is an empty string, then return true. + if mime_type.is_empty() { + return true; + } + // Step 2. If destination is "fetch", then return true. + // + // Fetch is handled as an empty string destination in the spec: + // https://fetch.spec.whatwg.org/#concept-potential-destination-translate + let Some(destination) = destination else { + return false; + }; + if destination == Destination::None { + return true; + } + // Step 3. Let mimeTypeRecord be the result of parsing type. + let Ok(mime_type_record) = Mime::from_str(mime_type) else { + // Step 4. If mimeTypeRecord is failure, then return false. + return false; + }; + // Step 5. If mimeTypeRecord is not supported by the user agent, then return false. + // + // We currently don't check if we actually support the mime type. Only if we can classify + // it according to the spec. + let Some(mime_type) = MimeClassifier::get_media_type(&mime_type_record) else { + return false; + }; + // Step 6. If any of the following are true: + if + // destination is "audio" or "video", and mimeTypeRecord is an audio or video MIME type; + ((destination == Destination::Audio || destination == Destination::Video) && + mime_type == MediaType::AudioVideo) + // destination is a script-like destination and mimeTypeRecord is a JavaScript MIME type; + || (destination.is_script_like() && mime_type == MediaType::JavaScript) + // destination is "image" and mimeTypeRecord is an image MIME type; + || (destination == Destination::Image && mime_type == MediaType::Image) + // destination is "font" and mimeTypeRecord is a font MIME type; + || (destination == Destination::Font && mime_type == MediaType::Font) + // destination is "json" and mimeTypeRecord is a JSON MIME type; + || (destination == Destination::Json && mime_type == MediaType::Json) + // destination is "style" and mimeTypeRecord's essence is text/css; or + || (destination == Destination::Style && mime_type_record == mime::TEXT_CSS) + // destination is "track" and mimeTypeRecord's essence is text/vtt, + || (destination == Destination::Track && mime_type_record.essence_str() == "text/vtt") + { + // then return true. + return true; + } + // Step 7. Return false. + false + } + + fn fire_event_after_response(&self, response: Result<ResourceFetchTiming, NetworkError>) { + if response.is_err() { + self.upcast::<EventTarget>() + .fire_event(atom!("error"), CanGc::note()); + } else { + // TODO(35035): Figure out why we need to queue a task for the load event. Otherwise + // the performance timing data hasn't been saved yet, which fails several preload + // WPT tests that assume that performance timing information is available when + // the load event is fired. + let this = Trusted::new(self); + self.owner_global() + .task_manager() + .performance_timeline_task_source() + .queue(task!(preload_load_event: move || { + let this = this.root(); + this + .upcast::<EventTarget>() + .fire_event(atom!("load"), CanGc::note()); + })); + } + } } impl StylesheetOwner for HTMLLinkElement { @@ -552,6 +781,21 @@ impl HTMLLinkElementMethods<crate::DomTypeHolder> for HTMLLinkElement { .set_tokenlist_attribute(&local_name!("rel"), rel, can_gc); } + // https://html.spec.whatwg.org/multipage/#dom-link-as + make_enumerated_getter!( + As, + "as", + "fetch" | "audio" | "audioworklet" | "document" | "embed" | "font" | "frame" + | "iframe" | "image" | "json" | "manifest" | "object" | "paintworklet" + | "report" | "script" | "serviceworker" | "sharedworker" | "style" | "track" + | "video" | "webidentity" | "worker" | "xslt", + missing => "", + invalid => "" + ); + + // https://html.spec.whatwg.org/multipage/#dom-link-as + make_setter!(SetAs, "as"); + // https://html.spec.whatwg.org/multipage/#dom-link-media make_getter!(Media, "media"); @@ -689,6 +933,8 @@ impl LinkProcessingOptions { self.has_trustworthy_ancestor_origin, self.policy_container, ) + .initiator(Initiator::Link) + .origin(self.origin) .integrity_metadata(self.integrity) .cryptographic_nonce_metadata(self.cryptographic_nonce_metadata) .referrer_policy(self.referrer_policy); @@ -795,3 +1041,77 @@ impl PreInvoke for PrefetchContext { true } } + +struct PreloadContext { + /// The `<link>` element that caused this preload operation + link: Trusted<HTMLLinkElement>, + + resource_timing: ResourceFetchTiming, + + /// The url being preloaded + url: ServoUrl, +} + +impl FetchResponseListener for PreloadContext { + fn process_request_body(&mut self, _: RequestId) {} + + fn process_request_eof(&mut self, _: RequestId) {} + + fn process_response( + &mut self, + _: RequestId, + fetch_metadata: Result<FetchMetadata, NetworkError>, + ) { + _ = fetch_metadata; + } + + fn process_response_chunk(&mut self, _: RequestId, chunk: Vec<u8>) { + _ = chunk; + } + + /// Step 3.1 of <https://html.spec.whatwg.org/multipage/#link-type-preload:fetch-and-process-the-linked-resource-2> + fn process_response_eof( + &mut self, + _: RequestId, + response: Result<ResourceFetchTiming, NetworkError>, + ) { + self.link.root().fire_event_after_response(response); + } + + fn resource_timing_mut(&mut self) -> &mut ResourceFetchTiming { + &mut self.resource_timing + } + + fn resource_timing(&self) -> &ResourceFetchTiming { + &self.resource_timing + } + + fn submit_resource_timing(&mut self) { + submit_timing(self, CanGc::note()) + } + + fn process_csp_violations(&mut self, _request_id: RequestId, violations: Vec<csp::Violation>) { + let global = &self.resource_timing_global(); + global.report_csp_violations(violations, None); + } +} + +impl ResourceTimingListener for PreloadContext { + fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) { + ( + InitiatorType::LocalName(self.url.clone().into_string()), + self.url.clone(), + ) + } + + fn resource_timing_global(&self) -> DomRoot<GlobalScope> { + self.link.root().upcast::<Node>().owner_doc().global() + } +} + +impl PreInvoke for PreloadContext { + fn should_invoke(&self) -> bool { + // Preload requests are never aborted. + true + } +} |