Skip to main content

script/dom/html/document_metadata/
htmlmetaelement.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::str::FromStr;
6
7use content_security_policy::{Policy, PolicyDisposition, PolicySource};
8use dom_struct::dom_struct;
9use embedder_traits::Theme;
10use html5ever::{LocalName, Prefix, local_name};
11use js::context::JSContext;
12use js::rust::HandleObject;
13use net_traits::ReferrerPolicy;
14use paint_api::viewport_description::ViewportDescription;
15use script_bindings::dom::UnrootedDom;
16use servo_config::pref;
17use style::str::HTML_SPACE_CHARACTERS;
18
19use crate::dom::bindings::codegen::Bindings::HTMLMetaElementBinding::HTMLMetaElementMethods;
20use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
21use crate::dom::bindings::inheritance::Castable;
22use crate::dom::bindings::root::DomRoot;
23use crate::dom::bindings::str::DOMString;
24use crate::dom::document::Document;
25use crate::dom::element::attributes::storage::AttrRef;
26use crate::dom::element::{AttributeMutation, Element};
27use crate::dom::html::htmlelement::HTMLElement;
28use crate::dom::html::htmlheadelement::HTMLHeadElement;
29use crate::dom::iterators::ShadowIncluding;
30use crate::dom::node::virtualmethods::VirtualMethods;
31use crate::dom::node::{BindContext, Node, NodeTraits, UnbindContext};
32
33#[dom_struct]
34pub(crate) struct HTMLMetaElement {
35    htmlelement: HTMLElement,
36}
37
38impl HTMLMetaElement {
39    fn new_inherited(
40        local_name: LocalName,
41        prefix: Option<Prefix>,
42        document: &Document,
43    ) -> HTMLMetaElement {
44        HTMLMetaElement {
45            htmlelement: HTMLElement::new_inherited(local_name, prefix, document),
46        }
47    }
48
49    pub(crate) fn new(
50        cx: &mut js::context::JSContext,
51        local_name: LocalName,
52        prefix: Option<Prefix>,
53        document: &Document,
54        proto: Option<HandleObject>,
55    ) -> DomRoot<HTMLMetaElement> {
56        Node::reflect_node_with_proto(
57            cx,
58            Box::new(HTMLMetaElement::new_inherited(local_name, prefix, document)),
59            document,
60            proto,
61        )
62    }
63
64    fn process_attributes(&self, cx: &mut JSContext) {
65        let element = self.upcast::<Element>();
66        if let Some(ref name) = element.get_name() {
67            let name = name.trim_matches(HTML_SPACE_CHARACTERS);
68            if name.eq_ignore_ascii_case("referrer") {
69                self.apply_referrer();
70            }
71            if name.eq_ignore_ascii_case("viewport") {
72                self.parse_and_send_viewport_if_necessary(cx);
73            }
74        // https://html.spec.whatwg.org/multipage/#attr-meta-http-equiv
75        } else if !self.HttpEquiv().is_empty() {
76            // TODO: Implement additional http-equiv candidates
77            if self.HttpEquiv().eq_ignore_ascii_case("refresh") {
78                self.declarative_refresh();
79            } else if self
80                .HttpEquiv()
81                .eq_ignore_ascii_case("content-security-policy")
82            {
83                self.apply_csp_list();
84            }
85        }
86    }
87
88    fn process_referrer_attribute(&self) {
89        let element = self.upcast::<Element>();
90        if let Some(ref name) = element.get_name() {
91            let name = name.trim_matches(HTML_SPACE_CHARACTERS);
92
93            if name.eq_ignore_ascii_case("referrer") {
94                self.apply_referrer();
95            }
96        }
97    }
98
99    /// <https://html.spec.whatwg.org/multipage/#meta-referrer>
100    fn apply_referrer(&self) {
101        let doc = self.owner_document();
102        // From spec: For historical reasons, unlike other standard metadata names, the processing model for referrer
103        // is not responsive to element removals, and does not use tree order. Only the most-recently-inserted or
104        // most-recently-modified meta element in this state has an effect.
105        // Step 1. If element is not in a document tree, then return.
106        let meta_node = self.upcast::<Node>();
107        if !meta_node.is_in_a_document_tree() {
108            return;
109        }
110
111        // Step 2. If element does not have a name attribute whose value is an ASCII
112        // case-insensitive match for "referrer", then return.
113        if self.upcast::<Element>().get_name() != Some(atom!("referrer")) {
114            return;
115        }
116
117        // Step 3. If element does not have a content attribute, or that attribute's value is the
118        // empty string, then return.
119        if let Some(content) = self
120            .upcast::<Element>()
121            .get_attribute_string_value(&local_name!("content"))
122            .filter(|value| !value.is_empty())
123        {
124            // Step 4. Let value be the value of element's content attribute, converted to ASCII
125            // lowercase.
126            // Step 5. If value is one of the values given in the first column of the following
127            // table, then set value to the value given in the second column:
128            // Step 6. If value is a referrer policy, then set element's node document's policy
129            // container's referrer policy to policy.
130            doc.set_referrer_policy(ReferrerPolicy::from_with_legacy(&content));
131        }
132    }
133
134    /// <https://drafts.csswg.org/css-viewport/#parsing-algorithm>
135    fn parse_and_send_viewport_if_necessary(&self, cx: &mut JSContext) {
136        if !pref!(viewport_meta_enabled) {
137            return;
138        }
139
140        // Skip processing if this isn't the top level frame
141        if !self.owner_window().is_top_level() {
142            return;
143        }
144        let element = self.upcast::<Element>();
145        let Some(content) = element.get_attribute_string_value(&local_name!("content")) else {
146            return;
147        };
148
149        if let Ok(viewport) = ViewportDescription::from_str(&content) {
150            let initial_scale = viewport.initial_scale.get();
151            let window = self.owner_window();
152            window.paint_api().viewport(window.webview_id(), viewport);
153            window
154                .get_or_init_visual_viewport(cx)
155                .update_scale(initial_scale);
156        }
157    }
158
159    /// <https://html.spec.whatwg.org/multipage/#meta-color-scheme>
160    fn obtain_page_supported_color_schemes(&self, cx: &mut JSContext) {
161        let doc = self.owner_document();
162        // Step 1. Let candidate elements be the list of all meta elements
163        // that meet the following criteria, in tree order:
164        let new_theme = doc
165            .upcast::<Node>()
166            // Do not traverse shadow trees for optimization, which also implies:
167            // > the element is in a document tree;
168            .traverse_preorder_non_rooting(cx.no_gc(), ShadowIncluding::No)
169            .filter_map(UnrootedDom::downcast::<HTMLMetaElement>)
170            .filter_map(|meta| {
171                let element = UnrootedDom::upcast::<Element>(meta);
172
173                // > the element has a content attribute.
174                element
175                    .get_attribute_string_value(&local_name!("content"))
176                    .filter(|_| {
177                        // > the element has a name attribute,
178                        // > whose value is an ASCII case-insensitive match for color-scheme; and
179                        element.get_name().is_color_scheme()
180                    })
181            })
182            // Step 2. For each element in candidate elements:
183            .find_map(|content| {
184                // Step 2.1. Let parsed be the result of parsing a list of
185                // component values given the value of element's content attribute.
186                // Step 2.2. If parsed is a valid CSS 'color-scheme' property value,
187                // then return parsed.
188                // TODO: Allow for more different themes than the ones that embedders can set
189                if content.eq_ignore_ascii_case("dark") {
190                    Some(Theme::Dark)
191                } else if content.eq_ignore_ascii_case("light") {
192                    Some(Theme::Light)
193                } else {
194                    // Step 3. Return null.
195                    None
196                }
197            });
198
199        doc.set_theme(new_theme);
200    }
201
202    /// <https://html.spec.whatwg.org/multipage/#attr-meta-http-equiv-content-security-policy>
203    fn apply_csp_list(&self) {
204        // Step 1. If the meta element is not a child of a head element, return.
205        if self
206            .upcast::<Node>()
207            .GetParentElement()
208            .is_none_or(|parent| !parent.is::<HTMLHeadElement>())
209        {
210            return;
211        };
212        // Step 2. If the meta element has no content attribute, or if that attribute's value is the empty string, then return.
213        let Some(content) = self
214            .upcast::<Element>()
215            .get_attribute_string_value(&local_name!("content"))
216        else {
217            return;
218        };
219        if content.is_empty() {
220            return;
221        }
222        // Step 3. Let policy be the result of executing Content Security Policy's
223        // parse a serialized Content Security Policy algorithm
224        // on the meta element's content attribute's value,
225        // with a source of "meta", and a disposition of "enforce".
226        let mut policy = Policy::parse(&content, PolicySource::Meta, PolicyDisposition::Enforce);
227        // Step 4. Remove all occurrences of the report-uri, frame-ancestors,
228        // and sandbox directives from policy.
229        policy.directive_set.retain(|directive| {
230            !matches!(
231                directive.name.as_str(),
232                "report-uri" | "frame-ancestors" | "sandbox"
233            )
234        });
235        // Step 5. Enforce the policy policy.
236        self.owner_document().enforce_csp_policy(policy);
237    }
238
239    /// <https://html.spec.whatwg.org/multipage/#shared-declarative-refresh-steps>
240    fn declarative_refresh(&self) {
241        if !self.upcast::<Node>().is_in_a_document_tree() {
242            return;
243        }
244
245        // Step 2. Let input be the value of the element's content attribute.
246        let content = self.Content();
247        // Step 1. If the meta element has no content attribute, or if that attribute's value is the empty string, then return.
248        if !content.is_empty() {
249            // Step 3. Run the shared declarative refresh steps with the meta element's node document, input, and the meta element.
250            self.owner_document().shared_declarative_refresh_steps(
251                &content.as_bytes(),
252                /* from_meta_element */ true,
253            );
254        }
255    }
256}
257
258impl HTMLMetaElementMethods<crate::DomTypeHolder> for HTMLMetaElement {
259    // https://html.spec.whatwg.org/multipage/#dom-meta-name
260    make_getter!(Name, "name");
261
262    // https://html.spec.whatwg.org/multipage/#dom-meta-name
263    make_atomic_setter!(SetName, "name");
264
265    // https://html.spec.whatwg.org/multipage/#dom-meta-content
266    make_getter!(Content, "content");
267
268    // https://html.spec.whatwg.org/multipage/#dom-meta-content
269    make_setter!(SetContent, "content");
270
271    // https://html.spec.whatwg.org/multipage/#dom-meta-httpequiv
272    make_getter!(HttpEquiv, "http-equiv");
273    // https://html.spec.whatwg.org/multipage/#dom-meta-httpequiv
274    make_atomic_setter!(SetHttpEquiv, "http-equiv");
275
276    // https://html.spec.whatwg.org/multipage/#dom-meta-scheme
277    make_getter!(Scheme, "scheme");
278    // https://html.spec.whatwg.org/multipage/#dom-meta-scheme
279    make_setter!(SetScheme, "scheme");
280}
281
282impl VirtualMethods for HTMLMetaElement {
283    fn super_type(&self) -> Option<&dyn VirtualMethods> {
284        Some(self.upcast::<HTMLElement>() as &dyn VirtualMethods)
285    }
286
287    fn bind_to_tree(&self, cx: &mut JSContext, context: &BindContext) {
288        if let Some(s) = self.super_type() {
289            s.bind_to_tree(cx, context);
290        }
291
292        if context.tree_connected {
293            self.process_attributes(cx);
294
295            // Optimization: only if this meta element has a color scheme we should update.
296            // Otherwise we would traverse the whole DOM for any meta element, which are
297            // commonly used for information for crawlers.
298            if self.upcast::<Element>().get_name().is_color_scheme() {
299                // https://html.spec.whatwg.org/multipage/#meta-color-scheme
300                // > If any meta elements are inserted into the document or removed from the document,
301                // > or existing meta elements have their name or content attributes changed,
302                // > user agents must re-run the above algorithm.
303                //
304                // When the element is inserted
305                self.obtain_page_supported_color_schemes(cx);
306            }
307        }
308    }
309
310    fn attribute_mutated(
311        &self,
312        cx: &mut js::context::JSContext,
313        attr: AttrRef<'_>,
314        mutation: AttributeMutation,
315    ) {
316        if let Some(s) = self.super_type() {
317            s.attribute_mutated(cx, attr, mutation);
318        }
319
320        self.process_referrer_attribute();
321
322        // Optimization: only if this meta element either did or does now specify a color-scheme.
323        // Or if the content of a meta element is changed that specifies a color-scheme
324        // Otherwise we would traverse the whole DOM for any meta element, which are
325        // commonly used for information for crawlers.
326        let affects_color_scheme = if *attr.local_name() == local_name!("name") {
327            mutation.old_value(attr).is_color_scheme() || mutation.new_value(attr).is_color_scheme()
328        } else {
329            self.upcast::<Element>().get_name().is_color_scheme() &&
330                *attr.local_name() == local_name!("content")
331        };
332
333        if affects_color_scheme {
334            // https://html.spec.whatwg.org/multipage/#meta-color-scheme
335            // > If any meta elements are inserted into the document or removed from the document,
336            // > or existing meta elements have their name or content attributes changed,
337            // > user agents must re-run the above algorithm.
338            //
339            // When the content attribute has changed
340            self.obtain_page_supported_color_schemes(cx);
341        }
342    }
343
344    fn unbind_from_tree(&self, cx: &mut js::context::JSContext, context: &UnbindContext) {
345        if let Some(s) = self.super_type() {
346            s.unbind_from_tree(cx, context);
347        }
348
349        if context.tree_connected {
350            self.process_referrer_attribute();
351
352            // Optimization: only if this meta element has a color scheme we should update.
353            // Otherwise we would traverse the whole DOM for any meta element, which are
354            // commonly used for information for crawlers.
355            if self.upcast::<Element>().get_name().is_color_scheme() {
356                // https://html.spec.whatwg.org/multipage/#meta-color-scheme
357                // > If any meta elements are inserted into the document or removed from the document,
358                // > or existing meta elements have their name or content attributes changed,
359                // > user agents must re-run the above algorithm.
360                //
361                // When the element is removed
362                self.obtain_page_supported_color_schemes(cx);
363            }
364        }
365    }
366}
367
368/// Trait to make it easier to make sure all callers lowercase to ASCII
369/// before comparing to the `color-scheme` value.
370/// Otherwise it is easy to miss one usage and compare case-sensitively.
371trait IsColorSchemeValue {
372    fn is_color_scheme(&self) -> bool;
373}
374
375impl<T: AsRef<str>> IsColorSchemeValue for Option<T> {
376    fn is_color_scheme(&self) -> bool {
377        self.as_ref()
378            .is_some_and(|name| name.as_ref().eq_ignore_ascii_case("color-scheme"))
379    }
380}