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