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