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