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