script/
stylesheet_loader.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::io::{Read, Seek, Write};
6
7use base::id::PipelineId;
8use crossbeam_channel::Sender;
9use cssparser::SourceLocation;
10use encoding_rs::UTF_8;
11use net_traits::mime_classifier::MimeClassifier;
12use net_traits::request::{CorsSettings, Destination, RequestId};
13use net_traits::{
14    FetchMetadata, FilteredMetadata, Metadata, NetworkError, ReferrerPolicy, ResourceFetchTiming,
15};
16use servo_arc::Arc;
17use servo_config::pref;
18use servo_url::ServoUrl;
19use style::context::QuirksMode;
20use style::global_style_data::STYLE_THREAD_POOL;
21use style::media_queries::MediaList;
22use style::shared_lock::{Locked, SharedRwLock};
23use style::stylesheets::import_rule::{ImportLayer, ImportSheet, ImportSupportsCondition};
24use style::stylesheets::{
25    ImportRule, Origin, Stylesheet, StylesheetLoader as StyleStylesheetLoader, UrlExtraData,
26};
27use style::values::CssUrl;
28
29use crate::document_loader::LoadType;
30use crate::dom::bindings::inheritance::Castable;
31use crate::dom::bindings::refcounted::Trusted;
32use crate::dom::bindings::reflector::DomGlobal;
33use crate::dom::bindings::root::DomRoot;
34use crate::dom::csp::{GlobalCspReporting, Violation};
35use crate::dom::document::Document;
36use crate::dom::element::Element;
37use crate::dom::eventtarget::EventTarget;
38use crate::dom::globalscope::GlobalScope;
39use crate::dom::html::htmlelement::HTMLElement;
40use crate::dom::html::htmllinkelement::{HTMLLinkElement, RequestGenerationId};
41use crate::dom::node::NodeTraits;
42use crate::dom::performance::performanceresourcetiming::InitiatorType;
43use crate::dom::shadowroot::ShadowRoot;
44use crate::dom::window::CSSErrorReporter;
45use crate::fetch::create_a_potential_cors_request;
46use crate::messaging::{CommonScriptMsg, MainThreadScriptMsg};
47use crate::network_listener::{self, FetchResponseListener, ResourceTimingListener};
48use crate::script_runtime::{CanGc, ScriptThreadEventCategory};
49use crate::task_source::TaskSourceName;
50use crate::unminify::{
51    BeautifyFileType, create_output_file, create_temp_files, execute_js_beautify,
52};
53
54pub(crate) trait StylesheetOwner {
55    /// Returns whether this element was inserted by the parser (i.e., it should
56    /// trigger a document-load-blocking load).
57    fn parser_inserted(&self) -> bool;
58
59    /// Which referrer policy should loads triggered by this owner follow
60    fn referrer_policy(&self) -> ReferrerPolicy;
61
62    /// Notes that a new load is pending to finish.
63    fn increment_pending_loads_count(&self);
64
65    /// Returns None if there are still pending loads, or whether any load has
66    /// failed since the loads started.
67    fn load_finished(&self, successful: bool) -> Option<bool>;
68
69    /// Sets origin_clean flag.
70    fn set_origin_clean(&self, origin_clean: bool);
71}
72
73pub(crate) enum StylesheetContextSource {
74    LinkElement,
75    Import(Arc<Locked<ImportRule>>),
76}
77
78/// The context required for asynchronously loading an external stylesheet.
79struct StylesheetContext {
80    /// The element that initiated the request.
81    element: Trusted<HTMLElement>,
82    source: StylesheetContextSource,
83    media: Arc<Locked<MediaList>>,
84    url: ServoUrl,
85    metadata: Option<Metadata>,
86    /// The response body received to date.
87    data: Vec<u8>,
88    /// The node document for elem when the load was initiated.
89    document: Trusted<Document>,
90    shadow_root: Option<Trusted<ShadowRoot>>,
91    origin_clean: bool,
92    /// A token which must match the generation id of the `HTMLLinkElement` for it to load the stylesheet.
93    /// This is ignored for `HTMLStyleElement` and imports.
94    request_generation_id: Option<RequestGenerationId>,
95}
96
97impl StylesheetContext {
98    fn unminify_css(&mut self, file_url: ServoUrl) {
99        let Some(unminified_dir) = self.document.root().window().unminified_css_dir() else {
100            return;
101        };
102
103        let mut style_content = std::mem::take(&mut self.data);
104        if let Some((input, mut output)) = create_temp_files() {
105            if execute_js_beautify(
106                input.path(),
107                output.try_clone().unwrap(),
108                BeautifyFileType::Css,
109            ) {
110                output.seek(std::io::SeekFrom::Start(0)).unwrap();
111                output.read_to_end(&mut style_content).unwrap();
112            }
113        }
114        match create_output_file(unminified_dir, &file_url, None) {
115            Ok(mut file) => {
116                file.write_all(&style_content).unwrap();
117            },
118            Err(why) => {
119                log::warn!("Could not store script {:?}", why);
120            },
121        }
122
123        self.data = style_content;
124    }
125
126    fn parse(
127        &self,
128        quirks_mode: QuirksMode,
129        shared_lock: SharedRwLock,
130        css_error_reporter: &CSSErrorReporter,
131        loader: ElementStylesheetLoader<'_>,
132    ) -> Arc<Stylesheet> {
133        let metadata = self
134            .metadata
135            .as_ref()
136            .expect("Should never call parse without metadata.");
137
138        let _span = profile_traits::trace_span!("ParseStylesheet").entered();
139        Arc::new(Stylesheet::from_bytes(
140            &self.data,
141            UrlExtraData(metadata.final_url.get_arc()),
142            metadata.charset.as_deref(),
143            // TODO: Get the actual value. http://dev.w3.org/csswg/css-syntax/#environment-encoding
144            Some(UTF_8),
145            Origin::Author,
146            self.media.clone(),
147            shared_lock,
148            Some(&loader),
149            Some(css_error_reporter),
150            quirks_mode,
151        ))
152    }
153
154    fn contributes_to_the_styling_processing_model(&self, element: &HTMLElement) -> bool {
155        if !element.upcast::<Element>().is_connected() {
156            return false;
157        }
158
159        // Whether or not this `StylesheetContext` is for a `<link>` element that comes
160        // from a previous generation. This prevents processing of earlier stylsheet URLs
161        // when the URL has changed.
162        //
163        // TODO(mrobinson): Shouldn't we also exit early if this is an import that was originally
164        // imported from a `<link>` element that has advanced a generation as well?
165        if !matches!(&self.source, StylesheetContextSource::LinkElement) {
166            return true;
167        }
168        let link = element.downcast::<HTMLLinkElement>().unwrap();
169        self.request_generation_id
170            .is_none_or(|generation| generation == link.get_request_generation_id())
171    }
172
173    fn decrement_load_and_render_blockers(&self, owner: &dyn StylesheetOwner, document: &Document) {
174        if !owner.parser_inserted() {
175            return;
176        }
177
178        document.decrement_script_blocking_stylesheet_count();
179
180        // From <https://html.spec.whatwg.org/multipage/#link-type-stylesheet>:
181        // > A link element of this type is implicitly potentially render-blocking if the element
182        // > was created by its node document's parser.
183        if matches!(self.source, StylesheetContextSource::LinkElement) {
184            document.decrement_render_blocking_element_count();
185        }
186    }
187
188    fn finish_load(
189        self,
190        successful: bool,
191        owner: &dyn StylesheetOwner,
192        element: &HTMLElement,
193        document: &Document,
194    ) {
195        self.decrement_load_and_render_blockers(owner, document);
196        document.finish_load(LoadType::Stylesheet(self.url), CanGc::note());
197
198        let Some(any_failed) = owner.load_finished(successful) else {
199            return;
200        };
201
202        // Do not fire any events on disconnected nodes.
203        if !element.upcast::<Element>().is_connected() {
204            return;
205        }
206
207        // We need to fire an event even if this load is for an ignored stylsheet (such as
208        // one from a previous generation). Events are delayed until all loads are complete,
209        // so we may need to fire the load event for the real load that happened earlier.
210        //
211        // TODO(mrobinson): This is a pretty confusing way of doing things and could potentially
212        // delay the "load" event. Loads from previous generations should likely not count for
213        // delaying the event.
214        let event = match any_failed {
215            true => atom!("error"),
216            false => atom!("load"),
217        };
218        element
219            .upcast::<EventTarget>()
220            .fire_event(event, CanGc::note());
221    }
222
223    fn do_post_parse_tasks(self, successful: bool, stylesheet: Option<Arc<Stylesheet>>) {
224        let element = self.element.root();
225        let document = self.document.root();
226        let owner = element
227            .upcast::<Element>()
228            .as_stylesheet_owner()
229            .expect("Stylesheet not loaded by <style> or <link> element!");
230
231        // From <https://html.spec.whatwg.org/multipage/#link-type-stylesheet>:
232        // > If `el` no longer creates an external resource link that contributes to the
233        // > styling processing model, or if, since the resource in question was fetched, it
234        // > has become appropriate to fetch it again, then:
235        // >   1. Remove el from el's node document's script-blocking style sheet set.
236        // >   2. Return.
237        if !self.contributes_to_the_styling_processing_model(&element) {
238            // Always consider ignored loads as successful, as they shouldn't cause any subsequent
239            // successful loads to fire an "error" event.
240            self.finish_load(true, owner, &element, &document);
241            return;
242        }
243
244        if let Some(stylesheet) = stylesheet {
245            match &self.source {
246                StylesheetContextSource::LinkElement => {
247                    let link = element.downcast::<HTMLLinkElement>().unwrap();
248                    if link.is_effectively_disabled() {
249                        stylesheet.set_disabled(true);
250                    }
251                    link.set_stylesheet(stylesheet);
252                },
253                StylesheetContextSource::Import(import_rule) => {
254                    // Construct a new WebFontDocumentContext for the stylesheet
255                    let window = element.owner_window();
256                    let document_context = window.web_font_context();
257
258                    // Layout knows about this stylesheet, because Stylo added it to the Stylist,
259                    // but Layout doesn't know about any new web fonts that it contains.
260                    document.load_web_fonts_from_stylesheet(&stylesheet, &document_context);
261
262                    let mut guard = document.style_shared_lock().write();
263                    import_rule.write_with(&mut guard).stylesheet = ImportSheet::Sheet(stylesheet);
264                },
265            }
266        }
267
268        if let Some(ref shadow_root) = self.shadow_root {
269            shadow_root.root().invalidate_stylesheets();
270        } else {
271            document.invalidate_stylesheets();
272        }
273        owner.set_origin_clean(self.origin_clean);
274
275        self.finish_load(successful, owner, &element, &document);
276    }
277}
278
279impl FetchResponseListener for StylesheetContext {
280    fn process_request_body(&mut self, _: RequestId) {}
281
282    fn process_request_eof(&mut self, _: RequestId) {}
283
284    fn process_response(&mut self, _: RequestId, metadata: Result<FetchMetadata, NetworkError>) {
285        if let Ok(FetchMetadata::Filtered {
286            filtered: FilteredMetadata::Opaque | FilteredMetadata::OpaqueRedirect(_),
287            ..
288        }) = metadata
289        {
290            self.origin_clean = false;
291        }
292
293        self.metadata = metadata.ok().map(|m| match m {
294            FetchMetadata::Unfiltered(m) => m,
295            FetchMetadata::Filtered { unsafe_, .. } => unsafe_,
296        });
297    }
298
299    fn process_response_chunk(&mut self, _: RequestId, mut payload: Vec<u8>) {
300        self.data.append(&mut payload);
301    }
302
303    fn process_response_eof(
304        mut self,
305        _: RequestId,
306        status: Result<ResourceFetchTiming, NetworkError>,
307    ) {
308        // FIXME: Revisit once consensus is reached at:
309        // https://github.com/whatwg/html/issues/1142
310        let successful = self
311            .metadata
312            .as_ref()
313            .map(|metadata| metadata.status == http::StatusCode::OK)
314            .unwrap_or(false);
315
316        let Ok(response) = status else {
317            self.do_post_parse_tasks(successful, None);
318            return;
319        };
320
321        network_listener::submit_timing(&self, &response, CanGc::note());
322
323        let Some(metadata) = self.metadata.as_ref() else {
324            self.do_post_parse_tasks(successful, None);
325            return;
326        };
327
328        let element = self.element.root();
329        let document = self.document.root();
330        let is_css =
331            metadata.content_type.clone().is_some_and(|content_type| {
332                MimeClassifier::is_css(&content_type.into_inner().into())
333            }) || (
334                // From <https://html.spec.whatwg.org/multipage/#link-type-stylesheet>:
335                // > Quirk: If the document has been set to quirks mode, has the same origin as
336                // > the URL of the external resource, and the Content-Type metadata of the
337                // > external resource is not a supported style sheet type, the user agent must
338                // > instead assume it to be text/css.
339                document.quirks_mode() == QuirksMode::Quirks &&
340                    document.origin().immutable().clone() == metadata.final_url.origin()
341            );
342
343        self.unminify_css(metadata.final_url.clone());
344        if !is_css {
345            self.data.clear();
346        }
347
348        // From <https://html.spec.whatwg.org/multipage/#link-type-stylesheet>:
349        // > If `el` no longer creates an external resource link that contributes to the
350        // > styling processing model, or if, since the resource in question was fetched, it
351        // > has become appropriate to fetch it again, then:
352        // >   1. Remove el from el's node document's script-blocking style sheet set.
353        // >   2. Return.
354        if !self.contributes_to_the_styling_processing_model(&element) {
355            self.do_post_parse_tasks(successful, None);
356            return;
357        }
358
359        let loader = if pref!(dom_parallel_css_parsing_enabled) {
360            ElementStylesheetLoader::Asynchronous(AsynchronousStylesheetLoader::new(&element))
361        } else {
362            ElementStylesheetLoader::Synchronous { element: &element }
363        };
364        loader.parse(successful, self, &element, &document);
365    }
366
367    fn process_csp_violations(&mut self, _request_id: RequestId, violations: Vec<Violation>) {
368        let global = &self.resource_timing_global();
369        global.report_csp_violations(violations, None, None);
370    }
371}
372
373impl ResourceTimingListener for StylesheetContext {
374    fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) {
375        let initiator_type = InitiatorType::LocalName(
376            self.element
377                .root()
378                .upcast::<Element>()
379                .local_name()
380                .to_string(),
381        );
382        (initiator_type, self.url.clone())
383    }
384
385    fn resource_timing_global(&self) -> DomRoot<GlobalScope> {
386        self.element.root().owner_document().global()
387    }
388}
389
390pub(crate) enum ElementStylesheetLoader<'a> {
391    Synchronous { element: &'a HTMLElement },
392    Asynchronous(AsynchronousStylesheetLoader),
393}
394
395impl<'a> ElementStylesheetLoader<'a> {
396    pub(crate) fn new(element: &'a HTMLElement) -> Self {
397        ElementStylesheetLoader::Synchronous { element }
398    }
399}
400
401impl ElementStylesheetLoader<'_> {
402    pub(crate) fn load(
403        &self,
404        source: StylesheetContextSource,
405        media: Arc<Locked<MediaList>>,
406        url: ServoUrl,
407        cors_setting: Option<CorsSettings>,
408        integrity_metadata: String,
409    ) {
410        match self {
411            ElementStylesheetLoader::Synchronous { element } => Self::load_with_element(
412                element,
413                source,
414                media,
415                url,
416                cors_setting,
417                integrity_metadata,
418            ),
419            ElementStylesheetLoader::Asynchronous { .. } => unreachable!(
420                "Should never call load directly on an asynchronous ElementStylesheetLoader"
421            ),
422        }
423    }
424
425    fn load_with_element(
426        element: &HTMLElement,
427        source: StylesheetContextSource,
428        media: Arc<Locked<MediaList>>,
429        url: ServoUrl,
430        cors_setting: Option<CorsSettings>,
431        integrity_metadata: String,
432    ) {
433        let document = element.owner_document();
434        let shadow_root = element
435            .containing_shadow_root()
436            .map(|sr| Trusted::new(&*sr));
437        let generation = element
438            .downcast::<HTMLLinkElement>()
439            .map(HTMLLinkElement::get_request_generation_id);
440        let context = StylesheetContext {
441            element: Trusted::new(element),
442            source,
443            media,
444            url: url.clone(),
445            metadata: None,
446            data: vec![],
447            document: Trusted::new(&*document),
448            shadow_root,
449            origin_clean: true,
450            request_generation_id: generation,
451        };
452
453        let owner = element
454            .upcast::<Element>()
455            .as_stylesheet_owner()
456            .expect("Stylesheet not loaded by <style> or <link> element!");
457        let referrer_policy = owner.referrer_policy();
458        owner.increment_pending_loads_count();
459
460        if owner.parser_inserted() {
461            document.increment_script_blocking_stylesheet_count();
462
463            // From <https://html.spec.whatwg.org/multipage/#link-type-stylesheet>:
464            // > A link element of this type is implicitly potentially render-blocking if the element
465            // > was created by its node document's parser.
466            if matches!(context.source, StylesheetContextSource::LinkElement) {
467                document.increment_render_blocking_element_count();
468            }
469        }
470
471        // https://html.spec.whatwg.org/multipage/#default-fetch-and-process-the-linked-resource
472        let global = element.global();
473        let request = create_a_potential_cors_request(
474            Some(document.webview_id()),
475            url.clone(),
476            Destination::Style,
477            cors_setting,
478            None,
479            global.get_referrer(),
480            document.insecure_requests_policy(),
481            document.has_trustworthy_ancestor_or_current_origin(),
482            global.policy_container(),
483        )
484        .origin(document.origin().immutable().clone())
485        .pipeline_id(Some(element.global().pipeline_id()))
486        .referrer_policy(referrer_policy)
487        .client(global.request_client())
488        .integrity_metadata(integrity_metadata);
489
490        document.fetch(LoadType::Stylesheet(url), request, context);
491    }
492
493    fn parse(
494        self,
495        successful: bool,
496        listener: StylesheetContext,
497        element: &HTMLElement,
498        document: &Document,
499    ) {
500        let shared_lock = document.style_shared_lock().clone();
501        let quirks_mode = document.quirks_mode();
502        let window = element.owner_window();
503
504        match self {
505            ElementStylesheetLoader::Synchronous { .. } => {
506                let stylesheet =
507                    listener.parse(quirks_mode, shared_lock, window.css_error_reporter(), self);
508                listener.do_post_parse_tasks(successful, Some(stylesheet));
509            },
510            ElementStylesheetLoader::Asynchronous(asynchronous_loader) => {
511                let css_error_reporter = window.css_error_reporter().clone();
512                let thread_pool = STYLE_THREAD_POOL.pool();
513                let thread_pool = thread_pool.as_ref().unwrap();
514
515                thread_pool.spawn(move || {
516                    let pipeline_id = asynchronous_loader.pipeline_id;
517                    let main_thread_sender = asynchronous_loader.main_thread_sender.clone();
518
519                    let loader = ElementStylesheetLoader::Asynchronous(asynchronous_loader);
520                    let stylesheet =
521                        listener.parse(quirks_mode, shared_lock, &css_error_reporter, loader);
522
523                    let task = task!(finish_parsing_of_stylesheet_on_main_thread: move || {
524                        listener.do_post_parse_tasks(successful, Some(stylesheet));
525                    });
526
527                    let _ = main_thread_sender.send(MainThreadScriptMsg::Common(
528                        CommonScriptMsg::Task(
529                            ScriptThreadEventCategory::StylesheetLoad,
530                            Box::new(task),
531                            Some(pipeline_id),
532                            TaskSourceName::Networking,
533                        ),
534                    ));
535                });
536            },
537        };
538    }
539}
540
541impl StyleStylesheetLoader for ElementStylesheetLoader<'_> {
542    /// Request a stylesheet after parsing a given `@import` rule, and return
543    /// the constructed `@import` rule.
544    fn request_stylesheet(
545        &self,
546        url: CssUrl,
547        source_location: SourceLocation,
548        lock: &SharedRwLock,
549        media: Arc<Locked<MediaList>>,
550        supports: Option<ImportSupportsCondition>,
551        layer: ImportLayer,
552    ) -> Arc<Locked<ImportRule>> {
553        // Ensure the supports conditions for this @import are true, if not, refuse to load
554        if supports.as_ref().is_some_and(|s| !s.enabled) {
555            return Arc::new(lock.wrap(ImportRule {
556                url,
557                stylesheet: ImportSheet::new_refused(),
558                supports,
559                layer,
560                source_location,
561            }));
562        }
563
564        let resolved_url = match url.url().cloned() {
565            Some(url) => url,
566            None => {
567                return Arc::new(lock.wrap(ImportRule {
568                    url,
569                    stylesheet: ImportSheet::new_refused(),
570                    supports,
571                    layer,
572                    source_location,
573                }));
574            },
575        };
576
577        let import_rule = Arc::new(lock.wrap(ImportRule {
578            url,
579            stylesheet: ImportSheet::new_pending(),
580            supports,
581            layer,
582            source_location,
583        }));
584
585        // TODO (mrnayak) : Whether we should use the original loader's CORS
586        // setting? Fix this when spec has more details.
587        let source = StylesheetContextSource::Import(import_rule.clone());
588
589        match self {
590            ElementStylesheetLoader::Synchronous { element } => {
591                Self::load_with_element(
592                    element,
593                    source,
594                    media,
595                    resolved_url.into(),
596                    None,
597                    "".to_owned(),
598                );
599            },
600            ElementStylesheetLoader::Asynchronous(AsynchronousStylesheetLoader {
601                element,
602                main_thread_sender,
603                pipeline_id,
604            }) => {
605                let element = element.clone();
606                let task = task!(load_import_stylesheet_on_main_thread: move || {
607                    Self::load_with_element(
608                        &element.root(),
609                        source,
610                        media,
611                        resolved_url.into(),
612                        None,
613                        "".to_owned()
614                    );
615                });
616                let _ =
617                    main_thread_sender.send(MainThreadScriptMsg::Common(CommonScriptMsg::Task(
618                        ScriptThreadEventCategory::StylesheetLoad,
619                        Box::new(task),
620                        Some(*pipeline_id),
621                        TaskSourceName::Networking,
622                    )));
623            },
624        }
625
626        import_rule
627    }
628}
629
630pub(crate) struct AsynchronousStylesheetLoader {
631    element: Trusted<HTMLElement>,
632    main_thread_sender: Sender<MainThreadScriptMsg>,
633    pipeline_id: PipelineId,
634}
635
636impl AsynchronousStylesheetLoader {
637    pub(crate) fn new(element: &HTMLElement) -> Self {
638        let window = element.owner_window();
639        Self {
640            element: Trusted::new(element),
641            main_thread_sender: window.main_thread_script_chan().clone(),
642            pipeline_id: window.pipeline_id(),
643        }
644    }
645}