Skip to content

Commit 8ac839e

Browse files
committed
Implement basics of link preloading
These changes allow a minimal set of checks for font-src CSP checks to pass. Part of servo#4577 Part of servo#35035 Signed-off-by: Tim van der Lippe <[email protected]>
1 parent c52ce2d commit 8ac839e

36 files changed

+441
-431
lines changed

components/net/lib.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ pub mod http_cache;
1616
pub mod http_loader;
1717
pub mod image_cache;
1818
pub mod local_directory_listing;
19-
pub mod mime_classifier;
2019
pub mod protocols;
2120
pub mod request_interceptor;
2221
pub mod resource_thread;

components/net/tests/main.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ mod filemanager_thread;
1414
mod hsts;
1515
mod http_cache;
1616
mod http_loader;
17-
mod mime_classifier;
1817
mod resource_thread;
1918
mod subresource_integrity;
2019

components/script/dom/htmllinkelement.rs

Lines changed: 226 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@
55
use std::borrow::{Borrow, ToOwned};
66
use std::cell::Cell;
77
use std::default::Default;
8+
use std::str::FromStr;
89

910
use base::id::WebViewId;
1011
use content_security_policy as csp;
1112
use dom_struct::dom_struct;
1213
use embedder_traits::EmbedderMsg;
1314
use html5ever::{LocalName, Prefix, local_name, ns};
1415
use js::rust::HandleObject;
16+
use mime::Mime;
17+
use net_traits::mime_classifier::{MediaType, MimeClassifier};
1518
use net_traits::policy_container::PolicyContainer;
1619
use net_traits::request::{
1720
CorsSettings, Destination, Initiator, InsecureRequestsPolicy, Referrer, RequestBuilder,
@@ -22,7 +25,7 @@ use net_traits::{
2225
ResourceTimingType,
2326
};
2427
use servo_arc::Arc;
25-
use servo_url::ServoUrl;
28+
use servo_url::{ImmutableOrigin, ServoUrl};
2629
use style::attr::AttrValue;
2730
use style::stylesheets::Stylesheet;
2831
use stylo_atoms::Atom;
@@ -78,6 +81,7 @@ struct LinkProcessingOptions {
7881
policy_container: PolicyContainer,
7982
source_set: Option<()>,
8083
base_url: ServoUrl,
84+
origin: ImmutableOrigin,
8185
insecure_requests_policy: InsecureRequestsPolicy,
8286
has_trustworthy_ancestor_origin: bool,
8387
// Some fields that we don't need yet are missing
@@ -257,6 +261,10 @@ impl VirtualMethods for HTMLLinkElement {
257261
if self.relations.get().contains(LinkRelations::PREFETCH) {
258262
self.fetch_and_process_prefetch_link(&attr.value());
259263
}
264+
265+
if self.relations.get().contains(LinkRelations::PRELOAD) {
266+
self.handle_preload_url();
267+
}
260268
},
261269
local_name!("sizes") if self.relations.get().contains(LinkRelations::ICON) => {
262270
if let Some(ref href) = get_attr(self.upcast(), &local_name!("href")) {
@@ -307,6 +315,10 @@ impl VirtualMethods for HTMLLinkElement {
307315
if relations.contains(LinkRelations::PREFETCH) {
308316
self.fetch_and_process_prefetch_link(&href);
309317
}
318+
319+
if relations.contains(LinkRelations::PRELOAD) {
320+
self.handle_preload_url();
321+
}
310322
}
311323
}
312324
}
@@ -348,6 +360,7 @@ impl HTMLLinkElement {
348360
referrer_policy: referrer_policy_for_element(element),
349361
policy_container: document.policy_container().to_owned(),
350362
source_set: None, // FIXME
363+
origin: document.borrow().origin().immutable().to_owned(),
351364
base_url: document.borrow().base_url(),
352365
insecure_requests_policy: document.insecure_requests_policy(),
353366
has_trustworthy_ancestor_origin: document.has_trustworthy_ancestor_or_current_origin(),
@@ -494,6 +507,128 @@ impl HTMLLinkElement {
494507
Err(e) => debug!("Parsing url {} failed: {}", href, e),
495508
}
496509
}
510+
511+
/// <https://html.spec.whatwg.org/multipage/#link-type-preload:fetch-and-process-the-linked-resource-2>
512+
fn handle_preload_url(&self) {
513+
// Step 1. Update the source set for el.
514+
// TODO
515+
// Step 2. Let options be the result of creating link options from el.
516+
let options = self.processing_options();
517+
// Step 3. Preload options, with the following steps given a response response:
518+
// Step 3.1 If response is a network error, fire an event named error at el.
519+
// Otherwise, fire an event named load at el.
520+
self.preload(options);
521+
}
522+
523+
/// <https://html.spec.whatwg.org/multipage/#preload>
524+
fn preload(&self, options: LinkProcessingOptions) {
525+
// Step 1. If options's type doesn't match options's destination, then return.
526+
if !HTMLLinkElement::type_matches_destination(&options.link_type, options.destination) {
527+
return;
528+
}
529+
// Step 2. If options's destination is "image" and options's source set is not null,
530+
// then set options's href to the result of selecting an image source from options's source set.
531+
// TODO
532+
// Step 3. Let request be the result of creating a link request given options.
533+
let url = options.base_url.clone();
534+
let Some(mut request) = options.create_link_request(self.owner_window().webview_id())
535+
else {
536+
// Step 4. If request is null, then return.
537+
return;
538+
};
539+
let document = self.upcast::<Node>().owner_doc();
540+
// Step 5. Let unsafeEndTime be 0.
541+
// TODO
542+
// Step 6. Let entry be a new preload entry whose integrity metadata is options's integrity.
543+
// TODO
544+
// Step 7. Let key be the result of creating a preload key given request.
545+
// TODO
546+
// Step 8. If options's document is "pending", then set request's initiator type to "early hint".
547+
if document.has_pending_parsing_blocking_script() {
548+
request = request.initiator(Initiator::EarlyHint);
549+
}
550+
// Step 9. Let controller be null.
551+
// Step 10. Let reportTiming given a Document document be to report timing for controller
552+
// given document's relevant global object.
553+
// Step 11. Set controller to the result of fetching request, with processResponseConsumeBody
554+
// set to the following steps given a response response and null, failure, or a byte sequence bodyBytes:
555+
let fetch_context = PreloadContext {
556+
url,
557+
link: Trusted::new(self),
558+
resource_timing: ResourceFetchTiming::new(ResourceTimingType::Resource),
559+
};
560+
document.fetch_background(request.clone(), fetch_context);
561+
}
562+
563+
/// <https://html.spec.whatwg.org/multipage/#match-preload-type>
564+
fn type_matches_destination(mime_type: &str, destination: Option<Destination>) -> bool {
565+
// Step 1. If type is an empty string, then return true.
566+
if mime_type.is_empty() {
567+
return true;
568+
}
569+
let Some(destination) = destination else {
570+
return false;
571+
};
572+
// Step 2. If destination is "fetch", then return true.
573+
//
574+
// Fetch is handled as an empty string destination in the spec
575+
if destination == Destination::None {
576+
return true;
577+
}
578+
// Step 3. Let mimeTypeRecord be the result of parsing type.
579+
let Ok(mime_type_record) = Mime::from_str(mime_type) else {
580+
// Step 4. If mimeTypeRecord is failure, then return false.
581+
return false;
582+
};
583+
// Step 5. If mimeTypeRecord is not supported by the user agent, then return false.
584+
let Some(mime_type) = MimeClassifier::get_media_type(&mime_type_record) else {
585+
return false;
586+
};
587+
// Step 6. If any of the following are true:
588+
if
589+
// destination is "audio" or "video", and mimeTypeRecord is an audio or video MIME type;
590+
((destination == Destination::Audio || destination == Destination::Video) &&
591+
mime_type == MediaType::AudioVideo)
592+
// destination is a script-like destination and mimeTypeRecord is a JavaScript MIME type;
593+
|| (destination.is_script_like() && mime_type == MediaType::JavaScript)
594+
// destination is "image" and mimeTypeRecord is an image MIME type;
595+
|| (destination == Destination::Image && mime_type == MediaType::Image)
596+
// destination is "font" and mimeTypeRecord is a font MIME type;
597+
|| (destination == Destination::Font && mime_type == MediaType::Font)
598+
// destination is "json" and mimeTypeRecord is a JSON MIME type;
599+
|| (destination == Destination::Json && mime_type == MediaType::Json)
600+
// destination is "style" and mimeTypeRecord's essence is text/css; or
601+
|| (destination == Destination::Style && mime_type_record == mime::TEXT_CSS)
602+
// destination is "track" and mimeTypeRecord's essence is text/vtt,
603+
// TODO
604+
{
605+
return true;
606+
}
607+
// Step 7. Return false.
608+
false
609+
}
610+
611+
fn fire_event_after_response(&self, response: Result<ResourceFetchTiming, NetworkError>) {
612+
if response.is_err() {
613+
self.upcast::<EventTarget>()
614+
.fire_event(atom!("error"), CanGc::note());
615+
} else {
616+
// TODO(35035): Figure out why we need to queue a task for the load event. Otherwise
617+
// the performance timing data hasn't been saved yet, which fails several preload
618+
// WPT tests that assume that performance timing information is available when
619+
// the load event is fired.
620+
let this = Trusted::new(self);
621+
self.owner_global()
622+
.task_manager()
623+
.performance_timeline_task_source()
624+
.queue(task!(preload_load_event: move || {
625+
let this = this.root();
626+
this
627+
.upcast::<EventTarget>()
628+
.fire_event(atom!("load"), CanGc::note());
629+
}));
630+
}
631+
}
497632
}
498633

499634
impl StylesheetOwner for HTMLLinkElement {
@@ -552,6 +687,21 @@ impl HTMLLinkElementMethods<crate::DomTypeHolder> for HTMLLinkElement {
552687
.set_tokenlist_attribute(&local_name!("rel"), rel, can_gc);
553688
}
554689

690+
// https://html.spec.whatwg.org/multipage/#dom-link-as
691+
make_enumerated_getter!(
692+
As,
693+
"as",
694+
"fetch" | "audio" | "audioworklet" | "document" | "embed" | "font" | "frame"
695+
| "iframe" | "image" | "json" | "manifest" | "object" | "paintworklet"
696+
| "report" | "script" | "serviceworker" | "sharedworker" | "style" | "track"
697+
| "video" | "webidentity" | "worker" | "xslt",
698+
missing => "",
699+
invalid => ""
700+
);
701+
702+
// https://html.spec.whatwg.org/multipage/#dom-link-as
703+
make_setter!(SetAs, "as");
704+
555705
// https://html.spec.whatwg.org/multipage/#dom-link-media
556706
make_getter!(Media, "media");
557707

@@ -689,6 +839,7 @@ impl LinkProcessingOptions {
689839
self.has_trustworthy_ancestor_origin,
690840
self.policy_container,
691841
)
842+
.origin(self.origin)
692843
.integrity_metadata(self.integrity)
693844
.cryptographic_nonce_metadata(self.cryptographic_nonce_metadata)
694845
.referrer_policy(self.referrer_policy);
@@ -795,3 +946,77 @@ impl PreInvoke for PrefetchContext {
795946
true
796947
}
797948
}
949+
950+
struct PreloadContext {
951+
/// The `<link>` element that caused this prefetch operation
952+
link: Trusted<HTMLLinkElement>,
953+
954+
resource_timing: ResourceFetchTiming,
955+
956+
/// The url being prefetched
957+
url: ServoUrl,
958+
}
959+
960+
impl FetchResponseListener for PreloadContext {
961+
fn process_request_body(&mut self, _: RequestId) {}
962+
963+
fn process_request_eof(&mut self, _: RequestId) {}
964+
965+
fn process_response(
966+
&mut self,
967+
_: RequestId,
968+
fetch_metadata: Result<FetchMetadata, NetworkError>,
969+
) {
970+
_ = fetch_metadata;
971+
}
972+
973+
fn process_response_chunk(&mut self, _: RequestId, chunk: Vec<u8>) {
974+
_ = chunk;
975+
}
976+
977+
/// Step 3.1 of <https://html.spec.whatwg.org/multipage/#link-type-preload:fetch-and-process-the-linked-resource-2>
978+
fn process_response_eof(
979+
&mut self,
980+
_: RequestId,
981+
response: Result<ResourceFetchTiming, NetworkError>,
982+
) {
983+
self.link.root().fire_event_after_response(response);
984+
}
985+
986+
fn resource_timing_mut(&mut self) -> &mut ResourceFetchTiming {
987+
&mut self.resource_timing
988+
}
989+
990+
fn resource_timing(&self) -> &ResourceFetchTiming {
991+
&self.resource_timing
992+
}
993+
994+
fn submit_resource_timing(&mut self) {
995+
submit_timing(self, CanGc::note())
996+
}
997+
998+
fn process_csp_violations(&mut self, _request_id: RequestId, violations: Vec<csp::Violation>) {
999+
let global = &self.resource_timing_global();
1000+
global.report_csp_violations(violations, None);
1001+
}
1002+
}
1003+
1004+
impl ResourceTimingListener for PreloadContext {
1005+
fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) {
1006+
(
1007+
InitiatorType::LocalName(self.url.clone().into_string()),
1008+
self.url.clone(),
1009+
)
1010+
}
1011+
1012+
fn resource_timing_global(&self) -> DomRoot<GlobalScope> {
1013+
self.link.root().upcast::<Node>().owner_doc().global()
1014+
}
1015+
}
1016+
1017+
impl PreInvoke for PreloadContext {
1018+
fn should_invoke(&self) -> bool {
1019+
// Preload requests are never aborted.
1020+
true
1021+
}
1022+
}

components/script_bindings/webidls/HTMLLinkElement.webidl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ interface HTMLLinkElement : HTMLElement {
1313
attribute DOMString? crossOrigin;
1414
[CEReactions]
1515
attribute DOMString rel;
16-
// [CEReactions] attribute DOMString as;
16+
[CEReactions] attribute DOMString as;
1717
[SameObject, PutForwards=value] readonly attribute DOMTokenList relList;
1818
[CEReactions]
1919
attribute DOMString media;

components/shared/net/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ pub mod blob_url_store;
4040
pub mod filemanager_thread;
4141
pub mod http_status;
4242
pub mod image_cache;
43+
pub mod mime_classifier;
4344
pub mod policy_container;
4445
pub mod pub_domains;
4546
pub mod quality;

0 commit comments

Comments
 (0)