LibWeb: Implement navigator.clipboard.read

This commit is contained in:
Timothy Flynn 2025-05-01 18:00:42 -04:00 committed by Tim Flynn
commit 912009f6b3
Notes: github-actions[bot] 2025-05-02 21:47:14 +00:00
5 changed files with 163 additions and 1 deletions

View file

@ -4,6 +4,7 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Runtime/Array.h>
#include <LibJS/Runtime/Realm.h>
#include <LibTextCodec/Decoder.h>
#include <LibWeb/Bindings/ClipboardPrototype.h>
@ -163,6 +164,149 @@ static bool check_clipboard_write_permission(JS::Realm& realm)
return false;
}
// https://w3c.github.io/clipboard-apis/#dom-clipboard-readtext
GC::Ref<WebIDL::Promise> Clipboard::read(ClipboardUnsanitizedFormats formats)
{
// 1. Let realm be this's relevant realm.
auto& realm = HTML::relevant_realm(*this);
// 2. Let p be a new promise in realm.
auto promise = WebIDL::create_promise(realm);
// 3. If formats is not empty, then:
if (formats.unsanitized.has_value()) {
// 1. For each format in formats["unsanitized"]:
for (auto const& format : *formats.unsanitized) {
// FIXME: 1. If format is not in optional unsanitized data types, then reject p with format "NotAllowedError"
// DOMException in realm.
(void)format;
}
}
// 4. Run the following steps in parallel:
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [&realm, promise, formats = move(formats)]() mutable {
// 1. Let r be the result of running check clipboard read permission.
auto result = check_clipboard_read_permission(realm);
// 2. If r is false, then:
if (!result) {
// 1. Queue a global task on the permission task source, given realms global object, to reject p with
// "NotAllowedError" DOMException in realm.
queue_global_task(HTML::Task::Source::Permissions, realm.global_object(), GC::create_function(realm.heap(), [&realm, promise]() mutable {
HTML::TemporaryExecutionContext execution_context { realm };
WebIDL::reject_promise(realm, promise, WebIDL::NotAllowedError::create(realm, "Clipboard reading is only allowed through user activation"_string));
}));
// 2. Abort these steps.
return;
}
// 3. Let data be a copy of the system clipboard data.
as<HTML::Window>(realm.global_object()).page().request_clipboard_entries(GC::create_function(realm.heap(), [&realm, promise, formats = move(formats)](Vector<SystemClipboardItem> data) mutable {
HTML::TemporaryExecutionContext execution_context { realm };
// 4. Let items be a sequence<clipboard item>.
GC::RootVector<JS::Value> items(realm.heap());
// 5. For each systemClipboardItem in data:
for (auto const& system_clipboard_item : data) {
// 1. Let item be a new clipboard item.
auto item = realm.create<ClipboardItem>(realm);
// 2. For each systemClipboardRepresentation in systemClipboardItem:
for (auto const& system_clipboard_representation : system_clipboard_item.system_clipboard_representations) {
// 1. Let mimeType be the result of running the well-known mime type from os specific format
// algorithm given systemClipboardRepresentations name.
auto mime_type = os_specific_well_known_format(system_clipboard_representation.mime_type);
// 2. If mimeType is null, continue this loop.
if (mime_type.is_empty())
continue;
auto decoder = TextCodec::decoder_for("UTF-8"sv);
auto string = MUST(TextCodec::convert_input_to_utf8_using_given_decoder_unless_there_is_a_byte_order_mark(*decoder, system_clipboard_representation.data));
// 3. Let representation be a new representation.
ClipboardItem::Representation representation {
// 4. Set representations MIME type to mimeType.
.mime_type = move(mime_type),
// 7. Resolve representations data with systemClipboardRepresentations data.
.data = WebIDL::create_resolved_promise(realm, JS::PrimitiveString::create(realm.vm(), move(string))),
};
// 5. Let isUnsanitized be false.
auto is_unsanitized = false;
// 6. If formats is not empty, then:
if (formats.unsanitized.has_value()) {
// 1. For each format in formats["unsanitized"]:
for (auto const& format : *formats.unsanitized) {
// 1. If format is equal to MIME type, set isUnsanitized to true.
if (format == representation.mime_type) {
is_unsanitized = true;
break;
}
}
}
// 8. The user agent, MAY sanitize representations data, unless representations MIME type's essence
// is "image/png", which should remain unsanitized to preserve meta data, or if it satisfies the
// below conditions:
// 1. representations MIME type is in optional unsanitized data types list.
// 2. isUnsanitized is true.
// FIXME: Sanitization is an underspecified spec feature. See:
// https://github.com/w3c/clipboard-apis/issues/73
(void)is_unsanitized;
// 9. Append representation to items list of representations.
item->append_representation(move(representation));
// 10. Set isUnsanitized to false.
is_unsanitized = false;
}
// 3. If items list of representations size is greater than 0, append item to items.
if (!item->representations().is_empty())
items.append(item);
}
// 6. If items has a size > 0, then:
if (!items.is_empty()) {
// FIXME: 1. Let firstItem be items[0]
// FIXME: 2. Run the read web custom format algorithm given firstItem.
}
// 7. Else:
else {
// FIXME: 1. Let customItem be a new clipboard item.
// FIXME: 2. Run the read web custom format algorithm given customItem.
// FIXME: 3. If customItems list of representations size is greater than 0, append customItem to items.
}
// 8. Queue a global task on the clipboard task source, given realms global object, to perform the below steps:
queue_global_task(HTML::Task::Source::Clipboard, realm.global_object(), GC::create_function(realm.heap(), [&realm, promise, items = move(items)]() mutable {
HTML::TemporaryExecutionContext execution_context { realm };
// 1. Let clipboardItems be a sequence<ClipboardItem>.
// 2. For each clipboard item underlyingItem of items:
// 1. Let clipboardItem be the result of running the steps of create a ClipboardItem object given
// underlyingItem and realm.
// 2. Append clipboardItem to clipboardItems.
// NOTE: We do not perform this song-and-dance of having ClipboardItem and "clipboard item" being
// separate concepts. But we do still need to convert the RootVector to a JS array.
auto clipboard_items = JS::Array::create_from(realm, items);
// 2. Resolve p with clipboardItems.
WebIDL::resolve_promise(realm, promise, clipboard_items);
}));
}));
}));
// 5. Return p.
return promise;
}
// https://w3c.github.io/clipboard-apis/#dom-clipboard-readtext
GC::Ref<WebIDL::Promise> Clipboard::read_text()
{

View file

@ -15,6 +15,11 @@
namespace Web::Clipboard {
struct ClipboardUnsanitizedFormats {
// FIXME: This should not actually be an Optional, but the IDL generator creates it as such.
Optional<Vector<String>> unsanitized;
};
class Clipboard final : public DOM::EventTarget {
WEB_PLATFORM_OBJECT(Clipboard, DOM::EventTarget);
GC_DECLARE_ALLOCATOR(Clipboard);
@ -23,6 +28,7 @@ public:
static WebIDL::ExceptionOr<GC::Ref<Clipboard>> construct_impl(JS::Realm&);
virtual ~Clipboard() override;
GC::Ref<WebIDL::Promise> read(ClipboardUnsanitizedFormats formats = {});
GC::Ref<WebIDL::Promise> read_text();
GC::Ref<WebIDL::Promise> write(GC::RootVector<GC::Root<ClipboardItem>>&);

View file

@ -6,8 +6,12 @@ typedef sequence<ClipboardItem> ClipboardItems;
// https://w3c.github.io/clipboard-apis/#clipboard
[SecureContext, Exposed=Window]
interface Clipboard : EventTarget {
[FIXME] Promise<ClipboardItems> read();
Promise<ClipboardItems> read(optional ClipboardUnsanitizedFormats formats = {});
Promise<DOMString> readText();
Promise<undefined> write(ClipboardItems data);
Promise<undefined> writeText(DOMString data);
};
dictionary ClipboardUnsanitizedFormats {
sequence<DOMString> unsanitized;
};

View file

@ -89,6 +89,12 @@ WebIDL::ExceptionOr<GC::Ref<ClipboardItem>> ClipboardItem::construct_impl(JS::Re
return clipboard_item;
}
void ClipboardItem::append_representation(Representation representation)
{
m_types.append(representation.mime_type);
m_representations.append(move(representation));
}
// https://w3c.github.io/clipboard-apis/#dom-clipboarditem-gettype
WebIDL::ExceptionOr<GC::Ref<WebIDL::Promise>> ClipboardItem::get_type(String const& type)
{

View file

@ -46,7 +46,9 @@ public:
Bindings::PresentationStyle presentation_style() const { return m_presentation_style; }
Vector<String> const& types() const { return m_types; }
Vector<Representation> const& representations() const { return m_representations; }
void append_representation(Representation);
WebIDL::ExceptionOr<GC::Ref<WebIDL::Promise>> get_type(String const& type);