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 cssparser::SourceLocation;
8use encoding_rs::UTF_8;
9use mime::{self, Mime};
10use net_traits::request::{CorsSettings, Destination, RequestId};
11use net_traits::{
12    FetchMetadata, FetchResponseListener, FilteredMetadata, Metadata, NetworkError, ReferrerPolicy,
13    ResourceFetchTiming, ResourceTimingType,
14};
15use servo_arc::Arc;
16use servo_url::ServoUrl;
17use style::context::QuirksMode;
18use style::media_queries::MediaList;
19use style::shared_lock::{Locked, SharedRwLock};
20use style::stylesheets::import_rule::{ImportLayer, ImportSheet, ImportSupportsCondition};
21use style::stylesheets::{
22    ImportRule, Origin, Stylesheet, StylesheetLoader as StyleStylesheetLoader, UrlExtraData,
23};
24use style::values::CssUrl;
25
26use crate::document_loader::LoadType;
27use crate::dom::bindings::inheritance::Castable;
28use crate::dom::bindings::refcounted::Trusted;
29use crate::dom::bindings::reflector::DomGlobal;
30use crate::dom::bindings::root::DomRoot;
31use crate::dom::csp::{GlobalCspReporting, Violation};
32use crate::dom::document::Document;
33use crate::dom::element::Element;
34use crate::dom::eventtarget::EventTarget;
35use crate::dom::globalscope::GlobalScope;
36use crate::dom::html::htmlelement::HTMLElement;
37use crate::dom::html::htmllinkelement::{HTMLLinkElement, RequestGenerationId};
38use crate::dom::node::NodeTraits;
39use crate::dom::performanceresourcetiming::InitiatorType;
40use crate::dom::shadowroot::ShadowRoot;
41use crate::fetch::create_a_potential_cors_request;
42use crate::network_listener::{self, PreInvoke, ResourceTimingListener};
43use crate::script_runtime::CanGc;
44use crate::unminify::{
45    BeautifyFileType, create_output_file, create_temp_files, execute_js_beautify,
46};
47
48pub(crate) trait StylesheetOwner {
49    /// Returns whether this element was inserted by the parser (i.e., it should
50    /// trigger a document-load-blocking load).
51    fn parser_inserted(&self) -> bool;
52
53    /// Which referrer policy should loads triggered by this owner follow
54    fn referrer_policy(&self) -> ReferrerPolicy;
55
56    /// Notes that a new load is pending to finish.
57    fn increment_pending_loads_count(&self);
58
59    /// Returns None if there are still pending loads, or whether any load has
60    /// failed since the loads started.
61    fn load_finished(&self, successful: bool) -> Option<bool>;
62
63    /// Sets origin_clean flag.
64    fn set_origin_clean(&self, origin_clean: bool);
65}
66
67pub(crate) enum StylesheetContextSource {
68    LinkElement {
69        media: Arc<Locked<MediaList>>,
70    },
71    Import {
72        import_rule: Arc<Locked<ImportRule>>,
73        media: Arc<Locked<MediaList>>,
74    },
75}
76
77/// The context required for asynchronously loading an external stylesheet.
78pub(crate) struct StylesheetContext {
79    /// The element that initiated the request.
80    elem: Trusted<HTMLElement>,
81    source: StylesheetContextSource,
82    url: ServoUrl,
83    metadata: Option<Metadata>,
84    /// The response body received to date.
85    data: Vec<u8>,
86    /// The node document for elem when the load was initiated.
87    document: Trusted<Document>,
88    shadow_root: Option<Trusted<ShadowRoot>>,
89    origin_clean: bool,
90    /// A token which must match the generation id of the `HTMLLinkElement` for it to load the stylesheet.
91    /// This is ignored for `HTMLStyleElement` and imports.
92    request_generation_id: Option<RequestGenerationId>,
93    resource_timing: ResourceFetchTiming,
94}
95
96impl StylesheetContext {
97    fn unminify_css(&self, data: Vec<u8>, file_url: ServoUrl) -> Vec<u8> {
98        let Some(unminified_dir) = self.document.root().window().unminified_css_dir() else {
99            return data;
100        };
101
102        let mut style_content = data;
103
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        style_content
124    }
125}
126
127impl PreInvoke for StylesheetContext {}
128
129impl FetchResponseListener for StylesheetContext {
130    fn process_request_body(&mut self, _: RequestId) {}
131
132    fn process_request_eof(&mut self, _: RequestId) {}
133
134    fn process_response(&mut self, _: RequestId, metadata: Result<FetchMetadata, NetworkError>) {
135        if let Ok(FetchMetadata::Filtered {
136            filtered: FilteredMetadata::Opaque | FilteredMetadata::OpaqueRedirect(_),
137            ..
138        }) = metadata
139        {
140            self.origin_clean = false;
141        }
142
143        self.metadata = metadata.ok().map(|m| match m {
144            FetchMetadata::Unfiltered(m) => m,
145            FetchMetadata::Filtered { unsafe_, .. } => unsafe_,
146        });
147    }
148
149    fn process_response_chunk(&mut self, _: RequestId, mut payload: Vec<u8>) {
150        self.data.append(&mut payload);
151    }
152
153    fn process_response_eof(
154        &mut self,
155        _: RequestId,
156        status: Result<ResourceFetchTiming, NetworkError>,
157    ) {
158        let element = self.elem.root();
159        let document = self.document.root();
160        let mut successful = false;
161
162        if status.is_ok() {
163            let metadata = match self.metadata.take() {
164                Some(meta) => meta,
165                None => return,
166            };
167
168            let mut is_css = metadata.content_type.is_some_and(|ct| {
169                let mime: Mime = ct.into_inner().into();
170                mime.type_() == mime::TEXT && mime.subtype() == mime::CSS
171            }) || (
172                // Quirk: If the document has been set to quirks mode,
173                // has the same origin as the URL of the external resource,
174                // and the Content-Type metadata of the external resource
175                // is not a supported style sheet type, the user agent must
176                // instead assume it to be text/css.
177                // <https://html.spec.whatwg.org/multipage/#link-type-stylesheet>
178                document.quirks_mode() == QuirksMode::Quirks &&
179                    document.origin().immutable().clone() == metadata.final_url.origin()
180            );
181
182            // From <https://html.spec.whatwg.org/multipage/#link-type-stylesheet>:
183            // > Quirk: If the document has been set to quirks mode, has the same origin as
184            // > the URL of the external resource, and the Content-Type metadata of the
185            // > external resource is not a supported style sheet type, the user agent must
186            // > instead assume it to be text/css.
187            if document.quirks_mode() == QuirksMode::Quirks &&
188                document.url().origin() == self.url.origin()
189            {
190                is_css = true;
191            }
192
193            let data = if is_css {
194                let data = std::mem::take(&mut self.data);
195                self.unminify_css(data, metadata.final_url.clone())
196            } else {
197                vec![]
198            };
199
200            // TODO: Get the actual value. http://dev.w3.org/csswg/css-syntax/#environment-encoding
201            let environment_encoding = UTF_8;
202            let protocol_encoding_label = metadata.charset.as_deref();
203            let final_url = metadata.final_url;
204
205            let win = element.owner_window();
206
207            let loader = ElementStylesheetLoader::new(&element);
208            let shared_lock = document.style_shared_lock();
209            let stylesheet = |media| {
210                #[cfg(feature = "tracing")]
211                let _span =
212                    tracing::trace_span!("ParseStylesheet", servo_profiling = true).entered();
213                Arc::new(Stylesheet::from_bytes(
214                    &data,
215                    UrlExtraData(final_url.get_arc()),
216                    protocol_encoding_label,
217                    Some(environment_encoding),
218                    Origin::Author,
219                    media,
220                    shared_lock.clone(),
221                    Some(&loader),
222                    win.css_error_reporter(),
223                    document.quirks_mode(),
224                ))
225            };
226            match &self.source {
227                StylesheetContextSource::LinkElement { media } => {
228                    let link = element.downcast::<HTMLLinkElement>().unwrap();
229                    // We must first check whether the generations of the context and the element match up,
230                    // else we risk applying the wrong stylesheet when responses come out-of-order.
231                    let is_stylesheet_load_applicable = self
232                        .request_generation_id
233                        .is_none_or(|generation| generation == link.get_request_generation_id());
234                    if is_stylesheet_load_applicable {
235                        let stylesheet = stylesheet(media.clone());
236                        if link.is_effectively_disabled() {
237                            stylesheet.set_disabled(true);
238                        }
239                        link.set_stylesheet(stylesheet);
240                    }
241                },
242                StylesheetContextSource::Import { import_rule, media } => {
243                    let stylesheet = stylesheet(media.clone());
244
245                    // Layout knows about this stylesheet, because Stylo added it to the Stylist,
246                    // but Layout doesn't know about any new web fonts that it contains.
247                    document.load_web_fonts_from_stylesheet(&stylesheet);
248
249                    let mut guard = shared_lock.write();
250                    import_rule.write_with(&mut guard).stylesheet = ImportSheet::Sheet(stylesheet);
251                },
252            }
253
254            if let Some(ref shadow_root) = self.shadow_root {
255                shadow_root.root().invalidate_stylesheets();
256            } else {
257                document.invalidate_stylesheets();
258            }
259
260            // FIXME: Revisit once consensus is reached at:
261            // https://github.com/whatwg/html/issues/1142
262            successful = metadata.status == http::StatusCode::OK;
263        }
264
265        let owner = element
266            .upcast::<Element>()
267            .as_stylesheet_owner()
268            .expect("Stylesheet not loaded by <style> or <link> element!");
269        owner.set_origin_clean(self.origin_clean);
270        if owner.parser_inserted() {
271            document.decrement_script_blocking_stylesheet_count();
272        }
273
274        // From <https://html.spec.whatwg.org/multipage/#link-type-stylesheet>:
275        // > A link element of this type is implicitly potentially render-blocking if the element
276        // > was created by its node document's parser.
277        if matches!(self.source, StylesheetContextSource::LinkElement { .. }) &&
278            owner.parser_inserted()
279        {
280            document.decrement_render_blocking_element_count();
281        }
282
283        document.finish_load(LoadType::Stylesheet(self.url.clone()), CanGc::note());
284
285        if let Some(any_failed) = owner.load_finished(successful) {
286            let event = if any_failed {
287                atom!("error")
288            } else {
289                atom!("load")
290            };
291            element
292                .upcast::<EventTarget>()
293                .fire_event(event, CanGc::note());
294        }
295    }
296
297    fn resource_timing_mut(&mut self) -> &mut ResourceFetchTiming {
298        &mut self.resource_timing
299    }
300
301    fn resource_timing(&self) -> &ResourceFetchTiming {
302        &self.resource_timing
303    }
304
305    fn submit_resource_timing(&mut self) {
306        network_listener::submit_timing(self, CanGc::note())
307    }
308
309    fn process_csp_violations(&mut self, _request_id: RequestId, violations: Vec<Violation>) {
310        let global = &self.resource_timing_global();
311        global.report_csp_violations(violations, None, None);
312    }
313}
314
315impl ResourceTimingListener for StylesheetContext {
316    fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) {
317        let initiator_type = InitiatorType::LocalName(
318            self.elem
319                .root()
320                .upcast::<Element>()
321                .local_name()
322                .to_string(),
323        );
324        (initiator_type, self.url.clone())
325    }
326
327    fn resource_timing_global(&self) -> DomRoot<GlobalScope> {
328        self.elem.root().owner_document().global()
329    }
330}
331
332pub(crate) struct ElementStylesheetLoader<'a> {
333    element: &'a HTMLElement,
334}
335
336impl<'a> ElementStylesheetLoader<'a> {
337    pub(crate) fn new(element: &'a HTMLElement) -> Self {
338        ElementStylesheetLoader { element }
339    }
340}
341
342impl ElementStylesheetLoader<'_> {
343    pub(crate) fn load(
344        &self,
345        source: StylesheetContextSource,
346        url: ServoUrl,
347        cors_setting: Option<CorsSettings>,
348        integrity_metadata: String,
349    ) {
350        let document = self.element.owner_document();
351        let shadow_root = self
352            .element
353            .containing_shadow_root()
354            .map(|sr| Trusted::new(&*sr));
355        let generation = self
356            .element
357            .downcast::<HTMLLinkElement>()
358            .map(HTMLLinkElement::get_request_generation_id);
359        let context = StylesheetContext {
360            elem: Trusted::new(self.element),
361            source,
362            url: url.clone(),
363            metadata: None,
364            data: vec![],
365            document: Trusted::new(&*document),
366            shadow_root,
367            origin_clean: true,
368            request_generation_id: generation,
369            resource_timing: ResourceFetchTiming::new(ResourceTimingType::Resource),
370        };
371
372        let owner = self
373            .element
374            .upcast::<Element>()
375            .as_stylesheet_owner()
376            .expect("Stylesheet not loaded by <style> or <link> element!");
377        let referrer_policy = owner.referrer_policy();
378        owner.increment_pending_loads_count();
379        if owner.parser_inserted() {
380            document.increment_script_blocking_stylesheet_count();
381        }
382
383        // From <https://html.spec.whatwg.org/multipage/#link-type-stylesheet>:
384        // > A link element of this type is implicitly potentially render-blocking if the element
385        // > was created by its node document's parser.
386        if matches!(context.source, StylesheetContextSource::LinkElement { .. }) &&
387            owner.parser_inserted()
388        {
389            document.increment_render_blocking_element_count();
390        }
391
392        // https://html.spec.whatwg.org/multipage/#default-fetch-and-process-the-linked-resource
393        let global = self.element.global();
394        let request = create_a_potential_cors_request(
395            Some(document.webview_id()),
396            url.clone(),
397            Destination::Style,
398            cors_setting,
399            None,
400            global.get_referrer(),
401            document.insecure_requests_policy(),
402            document.has_trustworthy_ancestor_or_current_origin(),
403            global.policy_container(),
404        )
405        .origin(document.origin().immutable().clone())
406        .pipeline_id(Some(self.element.global().pipeline_id()))
407        .referrer_policy(referrer_policy)
408        .integrity_metadata(integrity_metadata);
409
410        document.fetch(LoadType::Stylesheet(url), request, context);
411    }
412}
413
414impl StyleStylesheetLoader for ElementStylesheetLoader<'_> {
415    /// Request a stylesheet after parsing a given `@import` rule, and return
416    /// the constructed `@import` rule.
417    fn request_stylesheet(
418        &self,
419        url: CssUrl,
420        source_location: SourceLocation,
421        lock: &SharedRwLock,
422        media: Arc<Locked<MediaList>>,
423        supports: Option<ImportSupportsCondition>,
424        layer: ImportLayer,
425    ) -> Arc<Locked<ImportRule>> {
426        // Ensure the supports conditions for this @import are true, if not, refuse to load
427        if supports.as_ref().is_some_and(|s| !s.enabled) {
428            return Arc::new(lock.wrap(ImportRule {
429                url,
430                stylesheet: ImportSheet::new_refused(),
431                supports,
432                layer,
433                source_location,
434            }));
435        }
436
437        let resolved_url = match url.url().cloned() {
438            Some(url) => url,
439            None => {
440                return Arc::new(lock.wrap(ImportRule {
441                    url,
442                    stylesheet: ImportSheet::new_refused(),
443                    supports,
444                    layer,
445                    source_location,
446                }));
447            },
448        };
449
450        let import_rule = Arc::new(lock.wrap(ImportRule {
451            url,
452            stylesheet: ImportSheet::new_pending(),
453            supports,
454            layer,
455            source_location,
456        }));
457
458        // TODO (mrnayak) : Whether we should use the original loader's CORS
459        // setting? Fix this when spec has more details.
460        let source = StylesheetContextSource::Import {
461            import_rule: import_rule.clone(),
462            media,
463        };
464        self.load(source, resolved_url.into(), None, "".to_owned());
465
466        import_rule
467    }
468}