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