script/dom/
trustedtypepolicyfactory.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/. */
4use std::cell::RefCell;
5
6use dom_struct::dom_struct;
7use html5ever::{LocalName, Namespace, QualName, local_name, ns};
8use js::jsval::NullValue;
9use js::rust::HandleValue;
10use script_bindings::conversions::SafeToJSValConvertible;
11
12use crate::conversions::Convert;
13use crate::dom::bindings::codegen::Bindings::TrustedTypePolicyFactoryBinding::{
14    TrustedTypePolicyFactoryMethods, TrustedTypePolicyOptions,
15};
16use crate::dom::bindings::codegen::UnionTypes::TrustedHTMLOrTrustedScriptOrTrustedScriptURLOrString as TrustedTypeOrString;
17use crate::dom::bindings::conversions::root_from_handlevalue;
18use crate::dom::bindings::error::{Error, Fallible};
19use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object};
20use crate::dom::bindings::root::{DomRoot, MutNullableDom};
21use crate::dom::bindings::str::DOMString;
22use crate::dom::csp::CspReporting;
23use crate::dom::eventtarget::EventTarget;
24use crate::dom::globalscope::GlobalScope;
25use crate::dom::trustedhtml::TrustedHTML;
26use crate::dom::trustedscript::TrustedScript;
27use crate::dom::trustedscripturl::TrustedScriptURL;
28use crate::dom::trustedtypepolicy::{TrustedType, TrustedTypePolicy};
29use crate::script_runtime::{CanGc, JSContext};
30
31#[dom_struct]
32pub struct TrustedTypePolicyFactory {
33    reflector_: Reflector,
34
35    default_policy: MutNullableDom<TrustedTypePolicy>,
36    policy_names: RefCell<Vec<String>>,
37}
38
39pub(crate) static DEFAULT_SCRIPT_SINK_GROUP: &str = "'script'";
40
41// We currently always clone the result, so keep the `clone()` in the trait
42// for now to keep the caller side clean
43impl Convert<DOMString> for TrustedTypeOrString {
44    fn convert(self) -> DOMString {
45        match self {
46            TrustedTypeOrString::TrustedHTML(trusted_html) => trusted_html.data().clone(),
47            TrustedTypeOrString::TrustedScript(trusted_script) => trusted_script.data().clone(),
48            TrustedTypeOrString::TrustedScriptURL(trusted_script_url) => {
49                trusted_script_url.data().clone()
50            },
51            TrustedTypeOrString::String(str_) => str_,
52        }
53    }
54}
55
56impl TrustedTypePolicyFactory {
57    fn new_inherited() -> Self {
58        Self {
59            reflector_: Reflector::new(),
60            default_policy: Default::default(),
61            policy_names: RefCell::new(vec![]),
62        }
63    }
64
65    pub(crate) fn new(global: &GlobalScope, can_gc: CanGc) -> DomRoot<Self> {
66        reflect_dom_object(Box::new(Self::new_inherited()), global, can_gc)
67    }
68
69    /// <https://www.w3.org/TR/trusted-types/#create-trusted-type-policy-algorithm>
70    fn create_trusted_type_policy(
71        &self,
72        policy_name: String,
73        options: &TrustedTypePolicyOptions,
74        global: &GlobalScope,
75        can_gc: CanGc,
76    ) -> Fallible<DomRoot<TrustedTypePolicy>> {
77        // Avoid double borrow on policy_names
78        {
79            // Step 1: Let allowedByCSP be the result of executing Should Trusted Type policy creation be blocked by
80            // Content Security Policy? algorithm with global, policyName and factory’s created policy names value.
81            let policy_names = self.policy_names.borrow();
82            let policy_names: Vec<&str> = policy_names.iter().map(String::as_ref).collect();
83            let allowed_by_csp = global
84                .get_csp_list()
85                .is_trusted_type_policy_creation_allowed(global, &policy_name, &policy_names);
86
87            // Step 2: If allowedByCSP is "Blocked", throw a TypeError and abort further steps.
88            if !allowed_by_csp {
89                return Err(Error::Type("Not allowed by CSP".to_string()));
90            }
91        }
92
93        // Step 3: If policyName is default and the factory’s default policy value is not null, throw a TypeError
94        // and abort further steps.
95        if policy_name == "default" && self.default_policy.get().is_some() {
96            return Err(Error::Type(
97                "Already set default policy for factory".to_string(),
98            ));
99        }
100
101        // Step 4: Let policy be a new TrustedTypePolicy object.
102        // Step 5: Set policy’s name property value to policyName.
103        // Step 6: Set policy’s options value to «[ "createHTML" ->
104        // options["createHTML", "createScript" -> options["createScript",
105        // "createScriptURL" -> options["createScriptURL" ]».
106        let policy = TrustedTypePolicy::new(policy_name.clone(), options, global, can_gc);
107        // Step 7: If the policyName is default, set the factory’s default policy value to policy.
108        if policy_name == "default" {
109            self.default_policy.set(Some(&policy))
110        }
111        // Step 8: Append policyName to factory’s created policy names.
112        self.policy_names.borrow_mut().push(policy_name);
113        // Step 9: Return policy.
114        Ok(policy)
115    }
116
117    /// <https://w3c.github.io/trusted-types/dist/spec/#abstract-opdef-get-trusted-type-data-for-attribute>
118    #[expect(clippy::if_same_then_else)]
119    fn get_trusted_type_data_for_attribute(
120        element_namespace: &Namespace,
121        element_name: &LocalName,
122        attribute: &str,
123        attribute_namespace: Option<&Namespace>,
124    ) -> Option<(TrustedType, String)> {
125        // Step 1: Let data be null.
126        //
127        // We return the if directly
128        // Step 2: If attributeNs is null, « HTML namespace, SVG namespace, MathML namespace » contains
129        // element’s namespace, and attribute is the name of an event handler content attribute:
130        if attribute_namespace.is_none() &&
131            matches!(*element_namespace, ns!(html) | ns!(svg) | ns!(mathml)) &&
132            EventTarget::is_content_event_handler(attribute)
133        {
134            // Step 2.1. Return (Element, null, attribute, TrustedScript, "Element " + attribute).
135            return Some((
136                TrustedType::TrustedScript,
137                "Element ".to_owned() + attribute,
138            ));
139        }
140        // Step 3: Find the row in the following table, where element is in the first column,
141        // attributeNs is in the second column, and attribute is in the third column.
142        // If a matching row is found, set data to that row.
143        // Step 4: Return data.
144        if *element_namespace == ns!(html) &&
145            *element_name == local_name!("iframe") &&
146            attribute_namespace.is_none() &&
147            attribute == "srcdoc"
148        {
149            Some((
150                TrustedType::TrustedHTML,
151                "HTMLIFrameElement srcdoc".to_owned(),
152            ))
153        } else if *element_namespace == ns!(html) &&
154            *element_name == local_name!("script") &&
155            attribute_namespace.is_none() &&
156            attribute == "src"
157        {
158            Some((
159                TrustedType::TrustedScriptURL,
160                "HTMLScriptElement src".to_owned(),
161            ))
162        } else if *element_namespace == ns!(svg) &&
163            *element_name == local_name!("script") &&
164            attribute_namespace.is_none() &&
165            attribute == "href"
166        {
167            Some((
168                TrustedType::TrustedScriptURL,
169                "SVGScriptElement href".to_owned(),
170            ))
171        } else if *element_namespace == ns!(svg) &&
172            *element_name == local_name!("script") &&
173            attribute_namespace == Some(&ns!(xlink)) &&
174            attribute == "href"
175        {
176            Some((
177                TrustedType::TrustedScriptURL,
178                "SVGScriptElement href".to_owned(),
179            ))
180        } else {
181            None
182        }
183    }
184
185    /// <https://w3c.github.io/trusted-types/dist/spec/#validate-attribute-mutation>
186    pub(crate) fn get_trusted_types_compliant_attribute_value(
187        element_namespace: &Namespace,
188        element_name: &LocalName,
189        attribute: &str,
190        attribute_namespace: Option<&Namespace>,
191        new_value: TrustedTypeOrString,
192        global: &GlobalScope,
193        can_gc: CanGc,
194    ) -> Fallible<DOMString> {
195        // Step 1. If attributeNs is the empty string, set attributeNs to null.
196        let attribute_namespace =
197            attribute_namespace.and_then(|a| if *a == ns!() { None } else { Some(a) });
198        // Step 2. Set attributeData to the result of Get Trusted Type data for attribute algorithm,
199        // with the following arguments:
200        let Some(attribute_data) = Self::get_trusted_type_data_for_attribute(
201            element_namespace,
202            element_name,
203            attribute,
204            attribute_namespace,
205        ) else {
206            // Step 3. If attributeData is null, then:
207            // Step 3.1. If newValue is a string, return newValue.
208            // Step 3.2. Assert: newValue is TrustedHTML or TrustedScript or TrustedScriptURL.
209            // Step 3.3. Return value’s associated data.
210            return Ok(new_value.convert());
211        };
212        // Step 4. Let expectedType be the value of the fourth member of attributeData.
213        // Step 5. Let sink be the value of the fifth member of attributeData.
214        let (expected_type, sink) = attribute_data;
215        let new_value = if let TrustedTypeOrString::String(str_) = new_value {
216            str_
217        } else {
218            // If the type was already trusted, we should return immediately as
219            // all callers of `get_trusted_type_compliant_string` implement this
220            // check themselves. However, we should only do this if it matches
221            // the expected type.
222            if expected_type.matches_idl_trusted_type(&new_value) {
223                return Ok(new_value.convert());
224            }
225            new_value.convert()
226        };
227        // Step 6. Return the result of executing Get Trusted Type compliant string with the following arguments:
228        // If the algorithm threw an error, rethrow the error.
229        Self::get_trusted_type_compliant_string(
230            expected_type,
231            global,
232            new_value,
233            &sink,
234            DEFAULT_SCRIPT_SINK_GROUP,
235            can_gc,
236        )
237    }
238
239    /// <https://w3c.github.io/trusted-types/dist/spec/#process-value-with-a-default-policy-algorithm>
240    pub(crate) fn process_value_with_default_policy(
241        expected_type: TrustedType,
242        global: &GlobalScope,
243        input: DOMString,
244        sink: &str,
245        can_gc: CanGc,
246    ) -> Fallible<Option<DOMString>> {
247        // Step 1: Let defaultPolicy be the value of global’s trusted type policy factory's default policy.
248        let global_policy_factory = global.trusted_types(can_gc);
249        let default_policy = match global_policy_factory.default_policy.get() {
250            None => return Ok(None),
251            Some(default_policy) => default_policy,
252        };
253        let cx = GlobalScope::get_cx();
254        // Step 2: Let policyValue be the result of executing Get Trusted Type policy value,
255        // with the following arguments:
256        rooted!(in(*cx) let mut trusted_type_name_value = NullValue());
257        expected_type.clone().as_ref().safe_to_jsval(
258            cx,
259            trusted_type_name_value.handle_mut(),
260            can_gc,
261        );
262
263        rooted!(in(*cx) let mut sink_value = NullValue());
264        sink.safe_to_jsval(cx, sink_value.handle_mut(), can_gc);
265
266        let arguments = vec![trusted_type_name_value.handle(), sink_value.handle()];
267        let policy_value = default_policy.get_trusted_type_policy_value(
268            expected_type,
269            input,
270            arguments,
271            false,
272            can_gc,
273        );
274        let data_string = match policy_value {
275            // Step 3: If the algorithm threw an error, rethrow the error and abort the following steps.
276            Err(error) => return Err(error),
277            Ok(policy_value) => match policy_value {
278                // Step 4: If policyValue is null or undefined, return policyValue.
279                None => return Ok(None),
280                // Step 5: Let dataString be the result of stringifying policyValue.
281                Some(policy_value) => policy_value,
282            },
283        };
284        Ok(Some(data_string))
285    }
286    /// Step 1 is implemented by the caller
287    /// <https://w3c.github.io/trusted-types/dist/spec/#get-trusted-type-compliant-string-algorithm>
288    pub(crate) fn get_trusted_type_compliant_string(
289        expected_type: TrustedType,
290        global: &GlobalScope,
291        input: DOMString,
292        sink: &str,
293        sink_group: &str,
294        can_gc: CanGc,
295    ) -> Fallible<DOMString> {
296        // Step 2: Let requireTrustedTypes be the result of executing Does sink type require trusted types?
297        // algorithm, passing global, sinkGroup, and true.
298        let require_trusted_types = global
299            .get_csp_list()
300            .does_sink_type_require_trusted_types(sink_group, true);
301        // Step 3: If requireTrustedTypes is false, return stringified input and abort these steps.
302        if !require_trusted_types {
303            return Ok(input);
304        }
305        // Step 4: Let convertedInput be the result of executing Process value with a default policy
306        // with the same arguments as this algorithm.
307        let converted_input = TrustedTypePolicyFactory::process_value_with_default_policy(
308            expected_type,
309            global,
310            input.clone(),
311            sink,
312            can_gc,
313        );
314        // Step 5: If the algorithm threw an error, rethrow the error and abort the following steps.
315        match converted_input? {
316            // Step 6: If convertedInput is null or undefined, execute the following steps:
317            None => {
318                // Step 6.1: Let disposition be the result of executing Should sink type mismatch violation
319                // be blocked by Content Security Policy? algorithm, passing global,
320                // stringified input as source, sinkGroup and sink.
321                let is_blocked = global
322                    .get_csp_list()
323                    .should_sink_type_mismatch_violation_be_blocked_by_csp(
324                        global,
325                        sink,
326                        sink_group,
327                        &input.str(),
328                    );
329                // Step 6.2: If disposition is “Allowed”, return stringified input and abort further steps.
330                if !is_blocked {
331                    Ok(input)
332                } else {
333                    // Step 6.3: Throw a TypeError and abort further steps.
334                    Err(Error::Type(
335                        "Cannot set value, expected trusted type".to_owned(),
336                    ))
337                }
338            },
339            // Step 8: Return stringified convertedInput.
340            Some(converted_input) => Ok(converted_input),
341        }
342        // Step 7: Assert: convertedInput is an instance of expectedType.
343        // TODO(https://github.com/w3c/trusted-types/issues/566): Implement when spec is resolved
344    }
345
346    /// <https://www.w3.org/TR/trusted-types/#dom-trustedtypepolicyfactory-isscript>
347    pub(crate) fn is_trusted_script(
348        cx: JSContext,
349        value: HandleValue,
350    ) -> Result<DomRoot<TrustedScript>, ()> {
351        root_from_handlevalue::<TrustedScript>(value, cx)
352    }
353}
354
355impl TrustedTypePolicyFactoryMethods<crate::DomTypeHolder> for TrustedTypePolicyFactory {
356    /// <https://www.w3.org/TR/trusted-types/#dom-trustedtypepolicyfactory-createpolicy>
357    fn CreatePolicy(
358        &self,
359        policy_name: DOMString,
360        options: &TrustedTypePolicyOptions,
361        can_gc: CanGc,
362    ) -> Fallible<DomRoot<TrustedTypePolicy>> {
363        self.create_trusted_type_policy(policy_name.to_string(), options, &self.global(), can_gc)
364    }
365    /// <https://www.w3.org/TR/trusted-types/#dom-trustedtypepolicyfactory-ishtml>
366    fn IsHTML(&self, cx: JSContext, value: HandleValue) -> bool {
367        root_from_handlevalue::<TrustedHTML>(value, cx).is_ok()
368    }
369    /// <https://www.w3.org/TR/trusted-types/#dom-trustedtypepolicyfactory-isscript>
370    fn IsScript(&self, cx: JSContext, value: HandleValue) -> bool {
371        TrustedTypePolicyFactory::is_trusted_script(cx, value).is_ok()
372    }
373    /// <https://www.w3.org/TR/trusted-types/#dom-trustedtypepolicyfactory-isscripturl>
374    fn IsScriptURL(&self, cx: JSContext, value: HandleValue) -> bool {
375        root_from_handlevalue::<TrustedScriptURL>(value, cx).is_ok()
376    }
377    /// <https://www.w3.org/TR/trusted-types/#dom-trustedtypepolicyfactory-emptyhtml>
378    fn EmptyHTML(&self, can_gc: CanGc) -> DomRoot<TrustedHTML> {
379        TrustedHTML::new(DOMString::new(), &self.global(), can_gc)
380    }
381    /// <https://www.w3.org/TR/trusted-types/#dom-trustedtypepolicyfactory-emptyscript>
382    fn EmptyScript(&self, can_gc: CanGc) -> DomRoot<TrustedScript> {
383        TrustedScript::new(DOMString::new(), &self.global(), can_gc)
384    }
385    /// <https://www.w3.org/TR/trusted-types/#dom-trustedtypepolicyfactory-getattributetype>
386    fn GetAttributeType(
387        &self,
388        tag_name: DOMString,
389        attribute: DOMString,
390        element_namespace: Option<DOMString>,
391        attribute_namespace: Option<DOMString>,
392    ) -> Option<DOMString> {
393        // Step 1: Set localName to tagName in ASCII lowercase.
394        let local_name = tag_name.to_ascii_lowercase();
395        // Step 2: Set attribute to attribute in ASCII lowercase.
396        let attribute = attribute.to_ascii_lowercase();
397        // Step 3: If elementNs is null or an empty string, set elementNs to HTML namespace.
398        let element_namespace = match element_namespace {
399            Some(namespace) if !namespace.is_empty() => Namespace::from(namespace),
400            Some(_) | None => ns!(html),
401        };
402        // Step 4: If attrNs is an empty string, set attrNs to null.
403        let attribute_namespace = match attribute_namespace {
404            Some(namespace) if !namespace.is_empty() => Some(Namespace::from(namespace)),
405            Some(_) | None => None,
406        };
407        // Step 5: Let interface be the element interface for localName and elementNs.
408        // Step 6: Let expectedType be null.
409        // Step 7: Set attributeData to the result of Get Trusted Type data for attribute algorithm,
410        // with the following arguments: interface as element, attribute, attrNs
411        // Step 8: If attributeData is not null, then set expectedType to the interface’s name of
412        // the value of the fourth member of attributeData.
413        // Step 9: Return expectedType.
414        TrustedTypePolicyFactory::get_trusted_type_data_for_attribute(
415            &element_namespace,
416            &LocalName::from(local_name),
417            &attribute,
418            attribute_namespace.as_ref(),
419        )
420        .map(|tuple| DOMString::from(tuple.0.as_ref()))
421    }
422    /// <https://www.w3.org/TR/trusted-types/#dom-trustedtypepolicyfactory-getpropertytype>
423    #[expect(clippy::if_same_then_else)]
424    fn GetPropertyType(
425        &self,
426        tag_name: DOMString,
427        property: DOMString,
428        element_namespace: Option<DOMString>,
429    ) -> Option<DOMString> {
430        // Step 1: Set localName to tagName in ASCII lowercase.
431        let local_name = tag_name.to_ascii_lowercase();
432        // Step 2: If elementNs is null or an empty string, set elementNs to HTML namespace.
433        let element_namespace = match element_namespace {
434            Some(namespace) if !namespace.is_empty() => Namespace::from(namespace),
435            Some(_) | None => ns!(html),
436        };
437        // Step 3: Let interface be the element interface for localName and elementNs.
438        let interface = QualName::new(None, element_namespace, LocalName::from(local_name));
439        // Step 4: Let expectedType be null.
440        let mut expected_type = None;
441        // Step 5: Find the row in the following table, where the first column is "*" or interface’s name,
442        // and property is in the second column. If a matching row is found, set expectedType to
443        // the interface’s name of the value of the third column.
444        let property = property.str();
445        if interface.ns == ns!(html) &&
446            interface.local == local_name!("iframe") &&
447            property == "srcdoc"
448        {
449            expected_type = Some(DOMString::from("TrustedHTML"))
450        } else if interface.ns == ns!(html) &&
451            interface.local == local_name!("script") &&
452            property == "innerText"
453        {
454            expected_type = Some(DOMString::from("TrustedScript"))
455        } else if interface.ns == ns!(html) &&
456            interface.local == local_name!("script") &&
457            property == "src"
458        {
459            expected_type = Some(DOMString::from("TrustedScriptURL"))
460        } else if interface.ns == ns!(html) &&
461            interface.local == local_name!("script") &&
462            property == "text"
463        {
464            expected_type = Some(DOMString::from("TrustedScript"))
465        } else if interface.ns == ns!(html) &&
466            interface.local == local_name!("script") &&
467            property == "textContent"
468        {
469            expected_type = Some(DOMString::from("TrustedScript"))
470        } else if property == "innerHTML" {
471            expected_type = Some(DOMString::from("TrustedHTML"))
472        } else if property == "outerHTML" {
473            expected_type = Some(DOMString::from("TrustedHTML"))
474        }
475        // Step 6: Return expectedType.
476        expected_type
477    }
478    /// <https://www.w3.org/TR/trusted-types/#dom-trustedtypepolicyfactory-defaultpolicy>
479    fn GetDefaultPolicy(&self) -> Option<DomRoot<TrustedTypePolicy>> {
480        self.default_policy.get()
481    }
482}