Skip to main content

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