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