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