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, NoGC};
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 }
75 // https://html.spec.whatwg.org/multipage/#attr-meta-http-equiv
76 if !self.HttpEquiv().is_empty() {
77 // TODO: Implement additional http-equiv candidates
78 if self.HttpEquiv().eq_ignore_ascii_case("refresh") {
79 self.declarative_refresh(cx.no_gc());
80 } else if self
81 .HttpEquiv()
82 .eq_ignore_ascii_case("content-security-policy")
83 {
84 self.apply_csp_list();
85 } else if self.HttpEquiv().eq_ignore_ascii_case("content-language") {
86 self.pragma_set_default_language();
87 }
88 }
89 }
90
91 fn process_referrer_attribute(&self) {
92 let element = self.upcast::<Element>();
93 if let Some(ref name) = element.get_name() {
94 let name = name.trim_matches(HTML_SPACE_CHARACTERS);
95
96 if name.eq_ignore_ascii_case("referrer") {
97 self.apply_referrer();
98 }
99 }
100 }
101
102 /// <https://html.spec.whatwg.org/multipage/#meta-referrer>
103 fn apply_referrer(&self) {
104 let doc = self.owner_document();
105 // From spec: For historical reasons, unlike other standard metadata names, the processing model for referrer
106 // is not responsive to element removals, and does not use tree order. Only the most-recently-inserted or
107 // most-recently-modified meta element in this state has an effect.
108 // Step 1. If element is not in a document tree, then return.
109 let meta_node = self.upcast::<Node>();
110 if !meta_node.is_in_a_document_tree() {
111 return;
112 }
113
114 // Step 2. If element does not have a name attribute whose value is an ASCII
115 // case-insensitive match for "referrer", then return.
116 if self.upcast::<Element>().get_name() != Some(atom!("referrer")) {
117 return;
118 }
119
120 // Step 3. If element does not have a content attribute, or that attribute's value is the
121 // empty string, then return.
122 if let Some(content) = self
123 .upcast::<Element>()
124 .get_attribute_string_ref(&local_name!("content"))
125 .filter(|value| !value.is_empty())
126 {
127 let content = content.as_attr_ref().value();
128 // Step 4. Let value be the value of element's content attribute, converted to ASCII
129 // lowercase.
130 // Step 5. If value is one of the values given in the first column of the following
131 // table, then set value to the value given in the second column:
132 // Step 6. If value is a referrer policy, then set element's node document's policy
133 // container's referrer policy to policy.
134 doc.set_referrer_policy(ReferrerPolicy::from_with_legacy(&content));
135 }
136 }
137
138 /// <https://drafts.csswg.org/css-viewport/#parsing-algorithm>
139 fn parse_and_send_viewport_if_necessary(&self, cx: &mut JSContext) {
140 if !pref!(viewport_meta_enabled) {
141 return;
142 }
143
144 // Skip processing if this isn't the top level frame
145 if !self.owner_window().is_top_level() {
146 return;
147 }
148 let element = self.upcast::<Element>();
149 let Some(content) = element.get_attribute_string_value(&local_name!("content")) else {
150 return;
151 };
152
153 if let Ok(viewport) = ViewportDescription::from_str(&content) {
154 let initial_scale = viewport.initial_scale.get();
155 let window = self.owner_window();
156 window.paint_api().viewport(window.webview_id(), viewport);
157 window
158 .get_or_init_visual_viewport(cx)
159 .update_scale(initial_scale);
160 }
161 }
162
163 /// <https://html.spec.whatwg.org/multipage/#meta-color-scheme>
164 fn obtain_page_supported_color_schemes(&self, cx: &mut JSContext) {
165 let doc = self.owner_document();
166 // Step 1. Let candidate elements be the list of all meta elements
167 // that meet the following criteria, in tree order:
168 let new_theme = doc
169 .upcast::<Node>()
170 // Do not traverse shadow trees for optimization, which also implies:
171 // > the element is in a document tree;
172 .traverse_preorder_unrooted(cx.no_gc(), ShadowIncluding::No)
173 .filter_map(UnrootedDom::downcast::<HTMLMetaElement>)
174 .filter_map(|meta| {
175 let element = UnrootedDom::upcast::<Element>(meta);
176
177 // > the element has a content attribute.
178 element
179 .get_attribute_string_value(&local_name!("content"))
180 .filter(|_| {
181 // > the element has a name attribute,
182 // > whose value is an ASCII case-insensitive match for color-scheme; and
183 element.get_name().is_color_scheme()
184 })
185 })
186 // Step 2. For each element in candidate elements:
187 .find_map(|content| {
188 // Step 2.1. Let parsed be the result of parsing a list of
189 // component values given the value of element's content attribute.
190 // Step 2.2. If parsed is a valid CSS 'color-scheme' property value,
191 // then return parsed.
192 // TODO: Allow for more different themes than the ones that embedders can set
193 if content.eq_ignore_ascii_case("dark") {
194 Some(Theme::Dark)
195 } else if content.eq_ignore_ascii_case("light") {
196 Some(Theme::Light)
197 } else {
198 // Step 3. Return null.
199 None
200 }
201 });
202
203 doc.set_theme(new_theme);
204 }
205
206 /// <https://html.spec.whatwg.org/multipage/#attr-meta-http-equiv-content-security-policy>
207 fn apply_csp_list(&self) {
208 // Step 1. If the meta element is not a child of a head element, return.
209 if self
210 .upcast::<Node>()
211 .GetParentElement()
212 .is_none_or(|parent| !parent.is::<HTMLHeadElement>())
213 {
214 return;
215 };
216 // Step 2. If the meta element has no content attribute, or if that attribute's value is the empty string, then return.
217 let Some(content) = self
218 .upcast::<Element>()
219 .get_attribute_string_value(&local_name!("content"))
220 else {
221 return;
222 };
223 if content.is_empty() {
224 return;
225 }
226 // Step 3. Let policy be the result of executing Content Security Policy's
227 // parse a serialized Content Security Policy algorithm
228 // on the meta element's content attribute's value,
229 // with a source of "meta", and a disposition of "enforce".
230 let mut policy = Policy::parse(&content, PolicySource::Meta, PolicyDisposition::Enforce);
231 // Step 4. Remove all occurrences of the report-uri, frame-ancestors,
232 // and sandbox directives from policy.
233 policy.directive_set.retain(|directive| {
234 !matches!(
235 directive.name.as_str(),
236 "report-uri" | "frame-ancestors" | "sandbox"
237 )
238 });
239 // Step 5. Enforce the policy policy.
240 self.owner_document().enforce_csp_policy(policy);
241 }
242
243 /// <https://html.spec.whatwg.org/multipage/#shared-declarative-refresh-steps>
244 fn declarative_refresh(&self, no_gc: &NoGC) {
245 if !self.upcast::<Node>().is_in_a_document_tree() {
246 return;
247 }
248
249 // Step 2. Let input be the value of the element's content attribute.
250 let content = self.Content();
251 // Step 1. If the meta element has no content attribute, or if that attribute's value is the empty string, then return.
252 if !content.is_empty() {
253 // Step 3. Run the shared declarative refresh steps with the meta element's node document, input, and the meta element.
254 self.owner_document().shared_declarative_refresh_steps(
255 &content.as_bytes(no_gc),
256 /* from_meta_element */ true,
257 );
258 }
259 }
260
261 /// <https://html.spec.whatwg.org/multipage/#pragma-set-default-language>
262 fn pragma_set_default_language(&self) {
263 // Step 3. Let input be the value of the element's content attribute.
264 let input = self.Content();
265 let input = input.str();
266 // Step 2. If the element's content attribute contains
267 // a U+002C COMMA character (,), then return.
268 let candidate = if input.contains('\u{002C}') {
269 None
270 } else {
271 // Step 1. If the meta element has no content attribute, then return.
272 // Step 4. Let position point at the first character of input.
273 // Step 5. Skip ASCII whitespace within input given position.
274 // Step 6. Collect a sequence of code points that are
275 // not ASCII whitespace from input given position.
276 // Step 7. Let candidate be the string that resulted from the previous step.
277 // Step 8. If candidate is the empty string, return.
278 input
279 .trim_start()
280 .split_ascii_whitespace()
281 .next()
282 .filter(|candidate| !candidate.is_empty())
283 .map(|candidate| candidate.to_owned())
284 };
285
286 // Step 9. Set the pragma-set default language to candidate.
287 self.owner_document().set_default_language(candidate);
288 }
289}
290
291impl HTMLMetaElementMethods<crate::DomTypeHolder> for HTMLMetaElement {
292 // https://html.spec.whatwg.org/multipage/#dom-meta-name
293 make_getter!(Name, "name");
294
295 // https://html.spec.whatwg.org/multipage/#dom-meta-name
296 make_atomic_setter!(SetName, "name");
297
298 // https://html.spec.whatwg.org/multipage/#dom-meta-content
299 make_getter!(Content, "content");
300
301 // https://html.spec.whatwg.org/multipage/#dom-meta-content
302 make_setter!(SetContent, "content");
303
304 // https://html.spec.whatwg.org/multipage/#dom-meta-httpequiv
305 make_getter!(HttpEquiv, "http-equiv");
306 // https://html.spec.whatwg.org/multipage/#dom-meta-httpequiv
307 make_atomic_setter!(SetHttpEquiv, "http-equiv");
308
309 // https://html.spec.whatwg.org/multipage/#dom-meta-scheme
310 make_getter!(Scheme, "scheme");
311 // https://html.spec.whatwg.org/multipage/#dom-meta-scheme
312 make_setter!(SetScheme, "scheme");
313}
314
315impl VirtualMethods for HTMLMetaElement {
316 fn super_type(&self) -> Option<&dyn VirtualMethods> {
317 Some(self.upcast::<HTMLElement>() as &dyn VirtualMethods)
318 }
319
320 fn bind_to_tree(&self, cx: &mut JSContext, context: &BindContext) {
321 if let Some(s) = self.super_type() {
322 s.bind_to_tree(cx, context);
323 }
324
325 if context.tree_connected {
326 self.process_attributes(cx);
327
328 // Optimization: only if this meta element has a color scheme we should update.
329 // Otherwise we would traverse the whole DOM for any meta element, which are
330 // commonly used for information for crawlers.
331 if self.upcast::<Element>().get_name().is_color_scheme() {
332 // https://html.spec.whatwg.org/multipage/#meta-color-scheme
333 // > If any meta elements are inserted into the document or removed from the document,
334 // > or existing meta elements have their name or content attributes changed,
335 // > user agents must re-run the above algorithm.
336 //
337 // When the element is inserted
338 self.obtain_page_supported_color_schemes(cx);
339 }
340 }
341 }
342
343 fn attribute_mutated(
344 &self,
345 cx: &mut js::context::JSContext,
346 attr: AttrRef<'_>,
347 mutation: AttributeMutation,
348 ) {
349 if let Some(s) = self.super_type() {
350 s.attribute_mutated(cx, attr, mutation);
351 }
352
353 self.process_referrer_attribute();
354
355 // Optimization: only if this meta element either did or does now specify a color-scheme.
356 // Or if the content of a meta element is changed that specifies a color-scheme
357 // Otherwise we would traverse the whole DOM for any meta element, which are
358 // commonly used for information for crawlers.
359 let affects_color_scheme = if *attr.local_name() == local_name!("name") {
360 mutation.old_value(attr).is_color_scheme() || mutation.new_value(attr).is_color_scheme()
361 } else {
362 self.upcast::<Element>().get_name().is_color_scheme() &&
363 *attr.local_name() == local_name!("content")
364 };
365
366 if affects_color_scheme {
367 // https://html.spec.whatwg.org/multipage/#meta-color-scheme
368 // > If any meta elements are inserted into the document or removed from the document,
369 // > or existing meta elements have their name or content attributes changed,
370 // > user agents must re-run the above algorithm.
371 //
372 // When the content attribute has changed
373 self.obtain_page_supported_color_schemes(cx);
374 }
375 }
376
377 fn unbind_from_tree(&self, cx: &mut js::context::JSContext, context: &UnbindContext) {
378 if let Some(s) = self.super_type() {
379 s.unbind_from_tree(cx, context);
380 }
381
382 if context.tree_connected {
383 self.process_referrer_attribute();
384
385 // Optimization: only if this meta element has a color scheme we should update.
386 // Otherwise we would traverse the whole DOM for any meta element, which are
387 // commonly used for information for crawlers.
388 if self.upcast::<Element>().get_name().is_color_scheme() {
389 // https://html.spec.whatwg.org/multipage/#meta-color-scheme
390 // > If any meta elements are inserted into the document or removed from the document,
391 // > or existing meta elements have their name or content attributes changed,
392 // > user agents must re-run the above algorithm.
393 //
394 // When the element is removed
395 self.obtain_page_supported_color_schemes(cx);
396 }
397 }
398 }
399}
400
401/// Trait to make it easier to make sure all callers lowercase to ASCII
402/// before comparing to the `color-scheme` value.
403/// Otherwise it is easy to miss one usage and compare case-sensitively.
404trait IsColorSchemeValue {
405 fn is_color_scheme(&self) -> bool;
406}
407
408impl<T: AsRef<str>> IsColorSchemeValue for Option<T> {
409 fn is_color_scheme(&self) -> bool {
410 self.as_ref()
411 .is_some_and(|name| name.as_ref().eq_ignore_ascii_case("color-scheme"))
412 }
413}