script/dom/
clipboarditem.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::ops::Deref;
6use std::rc::Rc;
7use std::str::FromStr;
8
9use constellation_traits::BlobImpl;
10use data_url::mime::Mime;
11use dom_struct::dom_struct;
12use js::rust::{HandleObject, HandleValue as SafeHandleValue, MutableHandleValue};
13use script_bindings::record::Record;
14
15use crate::dom::bindings::cell::DomRefCell;
16use crate::dom::bindings::codegen::Bindings::ClipboardBinding::{
17    ClipboardItemMethods, ClipboardItemOptions, PresentationStyle,
18};
19use crate::dom::bindings::conversions::{
20    ConversionResult, SafeFromJSValConvertible, StringificationBehavior,
21};
22use crate::dom::bindings::error::{Error, Fallible};
23use crate::dom::bindings::frozenarray::CachedFrozenArray;
24use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object_with_proto};
25use crate::dom::bindings::root::DomRoot;
26use crate::dom::bindings::str::DOMString;
27use crate::dom::blob::Blob;
28use crate::dom::promise::Promise;
29use crate::dom::promisenativehandler::{Callback, PromiseNativeHandler};
30use crate::dom::window::Window;
31use crate::realms::{InRealm, enter_realm};
32use crate::script_runtime::{CanGc, JSContext as SafeJSContext};
33
34/// The fulfillment handler for the reacting to representationDataPromise part of
35/// <https://w3c.github.io/clipboard-apis/#dom-clipboarditem-gettype>.
36#[derive(Clone, JSTraceable, MallocSizeOf)]
37struct RepresentationDataPromiseFulfillmentHandler {
38    #[conditional_malloc_size_of]
39    promise: Rc<Promise>,
40    type_: String,
41}
42
43impl Callback for RepresentationDataPromiseFulfillmentHandler {
44    /// Substeps of 8.1.2.1 If representationDataPromise was fulfilled with value v, then:
45    fn callback(&self, cx: SafeJSContext, v: SafeHandleValue, _realm: InRealm, can_gc: CanGc) {
46        // 1. If v is a DOMString, then follow the below steps:
47        if v.get().is_string() {
48            // 1.1 Let dataAsBytes be the result of UTF-8 encoding v.
49            let data_as_bytes =
50                match DOMString::safe_from_jsval(cx, v, StringificationBehavior::Default) {
51                    Ok(ConversionResult::Success(s)) => s.as_bytes().to_owned(),
52                    _ => return,
53                };
54
55            // 1.2 Let blobData be a Blob created using dataAsBytes with its type set to mimeType, serialized.
56            let blob_data = Blob::new(
57                &self.promise.global(),
58                BlobImpl::new_from_bytes(data_as_bytes, self.type_.clone()),
59                can_gc,
60            );
61
62            // 1.3 Resolve p with blobData.
63            self.promise.resolve_native(&blob_data, can_gc);
64        }
65        // 2. If v is a Blob, then follow the below steps:
66        else if DomRoot::<Blob>::safe_from_jsval(cx, v, ())
67            .is_ok_and(|result| result.get_success_value().is_some())
68        {
69            // 2.1 Resolve p with v.
70            self.promise.resolve(cx, v, can_gc);
71        }
72    }
73}
74
75/// The rejection handler for the reacting to representationDataPromise part of
76/// <https://w3c.github.io/clipboard-apis/#dom-clipboarditem-gettype>.
77#[derive(Clone, JSTraceable, MallocSizeOf)]
78struct RepresentationDataPromiseRejectionHandler {
79    #[conditional_malloc_size_of]
80    promise: Rc<Promise>,
81}
82
83impl Callback for RepresentationDataPromiseRejectionHandler {
84    /// Substeps of 8.1.2.2 If representationDataPromise was rejected, then:
85    fn callback(&self, _cx: SafeJSContext, _v: SafeHandleValue, _realm: InRealm, can_gc: CanGc) {
86        // 1. Reject p with "NotFoundError" DOMException in realm.
87        self.promise.reject_error(Error::NotFound(None), can_gc);
88    }
89}
90
91/// <https://w3c.github.io/clipboard-apis/#web-custom-format>
92const CUSTOM_FORMAT_PREFIX: &str = "web ";
93
94/// <https://w3c.github.io/clipboard-apis/#representation>
95#[derive(JSTraceable, MallocSizeOf)]
96pub(super) struct Representation {
97    #[no_trace]
98    #[ignore_malloc_size_of = "Extern type"]
99    pub mime_type: Mime,
100    pub is_custom: bool,
101    #[conditional_malloc_size_of]
102    pub data: Rc<Promise>,
103}
104
105#[dom_struct]
106pub(crate) struct ClipboardItem {
107    reflector_: Reflector,
108    representations: DomRefCell<Vec<Representation>>,
109    presentation_style: DomRefCell<PresentationStyle>,
110    #[ignore_malloc_size_of = "mozjs"]
111    frozen_types: CachedFrozenArray,
112}
113
114impl ClipboardItem {
115    fn new_inherited() -> ClipboardItem {
116        ClipboardItem {
117            reflector_: Reflector::new(),
118            representations: Default::default(),
119            presentation_style: Default::default(),
120            frozen_types: CachedFrozenArray::new(),
121        }
122    }
123
124    fn new(window: &Window, proto: Option<HandleObject>, can_gc: CanGc) -> DomRoot<ClipboardItem> {
125        reflect_dom_object_with_proto(
126            Box::new(ClipboardItem::new_inherited()),
127            window,
128            proto,
129            can_gc,
130        )
131    }
132}
133
134impl ClipboardItemMethods<crate::DomTypeHolder> for ClipboardItem {
135    /// <https://w3c.github.io/clipboard-apis/#dom-clipboarditem-clipboarditem>
136    fn Constructor(
137        global: &Window,
138        proto: Option<HandleObject>,
139        can_gc: CanGc,
140        items: Record<DOMString, Rc<Promise>>,
141        options: &ClipboardItemOptions,
142    ) -> Fallible<DomRoot<ClipboardItem>> {
143        // Step 1 If items is empty, then throw a TypeError.
144        if items.is_empty() {
145            return Err(Error::Type(String::from("No item provided")));
146        }
147
148        // Step 2 If options is empty, then set options["presentationStyle"] = "unspecified".
149        // NOTE: This is done inside bindings
150
151        // Step 3 Set this's clipboard item to a new clipboard item.
152        let clipboard_item = ClipboardItem::new(global, proto, can_gc);
153
154        // Step 4 Set this's clipboard item's presentation style to options["presentationStyle"].
155        *clipboard_item.presentation_style.borrow_mut() = options.presentationStyle;
156
157        // Step 6 For each (key, value) in items:
158        for (key, value) in items.deref() {
159            // Step 6.2 Let isCustom be false.
160
161            // Step 6.3 If key starts with `"web "` prefix, then
162            // Step 6.3.1 Remove `"web "` prefix and assign the remaining string to key.
163            let key = key.str();
164            let (key, is_custom) = match key.strip_prefix(CUSTOM_FORMAT_PREFIX) {
165                None => (&*key, false),
166                // Step 6.3.2 Set isCustom true
167                Some(stripped) => (stripped, true),
168            };
169
170            // Step 6.5 Let mimeType be the result of parsing a MIME type given key.
171            // Step 6.6 If mimeType is failure, then throw a TypeError.
172            let mime_type =
173                Mime::from_str(key).map_err(|_| Error::Type(String::from("Invalid mime type")))?;
174
175            // Step 6.7 If this's clipboard item's list of representations contains a representation
176            // whose MIME type is mimeType and whose [representation/isCustom] is isCustom, then throw a TypeError.
177            if clipboard_item
178                .representations
179                .borrow()
180                .iter()
181                .any(|representation| {
182                    representation.mime_type == mime_type && representation.is_custom == is_custom
183                })
184            {
185                return Err(Error::Type(String::from("Tried to add a duplicate mime")));
186            }
187
188            // Step 6.1 Let representation be a new representation.
189            // Step 6.4 Set representation’s isCustom flag to isCustom.
190            // Step 6.8 Set representation’s MIME type to mimeType.
191            // Step 6.9 Set representation’s data to value.
192            let representation = Representation {
193                mime_type,
194                is_custom,
195                data: value.clone(),
196            };
197
198            // Step 6.10 Append representation to this's clipboard item's list of representations.
199            clipboard_item
200                .representations
201                .borrow_mut()
202                .push(representation);
203        }
204
205        // NOTE: The steps for creating a frozen array from the list of mimeType are done in the Types() method
206
207        Ok(clipboard_item)
208    }
209
210    /// <https://w3c.github.io/clipboard-apis/#dom-clipboarditem-presentationstyle>
211    fn PresentationStyle(&self) -> PresentationStyle {
212        *self.presentation_style.borrow()
213    }
214
215    /// <https://w3c.github.io/clipboard-apis/#dom-clipboarditem-types>
216    fn Types(&self, cx: SafeJSContext, can_gc: CanGc, retval: MutableHandleValue) {
217        self.frozen_types.get_or_init(
218            || {
219                // Step 5 Let types be a list of DOMString.
220                let mut types = Vec::new();
221
222                self.representations
223                    .borrow()
224                    .iter()
225                    .for_each(|representation| {
226                        // Step 6.11 Let mimeTypeString be the result of serializing a MIME type with mimeType.
227                        let mime_type_string = representation.mime_type.to_string();
228
229                        // Step 6.12 If isCustom is true, prefix mimeTypeString with `"web "`.
230                        let mime_type_string = if representation.is_custom {
231                            format!("{}{}", CUSTOM_FORMAT_PREFIX, mime_type_string)
232                        } else {
233                            mime_type_string
234                        };
235
236                        // Step 6.13 Add mimeTypeString to types.
237                        types.push(DOMString::from(mime_type_string));
238                    });
239                types
240            },
241            cx,
242            retval,
243            can_gc,
244        );
245    }
246
247    /// <https://w3c.github.io/clipboard-apis/#dom-clipboarditem-gettype>
248    fn GetType(&self, type_: DOMString, can_gc: CanGc) -> Fallible<Rc<Promise>> {
249        // Step 1 Let realm be this’s relevant realm.
250        let global = self.global();
251
252        // Step 2 Let isCustom be false.
253
254        // Step 3 If type starts with `"web "` prefix, then:
255        // Step 3.1 Remove `"web "` prefix and assign the remaining string to type.
256        let type_ = type_.str();
257        let (type_, is_custom) = match type_.strip_prefix(CUSTOM_FORMAT_PREFIX) {
258            None => (&*type_, false),
259            // Step 3.2 Set isCustom to true.
260            Some(stripped) => (stripped, true),
261        };
262
263        // Step 4 Let mimeType be the result of parsing a MIME type given type.
264        // Step 5 If mimeType is failure, then throw a TypeError.
265        let mime_type =
266            Mime::from_str(type_).map_err(|_| Error::Type(String::from("Invalid mime type")))?;
267
268        // Step 6 Let itemTypeList be this’s clipboard item’s list of representations.
269        let item_type_list = self.representations.borrow();
270
271        // Step 7 Let p be a new promise in realm.
272        let p = Promise::new(&global, can_gc);
273
274        // Step 8 For each representation in itemTypeList
275        for representation in item_type_list.iter() {
276            // Step 8.1 If representation’s MIME type is mimeType and representation’s isCustom is isCustom, then:
277            if representation.mime_type == mime_type && representation.is_custom == is_custom {
278                // Step 8.1.1 Let representationDataPromise be the representation’s data.
279                let representation_data_promise = &representation.data;
280
281                // Step 8.1.2 React to representationDataPromise:
282                let fulfillment_handler = Box::new(RepresentationDataPromiseFulfillmentHandler {
283                    promise: p.clone(),
284                    type_: representation.mime_type.to_string(),
285                });
286                let rejection_handler =
287                    Box::new(RepresentationDataPromiseRejectionHandler { promise: p.clone() });
288                let handler = PromiseNativeHandler::new(
289                    &global,
290                    Some(fulfillment_handler),
291                    Some(rejection_handler),
292                    can_gc,
293                );
294                let realm = enter_realm(&*global);
295                let comp = InRealm::Entered(&realm);
296                representation_data_promise.append_native_handler(&handler, comp, can_gc);
297
298                // Step 8.1.3 Return p.
299                return Ok(p);
300            }
301        }
302
303        // Step 9 Reject p with "NotFoundError" DOMException in realm.
304        p.reject_error(Error::NotFound(None), can_gc);
305
306        // Step 10 Return p.
307        Ok(p)
308    }
309
310    /// <https://w3c.github.io/clipboard-apis/#dom-clipboarditem-supports>
311    fn Supports(_: &Window, type_: DOMString) -> bool {
312        // TODO Step 1 If type is in mandatory data types or optional data types, then return true.
313        // Step 2 If not, then return false.
314        // NOTE: We only supports text/plain
315        type_ == "text/plain"
316    }
317}