servo/components/script/dom/clipboard.rs
Gae24 5b913441d4
async clipboard: implement readText (#36689)
part of #36084
Testing: Only idl harness tests will pass for now

---------

Signed-off-by: Gae24 <96017547+Gae24@users.noreply.github.com>
2025-05-07 15:11:37 +00:00

282 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::rc::Rc;
use std::str::FromStr;
use constellation_traits::BlobImpl;
use data_url::mime::Mime;
use dom_struct::dom_struct;
use embedder_traits::EmbedderMsg;
use js::rust::HandleValue as SafeHandleValue;
use crate::dom::bindings::codegen::Bindings::ClipboardBinding::{
ClipboardMethods, PresentationStyle,
};
use crate::dom::bindings::error::Error;
use crate::dom::bindings::refcounted::TrustedPromise;
use crate::dom::bindings::reflector::{DomGlobal, reflect_dom_object};
use crate::dom::bindings::root::DomRoot;
use crate::dom::bindings::str::DOMString;
use crate::dom::blob::Blob;
use crate::dom::clipboarditem::Representation;
use crate::dom::eventtarget::EventTarget;
use crate::dom::globalscope::GlobalScope;
use crate::dom::promise::Promise;
use crate::dom::promisenativehandler::{Callback, PromiseNativeHandler};
use crate::dom::window::Window;
use crate::realms::{InRealm, enter_realm};
use crate::routed_promise::{RoutedPromiseListener, route_promise};
use crate::script_runtime::{CanGc, JSContext as SafeJSContext};
/// The fulfillment handler for the reacting to representationDataPromise part of
/// <https://w3c.github.io/clipboard-apis/#dom-clipboard-readtext>.
#[derive(Clone, JSTraceable, MallocSizeOf)]
struct RepresentationDataPromiseFulfillmentHandler {
#[ignore_malloc_size_of = "Rc are hard"]
promise: Rc<Promise>,
}
impl Callback for RepresentationDataPromiseFulfillmentHandler {
/// The fulfillment case of Step 3.4.1.1.4.3 of
/// <https://w3c.github.io/clipboard-apis/#dom-clipboard-readtext>.
fn callback(&self, cx: SafeJSContext, v: SafeHandleValue, _realm: InRealm, can_gc: CanGc) {
// If v is a DOMString, then follow the below steps:
// Resolve p with v.
// Return p.
self.promise.resolve(cx, v, can_gc);
// NOTE: Since we ask text from arboard, v can't be a Blob
// If v is a Blob, then follow the below steps:
// Let string be the result of UTF-8 decoding vs underlying byte sequence.
// Resolve p with string.
// Return p.
}
}
/// The rejection handler for the reacting to representationDataPromise part of
/// <https://w3c.github.io/clipboard-apis/#dom-clipboard-readtext>.
#[derive(Clone, JSTraceable, MallocSizeOf)]
struct RepresentationDataPromiseRejectionHandler {
#[ignore_malloc_size_of = "Rc are hard"]
promise: Rc<Promise>,
}
impl Callback for RepresentationDataPromiseRejectionHandler {
/// The rejection case of Step 3.4.1.1.4.3 of
/// <https://w3c.github.io/clipboard-apis/#dom-clipboard-readtext>.
fn callback(&self, _cx: SafeJSContext, _v: SafeHandleValue, _realm: InRealm, can_gc: CanGc) {
// Reject p with "NotFoundError" DOMException in realm.
// Return p.
self.promise.reject_error(Error::NotFound, can_gc);
}
}
#[dom_struct]
pub(crate) struct Clipboard {
event_target: EventTarget,
}
impl Clipboard {
fn new_inherited() -> Clipboard {
Clipboard {
event_target: EventTarget::new_inherited(),
}
}
pub(crate) fn new(global: &GlobalScope, can_gc: CanGc) -> DomRoot<Clipboard> {
reflect_dom_object(Box::new(Clipboard::new_inherited()), global, can_gc)
}
}
impl ClipboardMethods<crate::DomTypeHolder> for Clipboard {
/// <https://w3c.github.io/clipboard-apis/#dom-clipboard-readtext>
fn ReadText(&self, can_gc: CanGc) -> Rc<Promise> {
// Step 1 Let realm be this's relevant realm.
let global = self.global();
// Step 2 Let p be a new promise in realm.
let p = Promise::new(&global, can_gc);
// Step 3 Run the following steps in parallel:
// TODO Step 3.1 Let r be the result of running check clipboard read permission.
// Step 3.2 If r is false, then:
// Step 3.2.1 Queue a global task on the permission task source, given realms global object,
// to reject p with "NotAllowedError" DOMException in realm.
// Step 3.2.2 Abort these steps.
// Step 3.3 Let data be a copy of the system clipboard data.
let window = global.as_window();
let sender = route_promise(&p, self, global.task_manager().clipboard_task_source());
window.send_to_embedder(EmbedderMsg::GetClipboardText(window.webview_id(), sender));
// Step 3.4 Queue a global task on the clipboard task source,
// given realms global object, to perform the below steps:
// NOTE: We queue the task inside route_promise and perform the steps inside handle_response
p
}
/// <https://w3c.github.io/clipboard-apis/#dom-clipboard-writetext>
fn WriteText(&self, data: DOMString, can_gc: CanGc) -> Rc<Promise> {
// Step 1 Let realm be this's relevant realm.
// Step 2 Let p be a new promise in realm.
let p = Promise::new(&self.global(), can_gc);
// Step 3 Run the following steps in parallel:
// TODO write permission could be removed from spec
// Step 3.1 Let r be the result of running check clipboard write permission.
// Step 3.2 If r is false, then:
// Step 3.2.1 Queue a global task on the permission task source, given realms global object,
// to reject p with "NotAllowedError" DOMException in realm.
// Step 3.2.2 Abort these steps.
let trusted_promise = TrustedPromise::new(p.clone());
let bytes = Vec::from(data);
// Step 3.3 Queue a global task on the clipboard task source,
// given realms global object, to perform the below steps:
self.global().task_manager().clipboard_task_source().queue(
task!(write_to_system_clipboard: move || {
let promise = trusted_promise.root();
let global = promise.global();
// Step 3.3.1 Let itemList be an empty sequence<Blob>.
let mut item_list = Vec::new();
// Step 3.3.2 Let textBlob be a new Blob created with: type attribute set to "text/plain;charset=utf-8",
// and its underlying byte sequence set to the UTF-8 encoding of data.
let text_blob = Blob::new(
&global,
BlobImpl::new_from_bytes(bytes, "text/plain;charset=utf-8".into()),
CanGc::note(),
);
// Step 3.3.3 Add textBlob to itemList.
item_list.push(text_blob);
// Step 3.3.4 Let option be set to "unspecified".
let option = PresentationStyle::Unspecified;
// Step 3.3.5 Write blobs and option to the clipboard with itemList and option.
write_blobs_and_option_to_the_clipboard(global.as_window(), item_list, option);
// Step 3.3.6 Resolve p.
promise.resolve_native(&(), CanGc::note());
}),
);
// Step 3.4 Return p.
p
}
}
impl RoutedPromiseListener<Result<String, String>> for Clipboard {
fn handle_response(
&self,
response: Result<String, String>,
promise: &Rc<Promise>,
can_gc: CanGc,
) {
let global = self.global();
let text = response.unwrap_or_default();
// Step 3.4.1 For each systemClipboardItem in data:
// Step 3.4.1.1 For each systemClipboardRepresentation in systemClipboardItem:
// TODO: Arboard provide the first item that has a String representation
// Step 3.4.1.1.1 Let mimeType be the result of running the
// well-known mime type from os specific format algorithm given systemClipboardRepresentations name.
// Note: This is done by arboard, so we just convert the format to a MIME
let mime_type = Mime::from_str("text/plain").unwrap();
// Step 3.4.1.1.2 If mimeType is null, continue this loop.
// Note: Since the previous step is infallible, we don't need to handle this case
// Step 3.4.1.1.3 Let representation be a new representation.
let representation = Representation {
mime_type,
is_custom: false,
data: Promise::new_resolved(
&global,
GlobalScope::get_cx(),
DOMString::from(text),
can_gc,
),
};
// Step 3.4.1.1.4 If representations MIME type essence is "text/plain", then:
// Step 3.4.1.1.4.1 Set representations MIME type to mimeType.
// Note: Done when creating a new representation
// Step 3.4.1.1.4.2 Let representationDataPromise be the representations data.
// Step 3.4.1.1.4.3 React to representationDataPromise:
let fulfillment_handler = Box::new(RepresentationDataPromiseFulfillmentHandler {
promise: promise.clone(),
});
let rejection_handler = Box::new(RepresentationDataPromiseRejectionHandler {
promise: promise.clone(),
});
let handler = PromiseNativeHandler::new(
&global,
Some(fulfillment_handler),
Some(rejection_handler),
can_gc,
);
let realm = enter_realm(&*global);
let comp = InRealm::Entered(&realm);
representation
.data
.append_native_handler(&handler, comp, can_gc);
// Step 3.4.2 Reject p with "NotFoundError" DOMException in realm.
// Step 3.4.3 Return p.
// NOTE: We follow the same behaviour of Gecko by doing nothing if no text is available instead of rejecting p
}
}
/// <https://w3c.github.io/clipboard-apis/#write-blobs-and-option-to-the-clipboard>
fn write_blobs_and_option_to_the_clipboard(
window: &Window,
items: Vec<DomRoot<Blob>>,
_presentation_style: PresentationStyle,
) {
// TODO Step 1 Let webCustomFormats be a sequence<Blob>.
// Step 2 For each item in items:
for item in items {
// TODO support more formats than just text/plain
// Step 2.1 Let formatString be the result of running os specific well-known format given items type.
// Step 2.2 If formatString is empty then follow the below steps:
// Step 2.2.1 Let webCustomFormatString be the items type.
// Step 2.2.2 Let webCustomFormat be an empty type.
// Step 2.2.3 If webCustomFormatString starts with "web " prefix,
// then remove the "web " prefix and store the remaining string in webMimeTypeString.
// Step 2.2.4 Let webMimeType be the result of parsing a MIME type given webMimeTypeString.
// Step 2.2.5 If webMimeType is failure, then abort all steps.
// Step 2.2.6 Let webCustomFormats type's essence equal to webMimeType.
// Step 2.2.7 Set items type to webCustomFormat.
// Step 2.2.8 Append webCustomFormat to webCustomFormats.
// Step 2.3 Let payload be the result of UTF-8 decoding items underlying byte sequence.
// Step 2.4 Insert payload and presentationStyle into the system clipboard
// using formatString as the native clipboard format.
window.send_to_embedder(EmbedderMsg::SetClipboardText(
window.webview_id(),
String::from_utf8(
item.get_bytes()
.expect("No bytes found for Blob created by caller"),
)
.expect("DOMString contained invalid bytes"),
));
}
// TODO Step 3 Write web custom formats given webCustomFormats.
// Needs support to arbitrary formats inside arboard
}