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}