Skip to main content

script/dom/css/
cssstylesheet.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::cell::{Cell, Ref};
6
7use dom_struct::dom_struct;
8use js::context::{JSContext, NoGC};
9use js::realm::CurrentRealm;
10use js::rust::HandleObject;
11use script_bindings::cell::DomRefCell;
12use script_bindings::codegen::GenericBindings::StyleSheetBinding::StyleSheetMethods;
13use script_bindings::inheritance::Castable;
14use script_bindings::reflector::{reflect_dom_object, reflect_dom_object_with_proto};
15use script_bindings::root::Dom;
16use servo_arc::Arc;
17use servo_url::ServoUrl;
18use style::shared_lock::{SharedRwLock, SharedRwLockReadGuard};
19use style::stylesheets::{
20    AllowImportRules, Origin, Stylesheet as StyleStyleSheet, StylesheetContents,
21    StylesheetInDocument, UrlExtraData,
22};
23
24use super::cssrulelist::{CSSRuleList, RulesSource};
25use super::stylesheet::StyleSheet;
26use super::stylesheetlist::StyleSheetListOwner;
27use crate::dom::bindings::codegen::Bindings::CSSStyleSheetBinding::{
28    CSSStyleSheetInit, CSSStyleSheetMethods,
29};
30use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
31use crate::dom::bindings::codegen::GenericBindings::CSSRuleListBinding::CSSRuleList_Binding::CSSRuleListMethods;
32use crate::dom::bindings::codegen::UnionTypes::MediaListOrString;
33use crate::dom::bindings::error::{Error, ErrorResult, Fallible};
34use crate::dom::bindings::refcounted::{Trusted, TrustedPromise};
35use crate::dom::bindings::reflector::DomGlobal;
36use crate::dom::bindings::root::{DomRoot, MutNullableDom};
37use crate::dom::bindings::str::{DOMString, USVString};
38use crate::dom::document::Document;
39use crate::dom::element::Element;
40use crate::dom::html::htmlstyleelement::HTMLStyleElement;
41use crate::dom::medialist::MediaList;
42use crate::dom::node::NodeTraits;
43use crate::dom::promise::RootedPromise;
44use crate::dom::types::Promise;
45use crate::dom::window::Window;
46
47#[dom_struct]
48pub(crate) struct CSSStyleSheet {
49    stylesheet: StyleSheet,
50
51    /// <https://drafts.csswg.org/cssom/#concept-css-style-sheet-owner-node>
52    owner_node: MutNullableDom<Element>,
53
54    /// <https://drafts.csswg.org/cssom/#ref-for-concept-css-style-sheet-css-rules>
55    rule_list: MutNullableDom<CSSRuleList>,
56
57    /// The inner Stylo's [Stylesheet].
58    #[ignore_malloc_size_of = "Stylo"]
59    #[no_trace]
60    style_stylesheet: DomRefCell<Arc<StyleStyleSheet>>,
61
62    /// The inner Stylo's [SharedRwLock], stored at here to avoid referencing
63    /// temporary variables.
64    #[no_trace]
65    style_shared_lock: SharedRwLock,
66
67    /// In which [Document] that this stylesheet was constructed.
68    ///
69    /// <https://drafts.csswg.org/cssom/#concept-css-style-sheet-constructor-document>
70    constructor_document: Option<Dom<Document>>,
71
72    /// <https://drafts.csswg.org/cssom/#concept-css-style-sheet-disallow-modification-flag>
73    disallow_modification: Cell<bool>,
74
75    /// <https://drafts.csswg.org/cssom/#concept-css-style-sheet-origin-clean-flag>
76    origin_clean: Cell<bool>,
77
78    /// <https://drafts.csswg.org/cssom/#concept-css-style-sheet-stylesheet-base-url>
79    #[no_trace]
80    stylesheet_base_url: DomRefCell<Option<ServoUrl>>,
81
82    /// Documents or shadow DOMs thats adopt this stylesheet, they will be notified whenever
83    /// the stylesheet is modified.
84    adopters: DomRefCell<Vec<StyleSheetListOwner>>,
85}
86
87impl CSSStyleSheet {
88    fn new_inherited(
89        owner: Option<&Element>,
90        type_: DOMString,
91        href: Option<DOMString>,
92        title: Option<DOMString>,
93        stylesheet: Arc<StyleStyleSheet>,
94        constructor_document: Option<&Document>,
95    ) -> CSSStyleSheet {
96        CSSStyleSheet {
97            stylesheet: StyleSheet::new_inherited(type_, href, title),
98            owner_node: MutNullableDom::new(owner),
99            rule_list: MutNullableDom::new(None),
100            style_shared_lock: stylesheet.shared_lock.clone(),
101            style_stylesheet: DomRefCell::new(stylesheet),
102            origin_clean: Cell::new(true),
103            constructor_document: constructor_document.map(Dom::from_ref),
104            adopters: Default::default(),
105            disallow_modification: Cell::new(false),
106            stylesheet_base_url: Default::default(),
107        }
108    }
109
110    #[allow(clippy::too_many_arguments)]
111    pub(crate) fn new(
112        cx: &mut JSContext,
113        window: &Window,
114        owner: Option<&Element>,
115        type_: DOMString,
116        href: Option<DOMString>,
117        title: Option<DOMString>,
118        stylesheet: Arc<StyleStyleSheet>,
119        constructor_document: Option<&Document>,
120    ) -> DomRoot<CSSStyleSheet> {
121        reflect_dom_object(
122            cx,
123            Box::new(CSSStyleSheet::new_inherited(
124                owner,
125                type_,
126                href,
127                title,
128                stylesheet,
129                constructor_document,
130            )),
131            window,
132        )
133    }
134
135    #[allow(clippy::too_many_arguments)]
136    fn new_with_proto(
137        cx: &mut JSContext,
138        window: &Window,
139        proto: Option<HandleObject>,
140        owner: Option<&Element>,
141        type_: DOMString,
142        href: Option<DOMString>,
143        title: Option<DOMString>,
144        stylesheet: Arc<StyleStyleSheet>,
145        constructor_document: Option<&Document>,
146    ) -> DomRoot<CSSStyleSheet> {
147        reflect_dom_object_with_proto(
148            cx,
149            Box::new(CSSStyleSheet::new_inherited(
150                owner,
151                type_,
152                href,
153                title,
154                stylesheet,
155                constructor_document,
156            )),
157            window,
158            proto,
159        )
160    }
161
162    pub(crate) fn rulelist(&self, cx: &mut JSContext) -> DomRoot<CSSRuleList> {
163        self.rule_list.or_init(|| {
164            let sheet = self.style_stylesheet.borrow();
165            let guard = sheet.shared_lock.read();
166            let rules = sheet.contents(&guard).rules.clone();
167            CSSRuleList::new(
168                cx,
169                self.global().as_window(),
170                None,
171                self,
172                RulesSource::Rules(rules),
173            )
174        })
175    }
176
177    pub(crate) fn disabled(&self) -> bool {
178        self.style_stylesheet.borrow().disabled()
179    }
180
181    pub(crate) fn href(&self) -> Option<DOMString> {
182        self.upcast::<StyleSheet>().GetHref()
183    }
184
185    pub(crate) fn title(&self) -> DOMString {
186        self.upcast::<StyleSheet>().GetTitle().unwrap_or_default()
187    }
188
189    pub(crate) fn get_rule_count(&self) -> u32 {
190        let sheet = self.style_stylesheet.borrow();
191        let guard = sheet.shared_lock.read();
192        sheet.contents(&guard).rules.read_with(&guard).0.len() as u32
193    }
194
195    pub(crate) fn origin(&self) -> Origin {
196        let guard = self.style_shared_lock.read();
197        self.style_stylesheet()
198            .clone()
199            .contents
200            .read_with(&guard)
201            .origin
202    }
203
204    pub(crate) fn owner_node(&self) -> Option<DomRoot<Element>> {
205        self.owner_node.get()
206    }
207
208    pub(crate) fn set_disabled(&self, no_gc: &NoGC, disabled: bool) {
209        if self.style_stylesheet.borrow().set_disabled(disabled) {
210            self.notify_invalidations(no_gc);
211        }
212    }
213
214    pub(crate) fn set_owner_node(&self, value: Option<&Element>) {
215        self.owner_node.set(value);
216    }
217
218    pub(crate) fn shared_lock(&self) -> &SharedRwLock {
219        &self.style_shared_lock
220    }
221
222    pub(crate) fn style_stylesheet(&self) -> Ref<'_, Arc<StyleStyleSheet>> {
223        self.style_stylesheet.borrow()
224    }
225
226    pub(crate) fn set_origin_clean(&self, origin_clean: bool) {
227        self.origin_clean.set(origin_clean);
228    }
229
230    pub(crate) fn medialist(&self, cx: &mut JSContext) -> DomRoot<MediaList> {
231        MediaList::new(
232            cx,
233            self.global().as_window(),
234            self,
235            self.style_stylesheet().media.clone(),
236        )
237    }
238
239    /// <https://drafts.csswg.org/cssom/#concept-css-style-sheet-constructed-flag>
240    #[inline]
241    pub(crate) fn is_constructed(&self) -> bool {
242        self.constructor_document.is_some()
243    }
244
245    pub(crate) fn constructor_document_matches(&self, other_doc: &Document) -> bool {
246        match &self.constructor_document {
247            Some(doc) => *doc == other_doc,
248            None => false,
249        }
250    }
251
252    /// Add a [StyleSheetListOwner] as an adopter to be notified whenever this stylesheet is
253    /// modified.
254    #[cfg_attr(crown, expect(crown::unrooted_must_root))]
255    pub(crate) fn add_adopter(&self, owner: StyleSheetListOwner) {
256        debug_assert!(self.is_constructed());
257        self.adopters.borrow_mut().push(owner);
258    }
259
260    pub(crate) fn remove_adopter(&self, owner: &StyleSheetListOwner) {
261        let adopters = &mut *self.adopters.borrow_mut();
262        if let Some(index) = adopters.iter().position(|o| o == owner) {
263            adopters.swap_remove(index);
264        }
265    }
266
267    pub(crate) fn will_modify(&self, no_gc: &NoGC) {
268        let Some(node) = self.owner_node.get() else {
269            return;
270        };
271
272        let Some(node) = node.downcast::<HTMLStyleElement>() else {
273            return;
274        };
275
276        node.will_modify_stylesheet(no_gc);
277    }
278
279    pub(crate) fn update_style_stylesheet(
280        &self,
281        style_stylesheet: &Arc<StyleStyleSheet>,
282        guard: &SharedRwLockReadGuard,
283    ) {
284        // When the shared `StylesheetContents` is about to be modified,
285        // `CSSStyleSheet::owner_node` performs a copy-on-write to avoid
286        // affecting other sharers, see `CSSStyleSheet::will_modify`. And
287        // then updates the references to `CssRule` or `PropertyDeclarationBlock`
288        // stored in the CSSOMs to ensure that modifications are made only
289        // on the new copy.
290        *self.style_stylesheet.borrow_mut() = style_stylesheet.clone();
291        if let Some(rulelist) = self.rule_list.get() {
292            let rules = style_stylesheet.contents(guard).rules.clone();
293            rulelist.update_rules(RulesSource::Rules(rules), guard);
294        }
295    }
296
297    /// Invalidate all stylesheet set this stylesheet is a part on.
298    pub(crate) fn notify_invalidations(&self, no_gc: &NoGC) {
299        if let Some(owner) = self.owner_node() {
300            owner.stylesheet_list_owner().invalidate_stylesheets(no_gc);
301        }
302        for adopter in self.adopters.borrow().iter() {
303            adopter.invalidate_stylesheets(no_gc);
304        }
305    }
306
307    /// <https://drafts.csswg.org/cssom/#concept-css-style-sheet-disallow-modification-flag>
308    pub(crate) fn disallow_modification(&self) -> bool {
309        self.disallow_modification.get()
310    }
311
312    /// <https://drafts.csswg.org/cssom/#dom-cssstylesheet-replacesync> Steps 2+
313    fn do_replace_sync(&self, no_gc: &NoGC, text: USVString) {
314        // Step 2. Let rules be the result of running parse a stylesheet’s contents from text.
315        let global = self.global();
316        let window = global.as_window();
317
318        self.will_modify(no_gc);
319
320        let _span = profile_traits::trace_span!("ParseStylesheet").entered();
321        let sheet = self.style_stylesheet();
322        let stylesheet_base_url = self
323            .stylesheet_base_url
324            .borrow()
325            .clone()
326            .unwrap_or(window.get_url());
327
328        let new_contents = StylesheetContents::from_str(
329            &text,
330            UrlExtraData(stylesheet_base_url.get_arc()),
331            Origin::Author,
332            &self.style_shared_lock,
333            None,
334            Some(window.css_error_reporter()),
335            window.Document().quirks_mode(),
336            AllowImportRules::No, // Step 3.If rules contains one or more @import rules, remove those rules from rules.
337            /* sanitization_data = */ None,
338        );
339
340        {
341            let mut write_guard = self.style_shared_lock.write();
342            *sheet.contents.write_with(&mut write_guard) = new_contents;
343        }
344
345        // Step 4. Set sheet’s CSS rules to rules.
346        // We reset our rule list, which will be initialized properly
347        // at the next getter access.
348        self.rule_list.set(None);
349
350        // Notify invalidation to update the styles immediately.
351        self.notify_invalidations(no_gc);
352    }
353}
354
355impl CSSStyleSheetMethods<crate::DomTypeHolder> for CSSStyleSheet {
356    /// <https://drafts.csswg.org/cssom/#dom-cssstylesheet-cssstylesheet>
357    fn Constructor(
358        cx: &mut JSContext,
359        window: &Window,
360        proto: Option<HandleObject>,
361        options: &CSSStyleSheetInit,
362    ) -> DomRoot<Self> {
363        let doc = window.Document();
364        let shared_lock = doc.style_shared_author_lock().clone();
365        let media = Arc::new(shared_lock.wrap(match &options.media {
366            MediaListOrString::MediaList(media_list) => media_list.clone_media_list(),
367            MediaListOrString::String(str) => MediaList::parse_media_list(&str.str(), window),
368        }));
369
370        let stylesheet = Arc::new(StyleStyleSheet::from_str(
371            "",
372            UrlExtraData(window.get_url().get_arc()),
373            Origin::Author,
374            media,
375            shared_lock,
376            None,
377            Some(window.css_error_reporter()),
378            doc.quirks_mode(),
379            AllowImportRules::No,
380        ));
381        if options.disabled {
382            stylesheet.set_disabled(true);
383        }
384
385        // Step 1. Construct a new CSSStyleSheet object sheet.
386        // Note: Subsequent steps are implicitly handled by the arguments passed down.
387        let sheet = Self::new_with_proto(
388            cx,
389            window,
390            proto,
391            None, // owner
392            DOMString::from_static("text/css"),
393            None, // href
394            None, // title
395            stylesheet,
396            Some(&window.Document()), // constructor_document
397        );
398
399        let base_url = options
400            .baseURL
401            .as_ref()
402            .and_then(|url| ServoUrl::parse(&url.str()).ok());
403        // Step 3. Set sheet’s stylesheet base URL to the baseURL attribute value from options.
404        *sheet.stylesheet_base_url.safe_borrow_mut(cx) = base_url;
405
406        // Step 14. Return sheet.
407        sheet
408    }
409
410    /// <https://drafts.csswg.org/cssom/#dom-cssstylesheet-cssrules>
411    fn GetCssRules(&self, cx: &mut JSContext) -> Fallible<DomRoot<CSSRuleList>> {
412        // If the origin-clean flag is unset, we throw an error as the API implicitly allows modification of CSS rules.
413        if !self.origin_clean.get() {
414            return Err(Error::Security(Some(
415                "Not allowed to access cross-origin style sheet".to_string(),
416            )));
417        }
418        Ok(self.rulelist(cx))
419    }
420
421    /// <https://drafts.csswg.org/cssom/#dom-cssstylesheet-insertrule>
422    fn InsertRule(&self, cx: &mut JSContext, rule: DOMString, index: u32) -> Fallible<u32> {
423        // Step 1. If the origin-clean flag is unset, throw a SecurityError exception.
424        if !self.origin_clean.get() {
425            return Err(Error::Security(Some(
426                "Not allowed to access cross-origin style sheet".to_string(),
427            )));
428        }
429
430        // Step 2. If the disallow modification flag is set, throw a NotAllowedError DOMException.
431        if self.disallow_modification() {
432            return Err(Error::NotAllowed(Some(
433                "This method can only be called on modifiable style sheets".to_string(),
434            )));
435        }
436
437        self.rulelist(cx).insert_rule(cx, &rule, index)
438    }
439
440    /// <https://drafts.csswg.org/cssom/#dom-cssstylesheet-deleterule>
441    fn DeleteRule(&self, cx: &mut JSContext, index: u32) -> ErrorResult {
442        // Step 1. If the origin-clean flag is unset, throw a SecurityError exception.
443        if !self.origin_clean.get() {
444            return Err(Error::Security(Some(
445                "Not allowed to access cross-origin style sheet".to_string(),
446            )));
447        }
448
449        // Step 2. If the disallow modification flag is set, throw a NotAllowedError DOMException.
450        if self.disallow_modification() {
451            return Err(Error::NotAllowed(Some(
452                "This method can only be called on modifiable style sheets".to_string(),
453            )));
454        }
455        self.rulelist(cx).remove_rule(cx, index)
456    }
457
458    /// <https://drafts.csswg.org/cssom/#dom-cssstylesheet-rules>
459    fn GetRules(&self, cx: &mut JSContext) -> Fallible<DomRoot<CSSRuleList>> {
460        self.GetCssRules(cx)
461    }
462
463    /// <https://drafts.csswg.org/cssom/#dom-cssstylesheet-removerule>
464    fn RemoveRule(&self, cx: &mut JSContext, index: u32) -> ErrorResult {
465        self.DeleteRule(cx, index)
466    }
467
468    /// <https://drafts.csswg.org/cssom/#dom-cssstylesheet-addrule>
469    fn AddRule(
470        &self,
471        cx: &mut js::context::JSContext,
472        selector: DOMString,
473        block: DOMString,
474        optional_index: Option<u32>,
475    ) -> Fallible<i32> {
476        // > 1. Let *rule* be an empty string.
477        // > 2. Append *selector* to *rule*.
478        let mut rule = selector;
479
480        // > 3. Append " { " to *rule*.
481        // > 4. If *block* is not empty, append *block*, followed by a space, to *rule*.
482        // > 5. Append "}" to *rule*.
483        if block.is_empty() {
484            rule.push_str(" { }");
485        } else {
486            rule.push_str(" { ");
487            rule.push_str(&block.str());
488            rule.push_str(" }");
489        };
490
491        // > 6. Let *index* be *optionalIndex* if provided, or the number of CSS rules in the stylesheet otherwise.
492        let index = optional_index.unwrap_or_else(|| self.rulelist(cx).Length());
493
494        // > 7. Call `insertRule()`, with *rule* and *index* as arguments.
495        self.InsertRule(cx, rule, index)?;
496
497        // > 8. Return -1.
498        Ok(-1)
499    }
500
501    /// <https://drafts.csswg.org/cssom/#dom-cssstylesheet-replace>
502    fn Replace(&self, cx: &mut CurrentRealm, text: USVString) -> Fallible<RootedPromise> {
503        // Step 1. Let promise be a promise.
504        let promise = Promise::new_in_realm_rooted(cx);
505
506        // Step 2. If the constructed flag is not set, or the disallow modification flag is set,
507        // reject promise with a NotAllowedError DOMException and return promise.
508        if !self.is_constructed() {
509            return Err(Error::NotAllowed(Some(
510                "This method can only be called on constructed style sheets".to_string(),
511            )));
512        }
513        if self.disallow_modification() {
514            return Err(Error::NotAllowed(Some(
515                "This method can only be called on modifiable style sheets".to_string(),
516            )));
517        }
518
519        // Step 3. Set the disallow modification flag.
520        self.disallow_modification.set(true);
521
522        // Step 4. In parallel, do these steps:
523        let trusted_sheet = Trusted::new(self);
524        let trusted_promise = TrustedPromise::from(&promise);
525
526        self.global()
527            .task_manager()
528            .dom_manipulation_task_source()
529            .queue(task!(cssstylesheet_replace: move |cx| {
530                let sheet = trusted_sheet.root();
531
532                // Step 4.1..4.3
533                sheet.do_replace_sync(cx.no_gc(), text);
534
535                // Step 4.4. Unset sheet’s disallow modification flag.
536                sheet.disallow_modification.set(false);
537
538                // Step 4.5. Resolve promise with sheet.
539                trusted_promise.root(cx).resolve_native(cx, &sheet);
540            }));
541
542        Ok(promise)
543    }
544
545    /// <https://drafts.csswg.org/cssom/#dom-cssstylesheet-replacesync>
546    fn ReplaceSync(&self, no_gc: &NoGC, text: USVString) -> Result<(), Error> {
547        // Step 1. If the constructed flag is not set, or the disallow modification flag is set,
548        // throw a NotAllowedError DOMException.
549        if !self.is_constructed() || self.disallow_modification() {
550            return Err(Error::NotAllowed(Some(
551                "This method can only be called on constructed style sheets".to_string(),
552            )));
553        }
554        if self.disallow_modification() {
555            return Err(Error::NotAllowed(Some(
556                "This method can only be called on modifiable style sheets".to_string(),
557            )));
558        }
559        self.do_replace_sync(no_gc, text);
560        Ok(())
561    }
562}