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