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