script/dom/clipboard/
clipboard.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::rc::Rc;
6use std::str::FromStr;
7
8use data_url::mime::Mime;
9use dom_struct::dom_struct;
10use embedder_traits::EmbedderMsg;
11use js::context::JSContext;
12use js::realm::CurrentRealm;
13use js::rust::HandleValue as SafeHandleValue;
14use servo_constellation_traits::BlobImpl;
15
16use super::clipboarditem::Representation;
17use crate::dom::bindings::codegen::Bindings::ClipboardBinding::{
18    ClipboardMethods, PresentationStyle,
19};
20use crate::dom::bindings::error::Error;
21use crate::dom::bindings::refcounted::TrustedPromise;
22use crate::dom::bindings::reflector::{DomGlobal, reflect_dom_object_with_cx};
23use crate::dom::bindings::root::DomRoot;
24use crate::dom::bindings::str::DOMString;
25use crate::dom::blob::Blob;
26use crate::dom::eventtarget::EventTarget;
27use crate::dom::globalscope::GlobalScope;
28use crate::dom::promise::Promise;
29use crate::dom::promisenativehandler::{Callback, PromiseNativeHandler};
30use crate::dom::window::Window;
31use crate::realms::{InRealm, enter_auto_realm};
32use crate::routed_promise::{RoutedPromiseListener, callback_promise};
33use crate::script_runtime::CanGc;
34
35/// The fulfillment handler for the reacting to representationDataPromise part of
36/// <https://w3c.github.io/clipboard-apis/#dom-clipboard-readtext>.
37#[derive(Clone, JSTraceable, MallocSizeOf)]
38struct RepresentationDataPromiseFulfillmentHandler {
39    #[conditional_malloc_size_of]
40    promise: Rc<Promise>,
41}
42
43impl Callback for RepresentationDataPromiseFulfillmentHandler {
44    /// The fulfillment case of Step 3.4.1.1.4.3 of
45    /// <https://w3c.github.io/clipboard-apis/#dom-clipboard-readtext>.
46    fn callback(&self, cx: &mut CurrentRealm, v: SafeHandleValue) {
47        // If v is a DOMString, then follow the below steps:
48        // Resolve p with v.
49        // Return p.
50        self.promise.resolve(cx.into(), v, CanGc::from_cx(cx));
51
52        // NOTE: Since we ask text from arboard, v can't be a Blob
53        // If v is a Blob, then follow the below steps:
54        // Let string be the result of UTF-8 decoding v’s underlying byte sequence.
55        // Resolve p with string.
56        // Return p.
57    }
58}
59
60/// The rejection handler for the reacting to representationDataPromise part of
61/// <https://w3c.github.io/clipboard-apis/#dom-clipboard-readtext>.
62#[derive(Clone, JSTraceable, MallocSizeOf)]
63struct RepresentationDataPromiseRejectionHandler {
64    #[conditional_malloc_size_of]
65    promise: Rc<Promise>,
66}
67
68impl Callback for RepresentationDataPromiseRejectionHandler {
69    /// The rejection case of Step 3.4.1.1.4.3 of
70    /// <https://w3c.github.io/clipboard-apis/#dom-clipboard-readtext>.
71    fn callback(&self, cx: &mut CurrentRealm, _v: SafeHandleValue) {
72        // Reject p with "NotFoundError" DOMException in realm.
73        // Return p.
74        self.promise
75            .reject_error(Error::NotFound(None), CanGc::from_cx(cx));
76    }
77}
78
79#[dom_struct]
80pub(crate) struct Clipboard {
81    event_target: EventTarget,
82}
83
84impl Clipboard {
85    fn new_inherited() -> Clipboard {
86        Clipboard {
87            event_target: EventTarget::new_inherited(),
88        }
89    }
90
91    pub(crate) fn new(cx: &mut JSContext, global: &GlobalScope) -> DomRoot<Clipboard> {
92        reflect_dom_object_with_cx(Box::new(Clipboard::new_inherited()), global, cx)
93    }
94}
95
96impl ClipboardMethods<crate::DomTypeHolder> for Clipboard {
97    /// <https://w3c.github.io/clipboard-apis/#dom-clipboard-readtext>
98    fn ReadText(&self, realm: &mut CurrentRealm) -> Rc<Promise> {
99        // Step 1 Let realm be this's relevant realm.
100        let global = self.global();
101
102        // Step 2 Let p be a new promise in realm.
103        let p = Promise::new_in_realm(realm);
104
105        // Step 3 Run the following steps in parallel:
106
107        // TODO Step 3.1 Let r be the result of running check clipboard read permission.
108        // Step 3.2 If r is false, then:
109        // Step 3.2.1 Queue a global task on the permission task source, given realm’s global object,
110        // to reject p with "NotAllowedError" DOMException in realm.
111        // Step 3.2.2 Abort these steps.
112
113        // Step 3.3 Let data be a copy of the system clipboard data.
114        let window = global.as_window();
115        let callback = callback_promise(&p, self, global.task_manager().clipboard_task_source());
116        window.send_to_embedder(EmbedderMsg::GetClipboardText(window.webview_id(), callback));
117
118        // Step 3.4 Queue a global task on the clipboard task source,
119        // given realm’s global object, to perform the below steps:
120        // NOTE: We queue the task inside route_promise and perform the steps inside handle_response
121
122        p
123    }
124
125    /// <https://w3c.github.io/clipboard-apis/#dom-clipboard-writetext>
126    fn WriteText(&self, realm: &mut CurrentRealm, data: DOMString) -> Rc<Promise> {
127        // Step 1 Let realm be this's relevant realm.
128        let global = self.global();
129        // Step 2 Let p be a new promise in realm.
130        let p = Promise::new_in_realm(realm);
131
132        // Step 3 Run the following steps in parallel:
133
134        // TODO write permission could be removed from spec
135        // Step 3.1 Let r be the result of running check clipboard write permission.
136        // Step 3.2 If r is false, then:
137        // Step 3.2.1 Queue a global task on the permission task source, given realm’s global object,
138        // to reject p with "NotAllowedError" DOMException in realm.
139        // Step 3.2.2 Abort these steps.
140
141        let trusted_promise = TrustedPromise::new(p.clone());
142        let bytes = Vec::from(data);
143
144        // Step 3.3 Queue a global task on the clipboard task source,
145        // given realm’s global object, to perform the below steps:
146        global.task_manager().clipboard_task_source().queue(
147            task!(write_to_system_clipboard: move |cx| {
148                let promise = trusted_promise.root();
149                let global = promise.global();
150
151                // Step 3.3.1 Let itemList be an empty sequence<Blob>.
152                let mut item_list = Vec::new();
153
154                // Step 3.3.2 Let textBlob be a new Blob created with: type attribute set to "text/plain;charset=utf-8",
155                // and its underlying byte sequence set to the UTF-8 encoding of data.
156                let text_blob = Blob::new(
157                    &global,
158                    BlobImpl::new_from_bytes(bytes, "text/plain;charset=utf-8".into()),
159                    CanGc::from_cx(cx),
160                );
161
162                // Step 3.3.3 Add textBlob to itemList.
163                item_list.push(text_blob);
164
165                // Step 3.3.4 Let option be set to "unspecified".
166                let option = PresentationStyle::Unspecified;
167
168                // Step 3.3.5 Write blobs and option to the clipboard with itemList and option.
169                write_blobs_and_option_to_the_clipboard(global.as_window(), item_list, option);
170
171                // Step 3.3.6 Resolve p.
172                promise.resolve_native(&(), CanGc::from_cx(cx));
173            }),
174        );
175
176        // Step 3.4 Return p.
177        p
178    }
179}
180
181impl RoutedPromiseListener<Result<String, String>> for Clipboard {
182    fn handle_response(
183        &self,
184        cx: &mut js::context::JSContext,
185        response: Result<String, String>,
186        promise: &Rc<Promise>,
187    ) {
188        let global = self.global();
189        let text = response.unwrap_or_default();
190
191        // Step 3.4.1 For each systemClipboardItem in data:
192        // Step 3.4.1.1 For each systemClipboardRepresentation in systemClipboardItem:
193        // TODO: Arboard provide the first item that has a String representation
194
195        // Step 3.4.1.1.1 Let mimeType be the result of running the
196        // well-known mime type from os specific format algorithm given systemClipboardRepresentation’s name.
197        // Note: This is done by arboard, so we just convert the format to a MIME
198        let mime_type = Mime::from_str("text/plain").unwrap();
199
200        // Step 3.4.1.1.2 If mimeType is null, continue this loop.
201        // Note: Since the previous step is infallible, we don't need to handle this case
202
203        // Step 3.4.1.1.3 Let representation be a new representation.
204        let representation = Representation {
205            mime_type,
206            is_custom: false,
207            data: Promise::new_resolved(
208                &global,
209                GlobalScope::get_cx(),
210                DOMString::from(text),
211                CanGc::from_cx(cx),
212            ),
213        };
214
215        // Step 3.4.1.1.4 If representation’s MIME type essence is "text/plain", then:
216
217        // Step 3.4.1.1.4.1 Set representation’s MIME type to mimeType.
218        // Note: Done when creating a new representation
219
220        // Step 3.4.1.1.4.2 Let representationDataPromise be the representation’s data.
221        // Step 3.4.1.1.4.3 React to representationDataPromise:
222        let fulfillment_handler = Box::new(RepresentationDataPromiseFulfillmentHandler {
223            promise: promise.clone(),
224        });
225        let rejection_handler = Box::new(RepresentationDataPromiseRejectionHandler {
226            promise: promise.clone(),
227        });
228        let handler = PromiseNativeHandler::new(
229            &global,
230            Some(fulfillment_handler),
231            Some(rejection_handler),
232            CanGc::from_cx(cx),
233        );
234        let mut realm = enter_auto_realm(cx, &*global);
235        let cx = &mut realm.current_realm();
236        let in_realm_proof = cx.into();
237        let comp = InRealm::Already(&in_realm_proof);
238        representation
239            .data
240            .append_native_handler(&handler, comp, CanGc::from_cx(cx));
241
242        // Step 3.4.2 Reject p with "NotFoundError" DOMException in realm.
243        // Step 3.4.3 Return p.
244        // NOTE: We follow the same behaviour of Gecko by doing nothing if no text is available instead of rejecting p
245    }
246}
247
248/// <https://w3c.github.io/clipboard-apis/#write-blobs-and-option-to-the-clipboard>
249fn write_blobs_and_option_to_the_clipboard(
250    window: &Window,
251    items: Vec<DomRoot<Blob>>,
252    _presentation_style: PresentationStyle,
253) {
254    // TODO Step 1 Let webCustomFormats be a sequence<Blob>.
255
256    // Step 2 For each item in items:
257    for item in items {
258        // TODO support more formats than just text/plain
259        // Step 2.1 Let formatString be the result of running os specific well-known format given item’s type.
260
261        // Step 2.2 If formatString is empty then follow the below steps:
262
263        // Step 2.2.1 Let webCustomFormatString be the item’s type.
264        // Step 2.2.2 Let webCustomFormat be an empty type.
265        // Step 2.2.3 If webCustomFormatString starts with "web " prefix,
266        // then remove the "web " prefix and store the remaining string in webMimeTypeString.
267        // Step 2.2.4 Let webMimeType be the result of parsing a MIME type given webMimeTypeString.
268        // Step 2.2.5 If webMimeType is failure, then abort all steps.
269        // Step 2.2.6 Let webCustomFormat’s type's essence equal to webMimeType.
270        // Step 2.2.7 Set item’s type to webCustomFormat.
271        // Step 2.2.8 Append webCustomFormat to webCustomFormats.
272
273        // Step 2.3 Let payload be the result of UTF-8 decoding item’s underlying byte sequence.
274        // Step 2.4 Insert payload and presentationStyle into the system clipboard
275        // using formatString as the native clipboard format.
276        window.send_to_embedder(EmbedderMsg::SetClipboardText(
277            window.webview_id(),
278            String::from_utf8(
279                item.get_bytes()
280                    .expect("No bytes found for Blob created by caller"),
281            )
282            .expect("DOMString contained invalid bytes"),
283        ));
284    }
285
286    // TODO Step 3 Write web custom formats given webCustomFormats.
287    // Needs support to arbitrary formats inside arboard
288}