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